recipes/backend/src/translation.rs

322 lines
7.7 KiB
Rust

use std::{borrow::Borrow, fs::File, sync::LazyLock};
use common::utils;
use ron::de::from_reader;
use serde::Deserialize;
use strum::EnumCount;
use strum_macros::EnumCount;
use tracing::{Level, event};
use crate::consts;
#[derive(Debug, Clone, EnumCount, Deserialize)]
pub enum Sentence {
MainTitle = 0,
CreateNewRecipe,
PrivateRecipes,
UntitledRecipe,
Name,
EmailAddress,
Password,
SignOut,
Save,
NotLoggedIn,
DatabaseError,
TemplateError,
// Sign in page.
SignInMenu,
SignInTitle,
SignInButton,
WrongEmailOrPassword,
// Sign up page.
SignUpMenu,
SignUpTitle,
SignUpButton,
SignUpEmailSent,
SignUpEmailTitle,
SignUpFollowEmailLink,
SignUpEmailValidationSuccess,
SignUpValidationExpired,
SignUpValidationErrorTryAgain,
SignUpClosed,
ChooseAPassword,
ReEnterPassword,
AccountMustBeValidatedFirst,
InvalidEmail,
PasswordDontMatch,
InvalidPassword,
EmailAlreadyTaken,
UnableToSendEmail,
// Validation.
ValidationSuccessful,
ValidationExpired,
ValidationErrorTryToSignUpAgain,
ValidationError,
ValidationUserAlreadyExists,
// Reset password page.
LostPassword,
AskResetChooseNewPassword,
AskResetButton,
AskResetAlreadyLoggedInError,
AskResetEmailAlreadyResetError,
AskResetEmailTitle,
AskResetFollowEmailLink,
AskResetEmailSent,
AskResetTokenMissing,
AskResetTokenExpired,
PasswordReset,
EmailUnknown,
UnableToSendResetEmail,
// Profile
ProfileTitle,
ProfileEmail,
ProfileDefaultServings,
ProfileNewPassword,
ProfileFollowEmailTitle,
ProfileFollowEmailLink,
ProfileEmailSent,
ProfileSaved,
// Recipe.
RecipeNotAllowedToEdit,
RecipeNotAllowedToView,
RecipeNotFound,
RecipeTitle,
RecipeDescription,
RecipeServings,
RecipeEstimatedTime,
RecipeDifficulty,
RecipeDifficultyEasy,
RecipeDifficultyMedium,
RecipeDifficultyHard,
RecipeTags,
RecipeLanguage,
RecipeIsPublic,
RecipeDelete,
RecipeAddAGroup,
RecipeRemoveGroup,
RecipeGroupName,
RecipeGroupComment,
RecipeAddAStep,
RecipeRemoveStep,
RecipeStepAction,
RecipeAddAnIngredient,
RecipeRemoveIngredient,
RecipeIngredientName,
RecipeIngredientQuantity,
RecipeIngredientUnit,
RecipeIngredientComment,
RecipeDeleteConfirmation,
RecipeGroupDeleteConfirmation,
RecipeStepDeleteConfirmation,
RecipeIngredientDeleteConfirmation,
// View Recipe.
RecipeOneServing,
RecipeSomeServings,
RecipeEstimatedTimeMinAbbreviation,
// Calendar.
CalendarMondayAbbreviation,
CalendarTuesdayAbbreviation,
CalendarWednesdayAbbreviation,
CalendarThursdayAbbreviation,
CalendarFridayAbbreviation,
CalendarSaturdayAbbreviation,
CalendarSundayAbbreviation,
CalendarJanuary,
CalendarFebruary,
CalendarMarch,
CalendarApril,
CalendarMay,
CalendarJune,
CalendarJuly,
CalendarAugust,
CalendarSeptember,
CalendarOctober,
CalendarNovember,
CalendarDecember,
CalendarAddToPlanner,
CalendarAddToPlannerSuccess,
CalendarAddToPlannerAlreadyExists,
CalendarDateFormat, // See https://docs.rs/chrono/latest/chrono/format/strftime/index.html.
CalendarAddIngredientsToShoppingList,
CalendarRemoveIngredientsFromShoppingList,
CalendarUnschedule,
CalendarUnscheduleConfirmation,
}
pub const DEFAULT_LANGUAGE_CODE: &str = "en";
pub const PLACEHOLDER_SUBSTITUTE: &str = "{}";
#[derive(Debug, Clone)]
pub struct Tr {
lang: &'static Language,
}
impl Tr {
pub fn new(code: &str) -> Self {
Self {
lang: get_language_translation(code),
}
}
pub fn t<T>(&self, sentence: T) -> &'static str
where
T: Borrow<Sentence>,
{
self.lang.get(sentence)
}
pub fn tp(&self, sentence: Sentence, params: &[Box<dyn ToString + Send>]) -> String {
let text = self.lang.get(sentence);
let params_as_string: Vec<String> = params.iter().map(|p| p.to_string()).collect();
utils::substitute(
text,
PLACEHOLDER_SUBSTITUTE,
&params_as_string
.iter()
.map(AsRef::as_ref)
.collect::<Vec<_>>(),
)
}
pub fn current_lang_code(&self) -> &str {
&self.lang.code
}
pub fn current_lang_and_territory_code(&self) -> String {
format!("{}-{}", self.lang.code, self.lang.territory)
}
}
// #[macro_export]
// macro_rules! t {
// ($self:expr, $str:expr) => {
// $self.t($str)
// };
// ($self:expr, $str:expr, $( $x:expr ),+ ) => {
// {
// let mut result = $self.t($str);
// $( result = result.replacen("{}", &$x.to_string(), 1); )+
// result
// }
// };
// }
#[derive(Debug, Deserialize)]
struct StoredLanguage {
code: String,
territory: String,
name: String,
translation: Vec<(Sentence, String)>,
}
#[derive(Debug)]
struct Language {
code: String,
territory: String,
name: String,
translation: Vec<String>,
}
impl Language {
pub fn from_stored_language(stored_language: StoredLanguage) -> Self {
Self {
code: stored_language.code,
territory: stored_language.territory,
name: stored_language.name,
translation: {
let mut translation = vec![String::new(); Sentence::COUNT];
for (sentence, text) in stored_language.translation {
translation[sentence as usize] = text;
}
translation
},
}
}
pub fn get<T>(&'static self, sentence: T) -> &'static str
where
T: Borrow<Sentence>,
{
let sentence_cloned: Sentence = sentence.borrow().clone();
let text: &str = self
.translation
.get(sentence_cloned as usize)
.unwrap()
.as_ref();
if text.is_empty() && self.code != DEFAULT_LANGUAGE_CODE {
return get_language_translation(DEFAULT_LANGUAGE_CODE).get(sentence);
}
text
}
}
pub fn available_languages() -> Vec<(&'static str, &'static str)> {
TRANSLATIONS
.iter()
.map(|tr| (tr.code.as_ref(), tr.name.as_ref()))
.collect()
}
pub fn available_codes() -> Vec<&'static str> {
TRANSLATIONS.iter().map(|tr| tr.code.as_ref()).collect()
}
fn get_language_translation(code: &str) -> &'static Language {
for lang in TRANSLATIONS.iter() {
if lang.code == code {
return lang;
}
}
event!(
Level::WARN,
"Unable to find translation for language {}",
code
);
if code != DEFAULT_LANGUAGE_CODE {
get_language_translation(DEFAULT_LANGUAGE_CODE)
} else {
// 'DEFAULT_LANGUAGE_CODE' must exist.
panic!("Unable to find language {}", code);
}
}
static TRANSLATIONS: LazyLock<Vec<Language>> =
LazyLock::new(|| match File::open(consts::TRANSLATION_FILE) {
Ok(file) => {
let stored_languages: Vec<StoredLanguage> = from_reader(file).unwrap_or_else(|error| {
{
panic!(
"Failed to read translation file {}: {}",
consts::TRANSLATION_FILE,
error
)
}
});
stored_languages
.into_iter()
.map(Language::from_stored_language)
.collect()
}
Err(error) => {
panic!(
"Failed to open translation file {}: {}",
consts::TRANSLATION_FILE,
error
)
}
});