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(&self, sentence: T) -> &'static str where T: Borrow, { self.lang.get(sentence) } pub fn tp(&self, sentence: Sentence, params: &[Box]) -> String { let text = self.lang.get(sentence); let params_as_string: Vec = params.iter().map(|p| p.to_string()).collect(); utils::substitute( text, PLACEHOLDER_SUBSTITUTE, ¶ms_as_string .iter() .map(AsRef::as_ref) .collect::>(), ) } 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, } 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(&'static self, sentence: T) -> &'static str where T: Borrow, { 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> = LazyLock::new(|| match File::open(consts::TRANSLATION_FILE) { Ok(file) => { let stored_languages: Vec = 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 ) } });