Skip to content

Commit

Permalink
feat(i18n): ✨ added fn on translations manager to get string translat…
Browse files Browse the repository at this point in the history
…ions

Avoids unnecessary deserialize-then-serialize.
  • Loading branch information
arctic-hen7 committed Sep 8, 2021
1 parent fc8eeaf commit 649a65d
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 25 deletions.
22 changes: 7 additions & 15 deletions packages/perseus-actix-web/src/translations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use crate::Options;
use actix_web::{web, HttpRequest, HttpResponse};
use perseus::TranslationsManager;

/// The handler for calls to `.perseus/page/*`. This will manage returning errors and the like.
/// The handler for calls to `.perseus/translations/{locale}`. This will manage returning errors and the like. THe JSON body returned
/// from this does NOT include the `locale` key, just a `HashMap<String, String>` of the translations themselves.
pub async fn translations<T: TranslationsManager>(
req: HttpRequest,
opts: web::Data<Options>,
Expand All @@ -11,23 +12,14 @@ pub async fn translations<T: TranslationsManager>(
let locale = req.match_info().query("locale");
// Check if the locale is supported
if opts.locales.is_supported(locale) {
// Create a translator for that locale (and hope the implementation is caching for sanity)
// We know that it''s supported, which means a failure is a 500
let translator = translations_manager
.get_translator_for_locale(locale.to_string())
.await;
let translator = match translator {
Ok(translator) => translator,
Err(err) => return HttpResponse::InternalServerError().body(err.to_string()),
};
// Serialize that into a JSON response
let json = serde_json::to_string(&translator);
let json = match json {
Ok(json) => json,
// We know that the locale is supported, so any failure to get translations is a 500
let translations = translations_manager.get_translations_str_for_locale(locale.to_string()).await;
let translations = match translations {
Ok(translations) => translations,
Err(err) => return HttpResponse::InternalServerError().body(err.to_string()),
};

HttpResponse::Ok().body(json)
HttpResponse::Ok().body(translations)
} else {
HttpResponse::NotFound().body("locale not supported".to_string())
}
Expand Down
10 changes: 6 additions & 4 deletions packages/perseus/src/client_translations_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::shell::fetch;
use crate::Locales;
use crate::Translator;
use std::rc::Rc;
use std::collections::HashMap;

/// Manages translations in the app shell. This handles fetching translations from the server as well as caching for performance.
/// This is distinct from `TranslationsManager` in that it operates on the client-side rather than on the server. This optimizes for
Expand Down Expand Up @@ -39,10 +40,11 @@ impl ClientTranslationsManager {
let translator = match translator_str {
Ok(translator_str) => match translator_str {
Some(translator_str) => {
// All good, deserialize the translator
let translator = serde_json::from_str::<Translator>(&translator_str);
match translator {
Ok(translator) => translator,
// All good, deserialize the translations
let translations = serde_json::from_str::<HashMap<String, String>>(&translator_str);
match translations {
// And then turn them into a translator
Ok(translations) => Translator::new(locale.to_string(), translations),
Err(err) => {
bail!(ErrorKind::AssetSerFailed(asset_url, err.to_string()))
}
Expand Down
31 changes: 25 additions & 6 deletions packages/perseus/src/translations_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::Translator;
use error_chain::{bail, error_chain};
use std::collections::HashMap;
use std::fs;
use std::sync::Arc;

// This has no foreign links because everything to do with config management should be isolated and generic
error_chain! {
Expand All @@ -29,12 +30,15 @@ error_chain! {
}
}

/// A trait for systems that manage where to put configuration files. At simplest, we'll just write them to static files, but they're
/// more likely to be stored on a CMS.
/// A trait for systems that manage where to put translations. At simplest, we'll just write them to static files, but they might also
/// be stored in a CMS.
#[async_trait::async_trait]
pub trait TranslationsManager: Clone {
/// Gets translations for the given locale.
/// Gets a translator for the given locale.
async fn get_translator_for_locale(&self, locale: String) -> Result<Translator>;
/// Gets the translations in stirng format for the given locale (avoids deserialize-then-serialize). This should NOT include the
/// translator's `locale` property, it should simply be a `HashMap<String, String>` in JSON format.
async fn get_translations_str_for_locale(&self, locale: String) -> Result<String>;
}

/// The default translations manager. This will store static files in the specified location on disk. This should be suitable for
Expand All @@ -43,16 +47,26 @@ pub trait TranslationsManager: Clone {
#[derive(Clone)]
pub struct FsTranslationsManager {
root_path: String,
/// A map of locales to cached translators. This decreases the number of file reads significantly for the locales specified. This
/// does NOT cache dynamically, and will only cache the requested locales.
cached_translators: HashMap<String, Arc<Translator>>,
/// The locales being cached for easier access.
cached_locales: Vec<String>
}
// TODO implement caching
impl FsTranslationsManager {
/// Creates a new filesystem translations manager. You should provide a path like `/translations` here.
pub fn new(root_path: String) -> Self {
Self { root_path }
Self {
root_path,
cached_translators: HashMap::new(),
cached_locales: Vec::new()
}
}
}
#[async_trait::async_trait]
impl TranslationsManager for FsTranslationsManager {
async fn get_translator_for_locale(&self, locale: String) -> Result<Translator> {
async fn get_translations_str_for_locale(&self, locale: String) -> Result<String> {
// The file must be named as the locale it describes
let asset_path = format!("{}/{}.json", self.root_path, locale);
let translations_str = match fs::metadata(&asset_path) {
Expand All @@ -63,9 +77,14 @@ impl TranslationsManager for FsTranslationsManager {
}
Err(err) => bail!(ErrorKind::ReadFailed(locale.to_string(), err.to_string())),
};

Ok(translations_str)
}
async fn get_translator_for_locale(&self, locale: String) -> Result<Translator> {
let translations_str = self.get_translations_str_for_locale(locale.clone()).await?;
// We expect the translations defined there, but not the locale itself
let translations = serde_json::from_str::<HashMap<String, String>>(&translations_str)
.map_err(|err| ErrorKind::SerializationFailed(locale.to_string(), err.to_string()))?;
.map_err(|err| ErrorKind::SerializationFailed(locale.clone(), err.to_string()))?;
let translator = Translator::new(locale, translations);

Ok(translator)
Expand Down

0 comments on commit 649a65d

Please sign in to comment.