Avoid to use an hash map in translations
authorGreg Burri <greg.burri@gmail.com>
Tue, 7 Jan 2025 15:47:24 +0000 (16:47 +0100)
committerGreg Burri <greg.burri@gmail.com>
Tue, 7 Jan 2025 15:47:24 +0000 (16:47 +0100)
Cargo.lock
backend/Cargo.toml
backend/scss/style.scss
backend/src/html_templates.rs
backend/src/main.rs
backend/src/services/ron.rs
backend/src/translation.rs
backend/templates/base_with_header.html
backend/templates/recipe_edit.html
backend/translation.ron

index 82ec42d..d9421b6 100644 (file)
@@ -122,9 +122,9 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.84"
+version = "0.1.85"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b1244b10dcd56c92219da4e14caa97e312079e185f04ba3eea25061561dc0a0"
+checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1756,18 +1756,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
 [[package]]
 name = "pin-project"
-version = "1.1.7"
+version = "1.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95"
+checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916"
 dependencies = [
  "pin-project-internal",
 ]
 
 [[package]]
 name = "pin-project-internal"
-version = "1.1.7"
+version = "1.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
+checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1776,9 +1776,9 @@ dependencies = [
 
 [[package]]
 name = "pin-project-lite"
-version = "0.2.15"
+version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
 
 [[package]]
 name = "pin-utils"
@@ -1930,9 +1930,10 @@ dependencies = [
  "rinja",
  "rinja_axum",
  "ron",
- "rustc-hash",
  "serde",
  "sqlx",
+ "strum",
+ "strum_macros",
  "thiserror 2.0.9",
  "tokio",
  "tower",
@@ -2210,9 +2211,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.134"
+version = "1.0.135"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d"
+checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
 dependencies = [
  "itoa",
  "memchr",
@@ -2572,6 +2573,25 @@ version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
+[[package]]
+name = "strum"
+version = "0.26.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
+
+[[package]]
+name = "strum_macros"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn",
+]
+
 [[package]]
 name = "subtle"
 version = "2.6.1"
index 9a47b14..0030c25 100644 (file)
@@ -23,7 +23,6 @@ ron = "0.8"
 serde = { version = "1.0", features = ["derive"] }
 
 itertools = "0.14"
-rustc-hash = "2.1"
 clap = { version = "4", features = ["derive"] }
 
 sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "chrono"] }
@@ -34,6 +33,8 @@ rinja_axum = "0.3"
 argon2 = { version = "0.5", features = ["default", "std"] }
 rand_core = { version = "0.6", features = ["std"] }
 rand = "0.8"
+strum = "0.26"
+strum_macros = "0.26"
 
 lettre = { version = "0.11", default-features = false, features = [
     "smtp-transport",
index fa47f09..b1c26fc 100644 (file)
@@ -76,6 +76,12 @@ body {
         font-size: 0.5em;
     }
 
+    .drag-handle {
+        width: 20px;
+        height: 20px;
+        background-color: gray;
+    }
+
     .main-container {
         display: flex;
         flex-direction: row;
index ab8ca0a..1679e6a 100644 (file)
@@ -2,7 +2,7 @@ use rinja_axum::Template;
 
 use crate::{
     data::model,
-    translation::{Sentence, Tr},
+    translation::{self, Sentence, Tr},
 };
 
 pub struct Recipes {
index 610ea49..274349a 100644 (file)
@@ -231,7 +231,7 @@ async fn translation(
     let language = if let Some(user) = user {
         user.lang
     } else {
-        let available_codes = Tr::available_codes();
+        let available_codes = translation::available_codes();
         let jar = CookieJar::from_headers(req.headers());
         match jar.get(consts::COOKIE_LANG_NAME) {
             Some(lang) if available_codes.contains(&lang.value()) => lang.value().to_string(),
index 0d31103..f93c3b1 100644 (file)
@@ -249,7 +249,7 @@ pub async fn set_language(
     Extension(user): Extension<Option<model::User>>,
     ExtractRon(ron): ExtractRon<common::ron_api::SetRecipeLanguage>,
 ) -> Result<StatusCode> {
-    if !crate::translation::Tr::available_codes()
+    if !crate::translation::available_codes()
         .iter()
         .any(|&l| l == ron.lang)
     {
index 2049ac2..aede01d 100644 (file)
@@ -1,15 +1,16 @@
 use std::{fs::File, sync::LazyLock};
 
 use ron::de::from_reader;
-use rustc_hash::FxHashMap;
 use serde::Deserialize;
+use strum::EnumCount;
+use strum_macros::EnumCount;
 use tracing::{event, Level};
 
 use crate::consts;
 
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Clone)]
+#[derive(Debug, Clone, EnumCount, Deserialize)]
 pub enum Sentence {
-    MainTitle,
+    MainTitle = 0,
     CreateNewRecipe,
     UnpublishedRecipes,
     UntitledRecipe,
@@ -107,6 +108,8 @@ pub enum Sentence {
     RecipeIngredientComment,
 }
 
+const DEFAULT_LANGUAGE_CODE: &str = "en";
+
 #[derive(Clone)]
 pub struct Tr {
     lang: &'static Language,
@@ -114,61 +117,48 @@ pub struct Tr {
 
 impl Tr {
     pub fn new(code: &str) -> Self {
-        for lang in TRANSLATIONS.iter() {
-            if lang.code == code {
-                return Self { lang };
-            }
+        Self {
+            lang: get_language_translation(code),
         }
-
-        event!(
-            Level::WARN,
-            "Unable to find translation for language {}",
-            code
-        );
-
-        Tr::new("en")
     }
 
     pub fn t(&self, sentence: Sentence) -> String {
-        match self.lang.translation.get(&sentence) {
-            Some(str) => str.clone(),
-            None => format!(
-                "Translation missing, lang: {}/{}, element: {:?}",
-                self.lang.name, self.lang.code, sentence
-            ),
-        }
+        //&'static str {
+        self.lang.get(sentence).to_string()
+        // match self.lang.translation.get(&sentence) {
+        //     Some(str) => str.clone(),
+        //     None => format!(
+        //         "Translation missing, lang: {}/{}, element: {:?}",
+        //         self.lang.name, self.lang.code, sentence
+        //     ),
+        // }
     }
 
     pub fn tp(&self, sentence: Sentence, params: &[Box<dyn ToString + Send>]) -> String {
-        match self.lang.translation.get(&sentence) {
-            Some(str) => {
-                let mut result = str.clone();
-                for p in params {
-                    result = result.replacen("{}", &p.to_string(), 1);
-                }
-                result
-            }
-            None => format!(
-                "Translation missing, lang: {}/{}, element: {:?}",
-                self.lang.name, self.lang.code, sentence
-            ),
+        // match self.lang.translation.get(&sentence) {
+        //     Some(str) => {
+        //         let mut result = str.clone();
+        //         for p in params {
+        //             result = result.replacen("{}", &p.to_string(), 1);
+        //         }
+        //         result
+        //     }
+        //     None => format!(
+        //         "Translation missing, lang: {}/{}, element: {:?}",
+        //         self.lang.name, self.lang.code, sentence
+        //     ),
+        // }
+        let text = self.lang.get(sentence);
+        let mut result = text.to_string();
+        for p in params {
+            result = result.replacen("{}", &p.to_string(), 1);
         }
+        result
     }
 
     pub fn current_lang_code(&self) -> &str {
         &self.lang.code
     }
-
-    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()
-    }
 }
 
 // #[macro_export]
@@ -185,22 +175,98 @@ impl Tr {
 //     };
 // }
 
-#[derive(Debug, Deserialize, Clone)]
+#[derive(Debug, Deserialize)]
+struct StoredLanguage {
+    code: String,
+    name: String,
+    translation: Vec<(Sentence, String)>,
+}
+
+#[derive(Debug)]
 struct Language {
     code: String,
     name: String,
-    translation: FxHashMap<Sentence, String>,
+    translation: Vec<String>,
+}
+
+impl Language {
+    pub fn from_stored_language(stored_language: StoredLanguage) -> Self {
+        println!("!!!!!!!!!!!! {:?}", &stored_language.code);
+        Self {
+            code: stored_language.code,
+            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: Sentence) -> &'static str {
+        let text: &str = self
+            .translation
+            .get(sentence.clone() 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) => from_reader(file).unwrap_or_else(|error| {
-            panic!(
-                "Failed to read translation file {}: {}",
-                consts::TRANSLATION_FILE,
-                error
-            )
-        }),
+        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 {}: {}",
index dfca040..c90a2b4 100644 (file)
@@ -21,7 +21,7 @@
         {% endmatch %}
 
         <select id="select-website-language">
-        {% for lang in Tr::available_languages() %}
+        {% for lang in translation::available_languages() %}
             <option value="{{ lang.0 }}"
             {%+ if tr.current_lang_code() == lang.0 %}
                 selected
index 7e2b677..4b89f93 100644 (file)
@@ -59,7 +59,7 @@
 
     <label for="select-language">{{ tr.t(Sentence::RecipeLanguage) }}</label>
     <select id="select-language">
-    {% for lang in Tr::available_languages() %}
+    {% for lang in translation::available_languages() %}
         <option value="{{ lang.0 }}"
         {%+ if recipe.lang == lang.0 %}
             selected
@@ -86,6 +86,8 @@
 
     <div id="hidden-templates">
         <div class="group">
+            <div class="drag-handle"></div>
+
             <label for="input-group-name">{{ tr.t(Sentence::RecipeGroupName) }}</label>
             <input class="input-group-name" type="text" />
 
         </div>
 
         <div class="step">
+            <div class="drag-handle"></div>
+
             <label for="text-area-step-action">{{ tr.t(Sentence::RecipeStepAction) }}</label>
             <textarea class="text-area-step-action"></textarea>
 
index ab0ad78..710682f 100644 (file)
     (
         code: "en",
         name: "English",
-        translation: {
-            MainTitle: "Cooking Recipes",
-            CreateNewRecipe: "Create a new recipe",
-            UnpublishedRecipes: "Unpublished recipes",
-            UntitledRecipe: "Untitled recipe",
-
-            Name: "Name",
-            EmailAddress: "Email address",
-            Password: "Password",
-
-            SignOut: "Sign out",
-            Save: "Save",
-            NotLoggedIn: "No logged in",
-
-            DatabaseError: "Database error",
-
-            SignInMenu: "Sign in",
-            SignInTitle: "Sign in",
-            SignInButton: "Sign in",
-            WrongEmailOrPassword: "Wrong email or password",
-            AccountMustBeValidatedFirst: "This account must be validated first",
-            InvalidEmail: "Invalid email",
-            PasswordDontMatch: "Passwords don't match",
-            InvalidPassword: "Password must have at least {} characters",
-            EmailAlreadyTaken: "This email is not available",
-            UnableToSendEmail: "Unable to send the validation email",
-
-            ValidationSuccessful: "Email validation successful",
-            ValidationExpired: "The validation has expired. Try to sign up again with the same email",
-            ValidationErrorTryToSignUpAgain: "Validation error. Try to sign up again with the same email",
-            ValidationError: "Validation error",
-            ValidationUserAlreadyExists: "User already exists",
-
-            SignUpMenu: "Sign up",
-            SignUpTitle: "Sign up",
-            SignUpButton: "Sign up",
-            SignUpEmailSent: "An email has been sent, follow the link to validate your account",
-            SignUpFollowEmailLink: "Follow this link to confirm your inscription: {}",
-            SignUpEmailValidationSuccess: "Email validation successful, your account has been created",
-            SignUpValidationExpired: "The validation has expired. Try to sign up again",
-            SignUpValidationErrorTryAgain: "Validation error. Try to sign up again",
-            ChooseAPassword: "Choose a password (minimum {} characters)",
-            ReEnterPassword: "Re-enter password",
-
-            LostPassword: "Lost password",
-            AskResetButton: "Ask reset",
-            AskResetAlreadyLoggedInError: "Can't ask to reset password when already logged in",
-            AskResetEmailAlreadyResetError: "The password has already been reset for this email",
-            AskResetFollowEmailLink: "Follow this link to reset your password: {}",
-            AskResetEmailSent: "An email has been sent, follow the link to reset your password",
-            AskResetTokenMissing: "Reset token missing",
-            AskResetTokenExpired: "Token expired, try to reset password again",
-            PasswordReset: "Your password has been reset",
-            EmailUnknown: "Email unknown",
-            UnableToSendResetEmail: "Unable to send the reset password email",
-
-            ProfileTitle: "Profile",
-            ProfileEmail: "Email (need to be revalidated if changed)",
-            ProfileNewPassword: "New password (minimum {} characters)",
-            ProfileFollowEmailLink: "Follow this link to validate this email address: {}",
-            ProfileEmailSent: "An email has been sent, follow the link to validate your new email",
-            ProfileSaved: "Profile saved",
-
-            RecipeNotAllowedToEdit: "Not allowed to edit this recipe",
-            RecipeNotAllowedToView: "Not allowed the view the recipe {}",
-            RecipeNotFound: "Recipe not found",
-            RecipeTitle : "Title",
-            RecipeDescription : "Description",
-            RecipeServings : "Servings",
-            RecipeEstimatedTime : "Estimated time [min]",
-            RecipeDifficulty : "Difficulty",
-            RecipeDifficultyEasy : "Easy",
-            RecipeDifficultyMedium : "Medium",
-            RecipeDifficultyHard : "Hard",
-            RecipeTags : "Tags",
-            RecipeLanguage : "Language",
-            RecipeIsPublished : "Is published",
-            RecipeDelete : "Delete recipe",
-            RecipeAddAGroup : "Add a group",
-            RecipeRemoveGroup : "Remove group",
-            RecipeGroupName : "Name",
-            RecipeGroupComment : "Comment",
-            RecipeAddAStep : "Add a step",
-            RecipeRemoveStep : "Remove step",
-            RecipeStepAction : "Action",
-            RecipeAddAnIngredient : "Add an ingredient",
-            RecipeRemoveIngredient : "Remove ingredient",
-            RecipeIngredientName : "Name",
-            RecipeIngredientQuantity : "Quantity",
-            RecipeIngredientUnit : "Unit",
-            RecipeIngredientComment : "Comment",
-        }
+        translation: [
+            (MainTitle, "Cooking Recipes"),
+            (CreateNewRecipe, "Create a new recipe"),
+            (UnpublishedRecipes, "Unpublished recipes"),
+            (UntitledRecipe, "Untitled recipe"),
+
+            (Name, "Name"),
+            (EmailAddress, "Email address"),
+            (Password, "Password"),
+
+            (SignOut, "Sign out"),
+            (Save, "Save"),
+            (NotLoggedIn, "No logged in"),
+
+            (DatabaseError, "Database error"),
+
+            (SignInMenu, "Sign in"),
+            (SignInTitle, "Sign in"),
+            (SignInButton, "Sign in"),
+            (WrongEmailOrPassword, "Wrong email or password"),
+            (AccountMustBeValidatedFirst, "This account must be validated first"),
+            (InvalidEmail, "Invalid email"),
+            (PasswordDontMatch, "Passwords don't match"),
+            (InvalidPassword, "Password must have at least {} characters"),
+            (EmailAlreadyTaken, "This email is not available"),
+            (UnableToSendEmail, "Unable to send the validation email"),
+
+            (ValidationSuccessful, "Email validation successful"),
+            (ValidationExpired, "The validation has expired. Try to sign up again with the same email"),
+            (ValidationErrorTryToSignUpAgain, "Validation error. Try to sign up again with the same email"),
+            (ValidationError, "Validation error"),
+            (ValidationUserAlreadyExists, "User already exists"),
+
+            (SignUpMenu, "Sign up"),
+            (SignUpTitle, "Sign up"),
+            (SignUpButton, "Sign up"),
+            (SignUpEmailSent, "An email has been sent), follow the link to validate your account"),
+            (SignUpFollowEmailLink, "Follow this link to confirm your inscription, {}"),
+            (SignUpEmailValidationSuccess, "Email validation successful), your account has been created"),
+            (SignUpValidationExpired, "The validation has expired. Try to sign up again"),
+            (SignUpValidationErrorTryAgain, "Validation error. Try to sign up again"),
+            (ChooseAPassword, "Choose a password (minimum {} characters)"),
+            (ReEnterPassword, "Re-enter password"),
+
+            (LostPassword, "Lost password"),
+            (AskResetButton, "Ask reset"),
+            (AskResetAlreadyLoggedInError, "Can't ask to reset password when already logged in"),
+            (AskResetEmailAlreadyResetError, "The password has already been reset for this email"),
+            (AskResetFollowEmailLink, "Follow this link to reset your password, {}"),
+            (AskResetEmailSent, "An email has been sent), follow the link to reset your password"),
+            (AskResetTokenMissing, "Reset token missing"),
+            (AskResetTokenExpired, "Token expired), try to reset password again"),
+            (PasswordReset, "Your password has been reset"),
+            (EmailUnknown, "Email unknown"),
+            (UnableToSendResetEmail, "Unable to send the reset password email"),
+
+            (ProfileTitle, "Profile"),
+            (ProfileEmail, "Email (need to be revalidated if changed)"),
+            (ProfileNewPassword, "New password (minimum {} characters)"),
+            (ProfileFollowEmailLink, "Follow this link to validate this email address, {}"),
+            (ProfileEmailSent, "An email has been sent), follow the link to validate your new email"),
+            (ProfileSaved, "Profile saved"),
+
+            (RecipeNotAllowedToEdit, "Not allowed to edit this recipe"),
+            (RecipeNotAllowedToView, "Not allowed the view the recipe {}"),
+            (RecipeNotFound, "Recipe not found"),
+            (RecipeTitle, "Title"),
+            (RecipeDescription, "Description"),
+            (RecipeServings, "Servings"),
+            (RecipeEstimatedTime, "Estimated time [min]"),
+            (RecipeDifficulty, "Difficulty"),
+            (RecipeDifficultyEasy, "Easy"),
+            (RecipeDifficultyMedium, "Medium"),
+            (RecipeDifficultyHard, "Hard"),
+            (RecipeTags, "Tags"),
+            (RecipeLanguage, "Language"),
+            (RecipeIsPublished, "Is published"),
+            (RecipeDelete, "Delete recipe"),
+            (RecipeAddAGroup, "Add a group"),
+            (RecipeRemoveGroup, "Remove group"),
+            (RecipeGroupName, "Name"),
+            (RecipeGroupComment, "Comment"),
+            (RecipeAddAStep, "Add a step"),
+            (RecipeRemoveStep, "Remove step"),
+            (RecipeStepAction, "Action"),
+            (RecipeAddAnIngredient, "Add an ingredient"),
+            (RecipeRemoveIngredient, "Remove ingredient"),
+            (RecipeIngredientName, "Name"),
+            (RecipeIngredientQuantity, "Quantity"),
+            (RecipeIngredientUnit, "Unit"),
+            (RecipeIngredientComment, "Comment"),
+        ]
     ),
     (
         code: "fr",
         name: "Français",
-        translation: {
-            MainTitle: "Recettes de Cuisine",
-            CreateNewRecipe: "Créer une nouvelle recette",
-            UnpublishedRecipes: "Recettes non-publiés",
-            UntitledRecipe: "Recette sans nom",
-
-            Name: "Nom",
-            EmailAddress: "Adresse email",
-            Password: "Mot de passe",
-
-            SignOut: "Se déconnecter",
-            Save: "Sauvegarder",
-            NotLoggedIn: "Pas connecté",
-
-            DatabaseError: "Erreur de la base de données",
-
-            SignInMenu: "Se connecter",
-            SignInTitle: "Se connecter",
-            SignInButton: "Se connecter",
-            WrongEmailOrPassword: "Mot de passe ou email invalide",
-            AccountMustBeValidatedFirst: "Ce compte doit d'abord être validé",
-            InvalidEmail: "Adresse email invalide",
-            PasswordDontMatch: "Les mots de passe ne correspondent pas",
-            InvalidPassword: "Le mot de passe doit avoir au moins {} caractères",
-            EmailAlreadyTaken: "Cette adresse email n'est pas disponible",
-            UnableToSendEmail: "L'email de validation n'a pas pu être envoyé",
-
-            ValidationSuccessful: "Email validé avec succès",
-            ValidationExpired: "La validation a expiré. Essayez de vous inscrire à nouveau avec la même adresse email",
-            ValidationErrorTryToSignUpAgain: "Erreur de validation. Essayez de vous inscrire à nouveau avec la même adresse email",
-            ValidationError: "Erreur de validation",
-            ValidationUserAlreadyExists: "Utilisateur déjà existant",
-
-            SignUpMenu: "S'inscrire",
-            SignUpTitle: "Inscription",
-            SignUpButton: "Valider",
-            SignUpEmailSent: "Un email a été envoyé, suivez le lien pour valider votre compte",
-            SignUpFollowEmailLink: "Suivez ce lien pour valider votre inscription: {}",
-            SignUpEmailValidationSuccess: "La validation de votre email s'est déroulée avec succès, votre compte a été créé",
-            SignUpValidationExpired: "La validation a expiré. Essayez de vous inscrire à nouveau",
-            SignUpValidationErrorTryAgain: "Erreur de validation. Essayez de vous inscrire à nouveau",
-            ChooseAPassword: "Choisir un mot de passe (minimum {} caractères)",
-            ReEnterPassword: "Entrez à nouveau le mot de passe",
-
-            LostPassword: "Mot de passe perdu",
-            AskResetButton: "Demander la réinitialisation",
-            AskResetAlreadyLoggedInError: "Impossible de demander une réinitialisation du mot de passe lorsque déjà connecté",
-            AskResetEmailAlreadyResetError: "Le mot de passe a déjà été réinitialisé pour cette adresse email",
-            AskResetFollowEmailLink: "Suivez ce lien pour réinitialiser votre mot de passe: {}",
-            AskResetEmailSent: "Un email a été envoyé, suivez le lien pour réinitialiser votre mot de passe",
-            AskResetTokenMissing: "Jeton de réinitialisation manquant",
-            AskResetTokenExpired: "Jeton expiré, essayez de réinitialiser votre mot de passe à nouveau",
-            PasswordReset: "Votre mot de passe a été réinitialisé",
-            EmailUnknown: "Email inconnu",
-            UnableToSendResetEmail: "Impossible d'envoyer l'email pour la réinitialisation du mot de passe",
-
-            ProfileTitle: "Profile",
-            ProfileEmail: "Email (doit être revalidé si changé)",
-            ProfileNewPassword: "Nouveau mot de passe (minimum {} caractères)",
-            ProfileFollowEmailLink: "Suivez ce lien pour valider l'adresse email: {}",
-            ProfileEmailSent: "Un email a été envoyé, suivez le lien pour valider la nouvelle adresse email",
-            ProfileSaved: "Profile sauvegardé",
-
-            RecipeNotAllowedToEdit: "Vous n'êtes pas autorisé à éditer cette recette",
-            RecipeNotAllowedToView: "Vous n'êtes pas autorisé à voir la recette {}",
-            RecipeNotFound: "Recette non-trouvée",
-            RecipeTitle : "Titre",
-            RecipeDescription : "Description",
-            RecipeServings : "Nombre de personnes",
-            RecipeEstimatedTime : "Temps estimé",
-            RecipeDifficulty : "Difficulté",
-            RecipeDifficultyEasy : "Facile",
-            RecipeDifficultyMedium : "Moyen",
-            RecipeDifficultyHard : "Difficile",
-            RecipeTags : "Tags",
-            RecipeLanguage : "Langue",
-            RecipeIsPublished : "Est publié",
-            RecipeDelete : "Supprimer la recette",
-            RecipeAddAGroup : "Ajouter un groupe",
-            RecipeRemoveGroup : "Supprimer le groupe",
-            RecipeGroupName : "Nom",
-            RecipeGroupComment : "Commentaire",
-            RecipeAddAStep : "Ajouter une étape",
-            RecipeRemoveStep : "Supprimer l'étape",
-            RecipeStepAction : "Action",
-            RecipeAddAnIngredient : "Ajouter un ingrédient",
-            RecipeRemoveIngredient : "Supprimer l'ingrédient",
-            RecipeIngredientName : "Nom",
-            RecipeIngredientQuantity : "Quantité",
-            RecipeIngredientUnit : "Unité",
-            RecipeIngredientComment : "Commentaire",
-        }
+        translation: [
+            (MainTitle, "Recettes de Cuisine"),
+            (CreateNewRecipe, "Créer une nouvelle recette"),
+            (UnpublishedRecipes, "Recettes non-publiés"),
+            (UntitledRecipe, "Recette sans nom"),
+
+            (Name, "Nom"),
+            (EmailAddress, "Adresse email"),
+            (Password, "Mot de passe"),
+
+            (SignOut, "Se déconnecter"),
+            (Save, "Sauvegarder"),
+            (NotLoggedIn, "Pas connecté"),
+
+            (DatabaseError, "Erreur de la base de données"),
+
+            (SignInMenu, "Se connecter"),
+            (SignInTitle, "Se connecter"),
+            (SignInButton, "Se connecter"),
+            (WrongEmailOrPassword, "Mot de passe ou email invalide"),
+            (AccountMustBeValidatedFirst, "Ce compte doit d'abord être validé"),
+            (InvalidEmail, "Adresse email invalide"),
+            (PasswordDontMatch, "Les mots de passe ne correspondent pas"),
+            (InvalidPassword, "Le mot de passe doit avoir au moins {} caractères"),
+            (EmailAlreadyTaken, "Cette adresse email n'est pas disponible"),
+            (UnableToSendEmail, "L'email de validation n'a pas pu être envoyé"),
+
+            (ValidationSuccessful, "Email validé avec succès"),
+            (ValidationExpired, "La validation a expiré. Essayez de vous inscrire à nouveau avec la même adresse email"),
+            (ValidationErrorTryToSignUpAgain, "Erreur de validation. Essayez de vous inscrire à nouveau avec la même adresse email"),
+            (ValidationError, "Erreur de validation"),
+            (ValidationUserAlreadyExists, "Utilisateur déjà existant"),
+
+            (SignUpMenu, "S'inscrire"),
+            (SignUpTitle, "Inscription"),
+            (SignUpButton, "Valider"),
+            (SignUpEmailSent, "Un email a été envoyé), suivez le lien pour valider votre compte"),
+            (SignUpFollowEmailLink, "Suivez ce lien pour valider votre inscription, {}"),
+            (SignUpEmailValidationSuccess, "La validation de votre email s'est déroulée avec succès), votre compte a été créé"),
+            (SignUpValidationExpired, "La validation a expiré. Essayez de vous inscrire à nouveau"),
+            (SignUpValidationErrorTryAgain, "Erreur de validation. Essayez de vous inscrire à nouveau"),
+            (ChooseAPassword, "Choisir un mot de passe (minimum {} caractères)"),
+            (ReEnterPassword, "Entrez à nouveau le mot de passe"),
+
+            (LostPassword, "Mot de passe perdu"),
+            (AskResetButton, "Demander la réinitialisation"),
+            (AskResetAlreadyLoggedInError, "Impossible de demander une réinitialisation du mot de passe lorsque déjà connecté"),
+            (AskResetEmailAlreadyResetError, "Le mot de passe a déjà été réinitialisé pour cette adresse email"),
+            (AskResetFollowEmailLink, "Suivez ce lien pour réinitialiser votre mot de passe, {}"),
+            (AskResetEmailSent, "Un email a été envoyé), suivez le lien pour réinitialiser votre mot de passe"),
+            (AskResetTokenMissing, "Jeton de réinitialisation manquant"),
+            (AskResetTokenExpired, "Jeton expiré), essayez de réinitialiser votre mot de passe à nouveau"),
+            (PasswordReset, "Votre mot de passe a été réinitialisé"),
+            (EmailUnknown, "Email inconnu"),
+            (UnableToSendResetEmail, "Impossible d'envoyer l'email pour la réinitialisation du mot de passe"),
+
+            (ProfileTitle, "Profile"),
+            (ProfileEmail, "Email (doit être revalidé si changé)"),
+            (ProfileNewPassword, "Nouveau mot de passe (minimum {} caractères)"),
+            (ProfileFollowEmailLink, "Suivez ce lien pour valider l'adresse email, {}"),
+            (ProfileEmailSent, "Un email a été envoyé), suivez le lien pour valider la nouvelle adresse email"),
+            (ProfileSaved, "Profile sauvegardé"),
+
+            (RecipeNotAllowedToEdit, "Vous n'êtes pas autorisé à éditer cette recette"),
+            (RecipeNotAllowedToView, "Vous n'êtes pas autorisé à voir la recette {}"),
+            (RecipeNotFound, "Recette non-trouvée"),
+            (RecipeTitle, "Titre"),
+            (RecipeDescription, "Description"),
+            (RecipeServings, "Nombre de personnes"),
+            (RecipeEstimatedTime, "Temps estimé"),
+            (RecipeDifficulty, "Difficulté"),
+            (RecipeDifficultyEasy, "Facile"),
+            (RecipeDifficultyMedium, "Moyen"),
+            (RecipeDifficultyHard, "Difficile"),
+            (RecipeTags, "Tags"),
+            (RecipeLanguage, "Langue"),
+            (RecipeIsPublished, "Est publié"),
+            (RecipeDelete, "Supprimer la recette"),
+            (RecipeAddAGroup, "Ajouter un groupe"),
+            (RecipeRemoveGroup, "Supprimer le groupe"),
+            (RecipeGroupName, "Nom"),
+            (RecipeGroupComment, "Commentaire"),
+            (RecipeAddAStep, "Ajouter une étape"),
+            (RecipeRemoveStep, "Supprimer l'étape"),
+            (RecipeStepAction, "Action"),
+            (RecipeAddAnIngredient, "Ajouter un ingrédient"),
+            (RecipeRemoveIngredient, "Supprimer l'ingrédient"),
+            (RecipeIngredientName, "Nom"),
+            (RecipeIngredientQuantity, "Quantité"),
+            (RecipeIngredientUnit, "Unité"),
+            (RecipeIngredientComment, "Commentaire"),
+        ]
     )
 ]
\ No newline at end of file