From a6f9f533a594c833180fba2cc0fd5a5dca067a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20PHELIPOT?= Date: Sat, 18 Jan 2020 00:43:18 +0100 Subject: [PATCH] Localization system for Voxygen - Added a localization system in voxygen - Support English and French languages - Added a configuration option in the interface settings to change the language --- CHANGELOG.md | 3 + Cargo.lock | 7 + assets/voxygen/i18n/en.ron | 337 +++++++++++++++++++++ assets/voxygen/i18n/fr_FR.ron | 303 +++++++++++++++++++ voxygen/Cargo.toml | 1 + voxygen/src/hud/character_window.rs | 37 ++- voxygen/src/hud/esc_menu.rs | 25 +- voxygen/src/hud/mod.rs | 151 +++++----- voxygen/src/hud/quest.rs | 16 +- voxygen/src/hud/settings_window.rs | 394 +++++++++++++------------ voxygen/src/hud/skillbar.rs | 26 +- voxygen/src/hud/social.rs | 54 ++-- voxygen/src/hud/spell.rs | 19 +- voxygen/src/i18n.rs | 113 +++++++ voxygen/src/main.rs | 15 +- voxygen/src/menu/char_selection/mod.rs | 9 +- voxygen/src/menu/char_selection/ui.rs | 129 +++++--- voxygen/src/menu/main/mod.rs | 13 +- voxygen/src/menu/main/ui.rs | 72 ++--- voxygen/src/session.rs | 33 ++- voxygen/src/settings.rs | 20 +- 21 files changed, 1357 insertions(+), 420 deletions(-) create mode 100644 assets/voxygen/i18n/en.ron create mode 100644 assets/voxygen/i18n/fr_FR.ron create mode 100644 voxygen/src/i18n.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 348f8f3b6e..fd3d3c2c1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added randomly selected Loading Screen background images - Added options to disable clouds and to use cheaper water rendering - Added client-side character saving +- Added a localization system to provide multi-language support + to voxygen +- Added French language for Voxygen ### Changed diff --git a/Cargo.lock b/Cargo.lock index 4446d54ec5..434a8f7f4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -760,6 +760,11 @@ dependencies = [ "syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "deunicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "directories" version = "2.0.2" @@ -3251,6 +3256,7 @@ dependencies = [ "cpal 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "criterion 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "deunicode 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "directories 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "dispatch 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "dot_vox 4.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3654,6 +3660,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum daggy 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9293a0da7d1bc1f30090ece4d9f9de79a07be7302ddb00e5eb1fefb6ee6409e2" "checksum deflate 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)" = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4" "checksum derivative 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "942ca430eef7a3806595a6737bc388bf51adb888d3fc0dd1b50f1c170167ee3a" +"checksum deunicode 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca8a0f5bbdedde60605d0719b998e282af68e2b1c50203110211fe4abe857560" "checksum directories 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "551a778172a450d7fc12e629ca3b0428d00f6afa9a43da1b630d54604e97371c" "checksum dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" "checksum dirs-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron new file mode 100644 index 0000000000..064997dd40 --- /dev/null +++ b/assets/voxygen/i18n/en.ron @@ -0,0 +1,337 @@ +/// Translation document instructions +/// +/// In order to keep localization documents readible please follow the following +/// rules: +/// - separate the string map sections using a commentary describing the purpose +/// of the next section +/// - prepend multi-line strings with a commentary +/// - append one blank lines after a multi-line strings and two after sections +/// +/// To add a new language in Veloren, just write an additional `.ron` file in +/// `assets/voxygen/i18n` and that's it! + +/// Localization for "global" English +VoxygenLocalization( + metadata: ( + language_name: "English", + language_identifier: "en", + ), + convert_utf8_to_ascii: false, + string_map: { + /// Start Common section + // Texts used in multiple locations with the same formatting + "common.username": "username", + "common.singleplayer": "Singleplayer", + "common.multiplayer": "Multiplayer", + "common.servers": "Servers", + "common.quit": "Quit", + "common.settings": "Settings", + "common.languages": "Languages", + "common.interface": "Interface", + "common.gameplay": "Gameplay", + "common.controls": "Controls", + "common.video": "Video", + "common.sound": "Sound", + "common.resume": "Resume", + "common.characters": "Characters", + "common.close": "Close", + "common.yes": "Yes", + "common.no": "No", + "common.back": "Back", + "common.create": "Create", + "common.okay": "Okay", + "common.disclaimer": "Disclaimer", + "common.cancel": "Cancel", + "common.none": "None", + + // Message when connection to the server is lost + "common.connection_lost": r#"Connection lost! +Did the server restart? +Is the client up to date?"#, + + + "common.races.orc": "Orc", + "common.races.human": "Human", + "common.races.dwarf": "Dwarf", + "common.races.elf": "Elf", + "common.races.undead": "Undead", + "common.races.danari": "Danari", + + "common.weapons.axe": "Axe", + "common.weapons.sword": "Sword", + "common.weapons.staff": "Staff", + "common.weapons.bow": "Bow", + "common.weapons.hammer": "Hammer", + /// End Common section + + + /// Start Main screen section + "main.connecting": "Connecting", + "main.creating_world": "Creating world", + + // Welcome notice that appears the first time Veloren is started + "main.notice": r#"Welcome to the alpha version of Veloren! + +Before you dive into the fun, please keep a few things in mind: + +- This is a very early alpha. Expect bugs, extremely unfinished gameplay, unpolished mechanics, and missing features. +- If you have constructive feedback or bug reports, you can contact us via Reddit, GitLab, or our community Discord server. +- Veloren is licensed under the GPL 3 open-source licence. That means you're free to play, modify, and redistribute the game however you wish (provided derived work is also under GPL 3). +- Veloren is a non-profit community project, and everybody working on it is a volunteer. +If you like what you see, you're welcome to join the development or art teams! +- 'Voxel RPG' is a genre in its own right. First-person shooters used to be called Doom clones. + +Like them, we're trying to build a niche. This game is not a clone, and its development will diverge from existing games in the future. + +Thanks for taking the time to read this notice, we hope you enjoy the game! + +~ The Veloren Devs"#, + + // Login process description + "main.login_process": r#"Information on the Login Process: + +The name you put in will be your character name ingame. + +Character names and appearances will be saved on your computer. + +Levels/Items are not saved yet."#, + + + /// End Main screen section + + + /// Start HUD Section + "hud.do_not_show_on_startup": "Don't show this on Startup", + "hud.show_tips": "Show Tips", + "hud.quests": "Quests", + "hud.you_died": "You Died", + + "hud.press_key_to_show_keybindings_fmt": "Press {key} to show keybindings", + "hud.press_key_to_show_debug_info_fmt": "Press {key} to show debug info", + "hud.press_key_to_toggle_keybindings_fmt": "Press {key} to toogle keybindings", + "hud.press_key_to_toggle_debug_info_fmt": "Press {key} to toogle debug info", + + // Respawn message + "hud.press_key_to_respawn": r#"Press {key} to respawn at your Waypoint. + +Press Enter, type in /waypoint and confirm to set it here."#, + + // Welcome message + "hud.welcome": r#"Welcome to the Veloren Alpha!, + + +Some tips before you start: + + +MOST IMPORTANTLY: To set your respawn point type /waypoint into the chat. + +This can also be done when you are already dead! + + +Press F1 to see the available key commands. + +Type /help into the chat to see chat commands + + +There are chests and other objects randomly spawning in the World! + +Right-Click to collect them. + +To actually use whatever you loot from those chests open your inventory with 'B'. + +Double click the items in your bag to use or equip them. + +Throw them away by clicking them once and clicking outside of the bag + + +Nights can get pretty dark in Veloren. + +Light your lantern by typing /lantern into the chat + + +Want to free your cursor to close this window? Press TAB! + + +Enjoy your stay in the World of Veloren."#, + + "hud.settings.general": "General", + "hud.settings.help_window": "Help Window", + "hud.settings.debug_info": "Debug Info", + "hud.settings.tips_on_startup": "Tips-On-Startup", + "hud.settings.ui_scale": "UI-Scale", + "hud.settings.relative_scaling": "Relative Scaling", + "hud.settings.custom_scaling": "Custom Scaling", + "hud.settings.crosshair": "Crosshair", + "hud.settings.transparency": "Transparency", + "hud.settings.hotbar": "Hotbar", + "hud.settings.toggle_bar_experience": "Toggle Experience Bar", + "hud.settings.toggle_shortcuts": "Toggle Shortcuts", + "hud.settings.toggle_bar_experience": "Toggle Shortcuts", + "hud.settings.scrolling_combat_text": "Scrolling Combat Text", + "hud.settings.single_damage_number": "Single Damage Numbers", + "hud.settings.cumulated_damage": "Cumulated Damage", + "hud.settings.incoming_damage": "Incoming Damage", + "hud.settings.cumulated_incoming_damage": "Cumulated Incoming Damage", + "hud.settings.energybar_numbers": "Energybar Numbers", + "hud.settings.values": "Values", + "hud.settings.percentages": "Percentages", + "hud.settings.chat": "Chat", + "hud.settings.background_transparency": "Background Transparency", + + "hud.settings.pan_sensitivity": "Pan Sensitivity", + "hud.settings.zoom_sensitivity": "Zoom Sensitivity", + "hud.settings.invert_scroll_zoom": "Invert Scroll Zoom", + "hud.settings.invert_mouse_y_axis": "Invert Mouse Y Axis", + + "hud.settings.view_distance": "View Distance", + "hud.settings.maximum_fps": "Maximum FPS", + "hud.settings.fov": "Field of View (deg)", + "hud.settings.antialiasing_mode": "AntiAliasing Mode", + "hud.settings.cloud_rendering_mode": "Cloud Rendering Mode", + "hud.settings.fluid_rendering_mode": "Fluid Rendering Mode", + "hud.settings.fluid_rendering_mode.cheap": "Cheap", + "hud.settings.fluid_rendering_mode.shiny": "Shiny", + "hud.settings.cloud_rendering_mode.regular": "Regular", + + "hud.settings.music_volume": "Music Volume", + "hud.settings.sound_effect_volume": "Sound Effects Volume", + "hud.settings.audio_device": "Audio Device", + + // Control list + "hud.settings.control_names": r#"Free Cursor +Toggle Help Window +Toggle Interface +Toggle FPS and Debug Info +Take Screenshot +Toggle Nametags +Toggle Fullscreen + + +Move Forward +Move Left +Move Right +Move Backwards + +Jump + +Glider + +Dodge + +Roll + +Climb + +Climb down + +Auto Walk + +Sheathe/Draw Weapons + +Put on/Remove Helmet + +Sit + +Mount + +Interact + + +Basic Attack +Secondary Attack/Block/Aim + + +Skillbar Slot 1 +Skillbar Slot 2 +Skillbar Slot 3 +Skillbar Slot 4 +Skillbar Slot 5 +Skillbar Slot 6 +Skillbar Slot 7 +Skillbar Slot 8 +Skillbar Slot 9 +Skillbar Slot 10 + + +Pause Menu +Settings +Social +Map +Spellbook +Character +Questlog +Bag + + + +Send Chat Message +Scroll Chat + + +Chat commands: + +/alias [Name] - Change your Chat Name +/tp [Name] - Teleports you to another player +/jump - Offset your position +/goto - Teleport to a position +/kill - Kill yourself +/pig - Spawn pig NPC +/wolf - Spawn wolf NPC +/help - Display chat commands"#, + + "hud.social": "Social", + "hud.social.online": "Online", + "hud.social.friends": "Friends", + "hud.social.not_yet_available": "Not yet available", + "hud.social.Faction": "Faction", + "hud.social.play_online_fmt": "{nb_player} player(s) online", + + "hud.spell": "Spell", + /// End HUD section + + + /// Start chracter selection section + "char_selection.delete_permanently": "Permanently delete this Character?", + "char_selection.change_server": "Change Server", + "char_selection.enter_world": "Enter World", + "char_selection.logout": "Logout", + "char_selection.create_new_charater": "Create New Character", + "char_selection.character_creation": "Character Creation", + + "char_selection.human_default": "Human Default", + "char_selection.level_fmt": "Level {level_nb}", + "char_selection.uncanny_valley": "Uncanny Valley", + "char_selection.plains_of_uncertainty": "Plains of Uncertainty", + "char_selection.beard": "Beard", + "char_selection.hair_style": "Beard", + "char_selection.hair_color": "Beard", + "char_selection.chest_color": "Chest color", + "char_selection.eye_color": "Eye color", + "char_selection.skin": "Skin", + "char_selection.eyebrows": "Eyebrows", + "char_selection.accessories": "Accessories", + + /// End chracter selection section + + + /// Start character window section + "character_window.character_name": "Character Name", + // Charater stats + "character_window.character_stats": r#"Stamina + +Strength + +Dexterity + +Intelligence"#, + + + /// Start character window section + + + /// Start Escape Menu Section + "esc_menu.logout": "Logout", + "esc_menu.quit_game": "Quit Game", + /// End Escape Menu Section + } +) \ No newline at end of file diff --git a/assets/voxygen/i18n/fr_FR.ron b/assets/voxygen/i18n/fr_FR.ron new file mode 100644 index 0000000000..e6f772dd35 --- /dev/null +++ b/assets/voxygen/i18n/fr_FR.ron @@ -0,0 +1,303 @@ +/// Localization for French (France locale) +VoxygenLocalization( + metadata: ( + language_name: "Français", + language_identifier: "fr_FR", + ), + convert_utf8_to_ascii: true, + string_map: { + // Common texts used in multiple locations + "common.username": "pseudo", + "common.singleplayer": "Solo", + "common.multiplayer": "Multijoueur", + "common.servers": "Serveurs", + "common.quit": "Quitter", + "common.settings": "Paramètres", + "common.languages": "Langues", + "common.interface": "Interface", + "common.gameplay": "Gameplay", + "common.controls": "Controles", + "common.video": "Video", + "common.sound": "Audio", + "common.resume": "Reprendre", + "common.characters": "Personages", + "common.close": "Fermer", + "common.create": "Créer", + "common.back": "Retour", + "common.yes": "Oui", + "common.no": "Non", + "common.okay": "Compris", + "common.cancel": "Annuler", + "common.none": "Aucun", + + "common.races.orc": "Orc", + "common.races.human": "Humain", + "common.races.dwarf": "Nain", + "common.races.elf": "Elfe", + "common.races.undead": "Mort vivant", + "common.races.danari": "Danari", + + "common.weapons.axe": "Hâche", + "common.weapons.sword": "Épée", + "common.weapons.staff": "Bâton", + "common.weapons.bow": "Arc", + "common.weapons.hammer": "Marteau", + + // Message when connection to the server is lost + "common.connection_lost": r#"Connexion perdue! +Le serveur a-il redémarré? +Le client est-il à jour?"#, + + + // Main screen texts + "main.connecting": "Connexion", + "main.creating_world": "Création du monde", + "main.notice": r#"Bienvenue dans la version alpha de Veloren! + +Avant de commencer à vous amuser, merci de garder les choses suivantes en tête: + +- Il s'agit d'une version alpha très jeune. Attendez-vous à des bugs, un gameplay non terminé, des mécaniques non peaufinées et des fonctionalités manquantes. +- Si vous avez des retours constructifs ou avez detecté un bug, vous pouvez nous contacter via Reddit, GitLab ou notre communauté Discord. +- Veloren est un logiciel open-source sour licence GPL3. Cela signifit que vous êtes libre de jouer, modfier et redistribuer le jeu comme il vous semble (licence contaminante sous GPL 3 pour toute modification) +- Veloren est un projet communautaire à but non-lucratif développé par des bénévolles. +Si vous apprecier ce jeu, vous êtes les bienvenues pour rejoindre les équipes de développement ou d'artistes! +- Le genre 'Voxel RPG' est un genre à part entiere. Les FPS étaient appelé Doom-like. De la même façon, nous essayons de construire un genre à part entière. Ce jeu n'est pas un clone et ses mechaniques changeront au cours du developpement. + +Merci d'avoir pris le temps de lire cette notice, nous esperons que vous apprecierez le jeu! + +~ L'équipe de Veloren"#, + + + "main.login_process": r#"Information sur la procédure de connexion: + +L'identifiant de connexion sera votre pseudo en jeu. + +Le nom et l'apparence que vous choisirez seront +sauvegardés sur votre ordinateur. + +Les niveaux/objets ne sont pas sauvegardés."#, + + + // HUD texts + "hud.you_died": "Vous êtes mort", + "hud.do_not_show_on_startup": "Ne pas afficher au démarage", + "hud.press_key_to_show_keybindings_fmt": "Appuyer sur {key} pour afficher les contrôles", + "hud.settings.toggle_shortcuts": "Activer les raccourcis", + "hud.show_tips": "Voir les astuces", + "hud.quests": "Quêtes", + "hud.spell": "Sorts", + "hud.press_key_to_toggle_debug_info_fmt": "Appuyer sur {key} pour activer les informations de debogage", + "hud.press_key_to_show_debug_info_fmt": "Appuyer sur {key} pour afficher les informations de debogage", + "hud.press_key_to_toggle_keybindings_fmt": "Appuyer sur {key} pour afficher les contrôles", + + + /// Respawn message + "hud.press_key_to_respawn": r#"Appuyer sur {key} pour revivre à votre point de repère. + +Appuyez sur Entrée, taper /waypoint pour configurer le point de repère à l'endroit actuel"#, + + + /// Welcome message + "hud.welcome": r#"Bienvenue dans la version Alpha de Veloren! + + +Quelques astuces avant de démarrer: + + +POINT LE PLUS IMPORTANT: Pour configurer votre point de resurection tapez +/waypoint dans le chat. (y-compris si vous être déjà mort!) + + +Appuyer sur F1 pour voir les commandes disponibles. + +Tapez /help dans le chat pour voir les commandes du chat. + + +Des coffres et autres objets sont disposés aléatoirement dans le monde! + +Utilisez le click droit pour le collecter. + +Pour utilisez ce que vous ramassez des coffres, ouvrez votre inventaire avec 'B'. + +Double cliquez sur les éléments de votre sac pour les utiliser ou les équiper. + +Jettez-les en cliquant sur un element puis en cliquant en dehors du sac. + +Les nuits peuvent être très sombre à Veloren. + +Allumez votre lanterne en tapant /lantern dans le chat. + +Vous souhaitez libérer votre souris pour fermer cette fenêtre? Tapez sur TAB! + +Profitez de votre séjour dans le monde de Veloren."#, + + "hud.settings.general": "Général", + "hud.settings.help_window": "Fenêtre d'aide", + "hud.settings.debug_info": "Information de débogage", + "hud.settings.tips_on_startup": "Astuces au démarrage", + + "hud.settings.ui_scale": "Echelle de l'interface", + "hud.settings.relative_scaling": "Echelle relative", + "hud.settings.custom_scaling": "Echelle personalisée", + + "hud.settings.crosshair": "Réticule", + "hud.settings.transparency": "Transparence", + "hud.settings.hotbar": "Barre active", + "hud.settings.toggle_bar_experience": "Activer la barre d'experience", + + "hud.settings.scrolling_combat_text": "Texte de combat défillant", + "hud.settings.single_damage_number": "Dégat adversaire (par dégat)", + "hud.settings.cumulated_damage": "Dégat adversaire (cumulé)", + "hud.settings.incoming_damage": "Dégat personnage (par dégat)", + "hud.settings.cumulated_incoming_damage": "Dégat personnage (cumulé)", + + "hud.settings.energybar_numbers": "Nombre des barres d'energie", + "hud.settings.none": "Aucuns", + "hud.settings.values": "Valeurs", + "hud.settings.percentages": "Pourcentages", + + "hud.settings.chat": "Tchat", + "hud.settings.background_transparency": "Transparence du fond", + + "hud.settings.pan_sensitivity": "Sensibilité de la souris", + "hud.settings.zoom_sensitivity": "Sensibilité du zoom", + "hud.settings.invert_scroll_zoom": "Inverser la molette", + "hud.settings.invert_mouse_y_axis": "Inverser l'axe Y", + + "hud.settings.view_distance": "Distance d'affichage", + "hud.settings.maximum_fps": "Limite FPS", + "hud.settings.fov": "Champs de vision (FOV)", + "hud.settings.antialiasing_mode": "Mode anticrénelage", + "hud.settings.cloud_rendering_mode": "Rendu des nuages", + "hud.settings.fluid_rendering_mode": "Rendu des fluides", + "hud.settings.fluid_rendering_mode.cheap": "Rapide", + "hud.settings.fluid_rendering_mode.shiny": "Qualité", + "hud.settings.cloud_rendering_mode.regular": "Normal", + + "hud.settings.music_volume": "Volume de la musique", + "hud.settings.sound_effect_volume": "Volume des effets", + "hud.settings.audio_device": "Périphérique audio", + + "hud.settings.control_names": r#"Libérer le curseur +Activer la fenêtre d'aide +Activer l'interface +Activer la fenêtre de débogage +Prendre une capture d'écran +Activer les noms de personnage +Activer le mode plein écran + + +Avancer +Aller à gauche +Aller à droite +Aller en arriere + +Sauter + +Planer + +Esquiver + +Roullade + +Escalader + +Desescalader + +Marche automatique + +Ranger/Sortir les armes + +Mettre/Enlever le casque + +S'assoir + +Monter + +Intéragir + + +Attaque basique +Attaque secondaire/Bloquer/Viser + + +Barre de compétence 1 +Barre de compétence 2 +Barre de compétence 3 +Barre de compétence 4 +Barre de compétence 5 +Barre de compétence 6 +Barre de compétence 7 +Barre de compétence 8 +Barre de compétence 9 +Barre de compétence 10 + + +Pause +Paramètres +Social +Carte +Livre de sorts +Personnages +Livre de quêtes +Sac à dos + + + +Envoyer un message +Défiler dans le tchat + + +Commandes du tchat: + +/alias [Nom] - Changer de nom +/tp [Nom] - Se téléporter vers un autre joueur +/jump - Sauter à partir de votre position +/goto - Se téléporter à une position +/kill - Se suicider +/pig - Faire aparaitre un cochon PNJ +/wolf - Faire aparaitre un loup PNJ +/help - Afficher les commandes de tchat"#, + + + "hud.social": "Social", + "hud.social.friends": "Amis", + "hud.social.Faction": "Faction", + "hud.social.online": "Jeu en ligne", + "hud.social.not_yet_available": "Pas encore disponible", + "hud.social.play_online_fmt": "{nb_player} joueurs en ligne", + "char_selection.change_server": "Changer de serveur", + "char_selection.delete_permanently": "Supprimer", + + + "esc_menu.quit_game": "Quitter le jeu", + "esc_menu.logout": "Deconnexion", + + + "char_selection.beard": "Barbe", + "char_selection.hair_style": "Coupe de cheveux", + "char_selection.hair_color": "Couleur de cheveux", + "char_selection.skin": "Couleur de peau", + "char_selection.eyebrows": "Sourcils", + "char_selection.eye_color": "Couleur des yeux", + "char_selection.accessories": "Accessoires", + "char_selection.level_fmt": "Niveau {level_nb}", + "char_selection.create_new_charater": "Créer un nouveau personnage", + "char_selection.human_default": "Humain par défault", + "char_selection.uncanny_valley": "Vallée dérangeante", + "char_selection.chest_color": "Couleur du buste", + "char_selection.logout": "Se déconnecter", + "char_selection.enter_world": "Entrer dans le monde", + "char_selection.plains_of_uncertainty": "Plaines de l'incertitude", + "char_selection.character_creation": "Création de personnages", + + "character_window.character_name": "Personnage", + "character_window.character_stats": r#"Endurance + +Force + +Dexterité + +Intelligence"#, + } +) \ No newline at end of file diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index aab878573f..2db1f7d0a4 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -63,6 +63,7 @@ hashbrown = { version = "0.6.2", features = ["serde", "nightly"] } chrono = "0.4.9" rust-argon2 = "0.5" bincode = "1.2" +deunicode = "1.0" [target.'cfg(target_os = "macos")'.dependencies] dispatch = "0.1.4" diff --git a/voxygen/src/hud/character_window.rs b/voxygen/src/hud/character_window.rs index 93a579ae8f..49e20081ef 100644 --- a/voxygen/src/hud/character_window.rs +++ b/voxygen/src/hud/character_window.rs @@ -1,4 +1,5 @@ use super::{img_ids::Imgs, Fonts, Show, TEXT_COLOR, XP_COLOR}; +use crate::i18n::VoxygenLocalization; use common::comp::Stats; use conrod_core::{ color, @@ -72,18 +73,26 @@ pub struct CharacterWindow<'a> { imgs: &'a Imgs, fonts: &'a Fonts, stats: &'a Stats, + localized_strings: &'a std::sync::Arc, #[conrod(common_builder)] common: widget::CommonBuilder, } impl<'a> CharacterWindow<'a> { - pub fn new(_show: &'a Show, stats: &'a Stats, imgs: &'a Imgs, fonts: &'a Fonts) -> Self { + pub fn new( + _show: &'a Show, + stats: &'a Stats, + imgs: &'a Imgs, + fonts: &'a Fonts, + localized_strings: &'a std::sync::Arc, + ) -> Self { Self { _show, imgs, fonts, stats, + localized_strings, common: widget::CommonBuilder::default(), } } @@ -144,12 +153,16 @@ impl<'a> Widget for CharacterWindow<'a> { // Title // TODO: Use an actual character name. - Text::new("Character Name") - .mid_top_with_margin_on(state.charwindow_frame, 6.0) - .font_id(self.fonts.cyri) - .font_size(14) - .color(TEXT_COLOR) - .set(state.charwindow_title, ui); + Text::new( + &self + .localized_strings + .get("character_window.character_name"), + ) + .mid_top_with_margin_on(state.charwindow_frame, 6.0) + .font_id(self.fonts.cyri) + .font_size(14) + .color(TEXT_COLOR) + .set(state.charwindow_title, ui); // Content Alignment Rectangle::fill_with([95.0 * 4.0, 108.0 * 4.0], color::TRANSPARENT) @@ -381,13 +394,9 @@ impl<'a> Widget for CharacterWindow<'a> { // Stats Text::new( - "Stamina\n\ - \n\ - Strength\n\ - \n\ - Dexterity\n\ - \n\ - Intelligence", + &self + .localized_strings + .get("character_window.character_stats"), ) .top_left_with_margins_on(state.charwindow_rectangle, 140.0, 5.0) .font_id(self.fonts.cyri) diff --git a/voxygen/src/hud/esc_menu.rs b/voxygen/src/hud/esc_menu.rs index 88ec9bc052..2c7269a955 100644 --- a/voxygen/src/hud/esc_menu.rs +++ b/voxygen/src/hud/esc_menu.rs @@ -1,10 +1,10 @@ +use super::{img_ids::Imgs, settings_window::SettingsTab, Fonts, TEXT_COLOR}; +use crate::i18n::VoxygenLocalization; use conrod_core::{ widget::{self, Button, Image}, widget_ids, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; -use super::{img_ids::Imgs, settings_window::SettingsTab, Fonts, TEXT_COLOR}; - widget_ids! { struct Ids { esc_bg, @@ -22,15 +22,22 @@ widget_ids! { pub struct EscMenu<'a> { imgs: &'a Imgs, _fonts: &'a Fonts, + localized_strings: &'a std::sync::Arc, + #[conrod(common_builder)] common: widget::CommonBuilder, } impl<'a> EscMenu<'a> { - pub fn new(imgs: &'a Imgs, _fonts: &'a Fonts) -> Self { + pub fn new( + imgs: &'a Imgs, + _fonts: &'a Fonts, + localized_strings: &'a std::sync::Arc, + ) -> Self { Self { imgs, _fonts, + localized_strings, common: widget::CommonBuilder::default(), } } @@ -82,7 +89,7 @@ impl<'a> Widget for EscMenu<'a> { .w_h(210.0, 50.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Resume") + .label(&self.localized_strings.get("common.resume")) .label_y(conrod_core::position::Relative::Scalar(3.0)) .label_color(TEXT_COLOR) .label_font_size(20) @@ -98,7 +105,7 @@ impl<'a> Widget for EscMenu<'a> { .w_h(210.0, 50.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Settings") + .label(&self.localized_strings.get("common.settings")) .label_y(conrod_core::position::Relative::Scalar(3.0)) .label_color(TEXT_COLOR) .label_font_size(20) @@ -113,7 +120,7 @@ impl<'a> Widget for EscMenu<'a> { .w_h(210.0, 50.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Controls") + .label(&self.localized_strings.get("common.controls")) .label_y(conrod_core::position::Relative::Scalar(3.0)) .label_color(TEXT_COLOR) .label_font_size(20) @@ -128,7 +135,7 @@ impl<'a> Widget for EscMenu<'a> { .w_h(210.0, 50.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Characters") + .label(&self.localized_strings.get("common.characters")) .label_y(conrod_core::position::Relative::Scalar(3.0)) .label_color(TEXT_COLOR) .label_font_size(20) @@ -143,7 +150,7 @@ impl<'a> Widget for EscMenu<'a> { .w_h(210.0, 50.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Logout") + .label(&self.localized_strings.get("esc_menu.logout")) .label_y(conrod_core::position::Relative::Scalar(3.0)) .label_color(TEXT_COLOR) .label_font_size(20) @@ -158,7 +165,7 @@ impl<'a> Widget for EscMenu<'a> { .w_h(210.0, 50.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Quit Game") + .label(&self.localized_strings.get("esc_menu.quit_game")) .label_y(conrod_core::position::Relative::Scalar(3.0)) .label_color(TEXT_COLOR) .label_font_size(20) diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 2b5eb02237..630618b8a7 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -36,6 +36,7 @@ use spell::Spell; use crate::{ ecs::comp as vcomp, + i18n::{i18n_asset_key, LanguageMetadata, VoxygenLocalization}, render::{AaMode, CloudMode, Consts, FluidMode, Globals, Renderer}, scene::camera::Camera, //settings::ControlSettings, @@ -44,7 +45,7 @@ use crate::{ GlobalState, }; use client::{Client, Event as ClientEvent}; -use common::{comp, terrain::TerrainChunk, vol::RectRasterableVol}; +use common::{assets::load_expect, comp, terrain::TerrainChunk, vol::RectRasterableVol}; use conrod_core::{ image::Id, text::cursor::Index, @@ -225,6 +226,7 @@ pub enum Event { DropInventorySlot(usize), Logout, Quit, + ChangeLanguage(LanguageMetadata), } // TODO: Are these the possible layouts we want? @@ -529,6 +531,10 @@ impl Hud { common::util::GIT_VERSION.to_string() ); + let localized_strings = load_expect::(&i18n_asset_key( + &global_state.settings.language.selected_language, + )); + if self.show.ingame { let ecs = client.state().ecs(); let pos = ecs.read_storage::(); @@ -1208,42 +1214,7 @@ impl Hud { } // Introduction Text - let intro_text: &'static str = "Welcome to the Veloren Alpha!\n\ - \n\ - \n\ - Some tips before you start:\n\ - \n\ - \n\ - MOST IMPORTANTLY: To set your respawn point type /waypoint into the chat.\n\ - \n\ - This can also be done when you are already dead!\n\ - \n\ - \n\ - Press F1 to see the available key commands.\n\ - \n\ - Type /help into the chat to see chat commands\n\ - \n\ - \n\ - There are chests and other objects randomly spawning in the World!\n\ - \n\ - Right-Click to collect them.\n\ - \n\ - To actually use whatever you loot from those chests open your inventory with 'B'.\n\ - \n\ - Double click the items in your bag to use or equip them.\n\ - \n\ - Throw them away by clicking them once and clicking outside of the bag\n\ - \n\ - \n\ - Nights can get pretty dark in Veloren.\n\ - \n\ - Light your lantern by typing /lantern into the chat\n\ - \n\ - \n\ - Want to free your cursor to close this window? Press TAB!\n\ - \n\ - \n\ - Enjoy your stay in the World of Veloren."; + let intro_text = &localized_strings.get("hud.welcome"); if self.show.intro && !self.show.esc_menu && !self.intro_2 { match global_state.settings.gameplay.intro_show { Intro::Show => { @@ -1260,7 +1231,7 @@ impl Hud { if Button::image(self.imgs.button) .w_h(100.0, 50.0) .mid_bottom_with_margin_on(self.ids.intro_bg, 10.0) - .label("Close") + .label(&localized_strings.get("common.close")) .label_font_size(20) .label_color(TEXT_COLOR) .hover_image(self.imgs.button_hover) @@ -1297,7 +1268,7 @@ impl Hud { { self.never_show = !self.never_show }; - Text::new("Don't show this on Startup") + Text::new(&localized_strings.get("hud.do_not_show_on_startup")) .right_from(self.ids.intro_check, 10.0) .font_size(10) .font_id(self.fonts.cyri) @@ -1343,7 +1314,7 @@ impl Hud { if Button::image(self.imgs.button) .w_h(100.0, 50.0) .mid_bottom_with_margin_on(self.ids.intro_bg, 10.0) - .label("Close") + .label(&localized_strings.get("common.close")) .label_font_size(20) .label_color(TEXT_COLOR) .hover_image(self.imgs.button_hover) @@ -1490,20 +1461,28 @@ impl Hud { .set(self.ids.num_figures, ui_widgets); // Help Window - Text::new(&format!( - "Press {:?} to show keybindings", - global_state.settings.controls.help - )) + Text::new( + &localized_strings + .get("hud.press_key_to_toggle_keybindings_fmt") + .replace( + "{key}", + &format!("{:?}", global_state.settings.controls.help), + ), + ) .color(TEXT_COLOR) .down_from(self.ids.num_figures, 5.0) .font_id(self.fonts.cyri) .font_size(14) .set(self.ids.help_info, ui_widgets); // Info about Debug Shortcut - Text::new(&format!( - "Press {:?} to toggle debug info", - global_state.settings.controls.toggle_debug - )) + Text::new( + &localized_strings + .get("hud.press_key_to_toggle_debug_info_fmt") + .replace( + "{key}", + &format!("{:?}", global_state.settings.controls.toggle_debug), + ), + ) .color(TEXT_COLOR) .down_from(self.ids.help_info, 5.0) .font_id(self.fonts.cyri) @@ -1511,20 +1490,28 @@ impl Hud { .set(self.ids.debug_info, ui_widgets); } else { // Help Window - Text::new(&format!( - "Press {:?} to show keybindings", - global_state.settings.controls.help - )) + Text::new( + &localized_strings + .get("hud.press_key_to_show_keybindings_fmt") + .replace( + "{key}", + &format!("{:?}", global_state.settings.controls.help), + ), + ) .color(TEXT_COLOR) .top_left_with_margins_on(ui_widgets.window, 5.0, 5.0) .font_id(self.fonts.cyri) .font_size(16) .set(self.ids.help_info, ui_widgets); // Info about Debug Shortcut - Text::new(&format!( - "Press {:?} to toggle debug info", - global_state.settings.controls.toggle_debug - )) + Text::new( + &localized_strings + .get("hud.press_key_to_show_debug_info_fmt") + .replace( + "{key}", + &format!("{:?}", global_state.settings.controls.toggle_debug), + ), + ) .color(TEXT_COLOR) .down_from(self.ids.help_info, 5.0) .font_id(self.fonts.cyri) @@ -1563,7 +1550,7 @@ impl Hud { .w_h(120.0, 50.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Show Tips") + .label(&localized_strings.get("hud.show_tips")) .label_font_size(20) .label_color(TEXT_COLOR) .mid_bottom_with_margin_on(self.ids.help, 20.0) @@ -1702,8 +1689,14 @@ impl Hud { // Settings if let Windows::Settings = self.show.open_windows { - for event in SettingsWindow::new(&global_state, &self.show, &self.imgs, &self.fonts) - .set(self.ids.settings_window, ui_widgets) + for event in SettingsWindow::new( + &global_state, + &self.show, + &self.imgs, + &self.fonts, + &localized_strings, + ) + .set(self.ids.settings_window, ui_widgets) { match event { settings_window::Event::Sct(sct) => { @@ -1782,6 +1775,9 @@ impl Hud { settings_window::Event::ChangeFluidMode(new_fluid_mode) => { events.push(Event::ChangeFluidMode(new_fluid_mode)); } + settings_window::Event::ChangeLanguage(language) => { + events.push(Event::ChangeLanguage(language)); + } } } } @@ -1789,10 +1785,11 @@ impl Hud { // Social Window if self.show.social { for event in Social::new( - /*&global_state,*/ &self.show, + &self.show, client, &self.imgs, &self.fonts, + &localized_strings, ) .set(self.ids.social_window, ui_widgets) { @@ -1810,8 +1807,14 @@ impl Hud { let ecs = client.state().ecs(); let stats = ecs.read_storage::(); let player_stats = stats.get(client.entity()).unwrap(); - match CharacterWindow::new(&self.show, &player_stats, &self.imgs, &self.fonts) - .set(self.ids.character_window, ui_widgets) + match CharacterWindow::new( + &self.show, + &player_stats, + &self.imgs, + &self.fonts, + &localized_strings, + ) + .set(self.ids.character_window, ui_widgets) { Some(character_window::Event::Close) => { self.show.character_window(false); @@ -1823,8 +1826,14 @@ impl Hud { // Spellbook if self.show.spell { - match Spell::new(&self.show, client, &self.imgs, &self.fonts) - .set(self.ids.spell, ui_widgets) + match Spell::new( + &self.show, + client, + &self.imgs, + &self.fonts, + &localized_strings, + ) + .set(self.ids.spell, ui_widgets) { Some(spell::Event::Close) => { self.show.spell(false); @@ -1836,8 +1845,14 @@ impl Hud { // Quest Log if self.show.quest { - match Quest::new(&self.show, client, &self.imgs, &self.fonts) - .set(self.ids.quest, ui_widgets) + match Quest::new( + &self.show, + client, + &self.imgs, + &self.fonts, + &localized_strings, + ) + .set(self.ids.quest, ui_widgets) { Some(quest::Event::Close) => { self.show.quest(false); @@ -1868,7 +1883,9 @@ impl Hud { } if self.show.esc_menu { - match EscMenu::new(&self.imgs, &self.fonts).set(self.ids.esc_menu, ui_widgets) { + match EscMenu::new(&self.imgs, &self.fonts, &localized_strings) + .set(self.ids.esc_menu, ui_widgets) + { Some(esc_menu::Event::OpenSettings(tab)) => { self.show.open_setting_tab(tab); } diff --git a/voxygen/src/hud/quest.rs b/voxygen/src/hud/quest.rs index cca95d1449..c18bc59adb 100644 --- a/voxygen/src/hud/quest.rs +++ b/voxygen/src/hud/quest.rs @@ -1,4 +1,6 @@ use super::{img_ids::Imgs, Fonts, Show, TEXT_COLOR}; +use crate::i18n::VoxygenLocalization; +use client::{self, Client}; use conrod_core::{ color, widget::{self, Button, Image, Rectangle, Text}, @@ -6,8 +8,6 @@ use conrod_core::{ Colorable, Positionable, Sizeable, Widget, WidgetCommon, }; -use client::{self, Client}; - widget_ids! { pub struct Ids { quest_frame, @@ -25,17 +25,25 @@ pub struct Quest<'a> { imgs: &'a Imgs, fonts: &'a Fonts, + localized_strings: &'a std::sync::Arc, #[conrod(common_builder)] common: widget::CommonBuilder, } impl<'a> Quest<'a> { - pub fn new(show: &'a Show, _client: &'a Client, imgs: &'a Imgs, fonts: &'a Fonts) -> Self { + pub fn new( + show: &'a Show, + _client: &'a Client, + imgs: &'a Imgs, + fonts: &'a Fonts, + localized_strings: &'a std::sync::Arc, + ) -> Self { Self { _show: show, imgs, _client, fonts: fonts, + localized_strings, common: widget::CommonBuilder::default(), } } @@ -93,7 +101,7 @@ impl<'a> Widget for Quest<'a> { // Title // TODO: Use an actual character name. - Text::new("Quest") + Text::new(&self.localized_strings.get("hud.quests")) .mid_top_with_margin_on(state.quest_frame, 6.0) .font_id(self.fonts.cyri) .font_size(14) diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs index f7d19d06bb..861b92c85e 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -3,6 +3,7 @@ use super::{ TEXT_COLOR, }; use crate::{ + i18n::{list_localizations, LanguageMetadata, VoxygenLocalization}, render::{AaMode, CloudMode, FluidMode}, ui::{ImageSlider, ScaleMode, ToggleButton}, GlobalState, @@ -41,6 +42,8 @@ widget_ids! { absolute_scale_text, gameplay, controls, + languages, + languages_list, rectangle, general_txt, debug_button, @@ -48,6 +51,7 @@ widget_ids! { tips_button, tips_button_label, interface, + language_text, mouse_pan_slider, mouse_pan_label, mouse_pan_value, @@ -146,11 +150,10 @@ pub enum SettingsTab { #[derive(WidgetCommon)] pub struct SettingsWindow<'a> { global_state: &'a GlobalState, - show: &'a Show, - imgs: &'a Imgs, fonts: &'a Fonts, + localized_strings: &'a std::sync::Arc, #[conrod(common_builder)] common: widget::CommonBuilder, } @@ -161,12 +164,14 @@ impl<'a> SettingsWindow<'a> { show: &'a Show, imgs: &'a Imgs, fonts: &'a Fonts, + localized_strings: &'a std::sync::Arc, ) -> Self { Self { global_state, show, imgs, fonts, + localized_strings, common: widget::CommonBuilder::default(), } } @@ -205,6 +210,7 @@ pub enum Event { Sct(bool), SctPlayerBatch(bool), SctDamageBatch(bool), + ChangeLanguage(LanguageMetadata), } pub enum ScaleChange { @@ -277,7 +283,7 @@ impl<'a> Widget for SettingsWindow<'a> { } // Title - Text::new("Settings") + Text::new(&self.localized_strings.get("common.settings")) .mid_top_with_margin_on(state.ids.settings_bg, 5.0) .font_size(14) .color(TEXT_COLOR) @@ -301,7 +307,7 @@ impl<'a> Widget for SettingsWindow<'a> { self.imgs.settings_button_press }) .top_left_with_margins_on(state.ids.settings_l, 8.0 * 4.0, 2.0 * 4.0) - .label("Interface") + .label(&self.localized_strings.get("common.interface")) .label_font_size(14) .label_color(TEXT_COLOR) .set(state.ids.interface, ui) @@ -317,7 +323,7 @@ impl<'a> Widget for SettingsWindow<'a> { let ui_scale = self.global_state.settings.gameplay.ui_scale; let chat_transp = self.global_state.settings.gameplay.chat_transp; - Text::new("General") + Text::new(&self.localized_strings.get("hud.settings.general")) .top_left_with_margins_on(state.ids.settings_content, 5.0, 5.0) .font_size(18) .font_id(self.fonts.cyri) @@ -340,7 +346,7 @@ impl<'a> Widget for SettingsWindow<'a> { events.push(Event::ToggleHelp); } - Text::new("Help Window") + Text::new(&self.localized_strings.get("hud.settings.help_window")) .right_from(state.ids.button_help, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -364,7 +370,7 @@ impl<'a> Widget for SettingsWindow<'a> { events.push(Event::ToggleDebug); } - Text::new("Debug Info") + Text::new(&self.localized_strings.get("hud.settings.debug_info")) .right_from(state.ids.debug_button, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -391,7 +397,7 @@ impl<'a> Widget for SettingsWindow<'a> { Intro::Never => events.push(Event::Intro(Intro::Show)), } }; - Text::new("Tips on Startup") + Text::new(&self.localized_strings.get("hud.settings.tips_on_startup")) .right_from(state.ids.tips_button, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -400,7 +406,7 @@ impl<'a> Widget for SettingsWindow<'a> { .set(state.ids.tips_button_label, ui); // Ui Scale - Text::new("UI-Scale") + Text::new(&self.localized_strings.get("hud.settings.ui_scale")) .down_from(state.ids.tips_button, 20.0) .font_size(18) .font_id(self.fonts.cyri) @@ -646,13 +652,13 @@ impl<'a> Widget for SettingsWindow<'a> { .graphics_for(state.ids.ch_3_bg) .set(state.ids.crosshair_inner_3, ui); // Crosshair Transparency Text and Slider - Text::new("Crosshair") + Text::new(&self.localized_strings.get("hud.settings.crosshair")) .down_from(state.ids.absolute_scale_button, 20.0) .font_size(18) .font_id(self.fonts.cyri) .color(TEXT_COLOR) .set(state.ids.ch_title, ui); - Text::new("Transparency") + Text::new(&self.localized_strings.get("hud.settings.transparency")) .right_from(state.ids.ch_3_bg, 20.0) .font_size(14) .font_id(self.fonts.cyri) @@ -685,7 +691,7 @@ impl<'a> Widget for SettingsWindow<'a> { .set(state.ids.ch_transp_value, ui); // Hotbar text - Text::new("Hotbar") + Text::new(&self.localized_strings.get("hud.settings.hotbar")) .down_from(state.ids.ch_1_bg, 20.0) .font_size(18) .font_id(self.fonts.cyri) @@ -714,13 +720,17 @@ impl<'a> Widget for SettingsWindow<'a> { XpBar::OnGain => events.push(Event::ToggleXpBar(XpBar::Always)), } } - Text::new("Toggle Experience Bar") - .right_from(state.ids.show_xpbar_button, 10.0) - .font_size(14) - .font_id(self.fonts.cyri) - .graphics_for(state.ids.show_xpbar_button) - .color(TEXT_COLOR) - .set(state.ids.show_xpbar_text, ui); + Text::new( + &self + .localized_strings + .get("hud.settings.toggle_bar_experience"), + ) + .right_from(state.ids.show_xpbar_button, 10.0) + .font_size(14) + .font_id(self.fonts.cyri) + .graphics_for(state.ids.show_xpbar_button) + .color(TEXT_COLOR) + .set(state.ids.show_xpbar_text, ui); // Show Shortcut Numbers if Button::image(match self.global_state.settings.gameplay.shortcut_numbers { ShortcutNumbers::On => self.imgs.checkbox_checked, @@ -748,7 +758,7 @@ impl<'a> Widget for SettingsWindow<'a> { } } } - Text::new("Toggle Shortcuts") + Text::new(&self.localized_strings.get("hud.settings.toggle_shortcuts")) .right_from(state.ids.show_shortcuts_button, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -773,12 +783,16 @@ impl<'a> Widget for SettingsWindow<'a> { Number Display Duration: 1s ----I----5s */ // SCT/ Scrolling Combat Text - Text::new("Scrolling Combat Text") - .top_left_with_margins_on(state.ids.settings_content_r, 5.0, 5.0) - .font_size(18) - .font_id(self.fonts.cyri) - .color(TEXT_COLOR) - .set(state.ids.sct_title, ui); + Text::new( + &self + .localized_strings + .get("hud.settings.scrolling_combat_text"), + ) + .top_left_with_margins_on(state.ids.settings_content_r, 5.0, 5.0) + .font_size(18) + .font_id(self.fonts.cyri) + .color(TEXT_COLOR) + .set(state.ids.sct_title, ui); // Generally toggle the SCT let show_sct = ToggleButton::new( self.global_state.settings.gameplay.sct, @@ -794,13 +808,17 @@ impl<'a> Widget for SettingsWindow<'a> { if self.global_state.settings.gameplay.sct != show_sct { events.push(Event::Sct(!self.global_state.settings.gameplay.sct)) } - Text::new("Scrolling Combat Text") - .right_from(state.ids.sct_show_radio, 10.0) - .font_size(14) - .font_id(self.fonts.cyri) - .graphics_for(state.ids.sct_show_radio) - .color(TEXT_COLOR) - .set(state.ids.sct_show_text, ui); + Text::new( + &self + .localized_strings + .get("hud.settings.scrolling_combat_text"), + ) + .right_from(state.ids.sct_show_radio, 10.0) + .font_size(14) + .font_id(self.fonts.cyri) + .graphics_for(state.ids.sct_show_radio) + .color(TEXT_COLOR) + .set(state.ids.sct_show_text, ui); if self.global_state.settings.gameplay.sct { // Toggle single damage numbers let show_sct_damage_batch = !ToggleButton::new( @@ -814,13 +832,17 @@ impl<'a> Widget for SettingsWindow<'a> { .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked) .set(state.ids.sct_single_dmg_radio, ui); - Text::new("Single Damage Numbers") - .right_from(state.ids.sct_single_dmg_radio, 10.0) - .font_size(14) - .font_id(self.fonts.cyri) - .graphics_for(state.ids.sct_single_dmg_radio) - .color(TEXT_COLOR) - .set(state.ids.sct_single_dmg_text, ui); + Text::new( + &self + .localized_strings + .get("hud.settings.single_damage_number"), + ) + .right_from(state.ids.sct_single_dmg_radio, 10.0) + .font_size(14) + .font_id(self.fonts.cyri) + .graphics_for(state.ids.sct_single_dmg_radio) + .color(TEXT_COLOR) + .set(state.ids.sct_single_dmg_text, ui); // Toggle Batched Damage let show_sct_damage_batch = ToggleButton::new( show_sct_damage_batch, @@ -838,7 +860,7 @@ impl<'a> Widget for SettingsWindow<'a> { !self.global_state.settings.gameplay.sct_damage_batch, )) } - Text::new("Cumulated Damage") + Text::new(&self.localized_strings.get("hud.settings.cumulated_damage")) .right_from(state.ids.sct_show_batch_radio, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -857,7 +879,7 @@ impl<'a> Widget for SettingsWindow<'a> { .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked) .set(state.ids.sct_inc_dmg_radio, ui); - Text::new("Incoming Damage") + Text::new(&self.localized_strings.get("hud.settings.incoming_damage")) .right_from(state.ids.sct_inc_dmg_radio, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -881,18 +903,22 @@ impl<'a> Widget for SettingsWindow<'a> { !self.global_state.settings.gameplay.sct_player_batch, )) } - Text::new("Cumulated Incoming Damage") - .right_from(state.ids.sct_batch_inc_radio, 10.0) - .font_size(14) - .font_id(self.fonts.cyri) - .graphics_for(state.ids.sct_batch_inc_radio) - .color(TEXT_COLOR) - .set(state.ids.sct_batch_inc_text, ui); + Text::new( + &self + .localized_strings + .get("hud.settings.cumulated_incoming_damage"), + ) + .right_from(state.ids.sct_batch_inc_radio, 10.0) + .font_size(14) + .font_id(self.fonts.cyri) + .graphics_for(state.ids.sct_batch_inc_radio) + .color(TEXT_COLOR) + .set(state.ids.sct_batch_inc_text, ui); } // Energybars Numbers // Hotbar text - Text::new("Energybar Numbers") + Text::new(&self.localized_strings.get("hud.settings.energybar_numbers")) .down_from( if self.global_state.settings.gameplay.sct { state.ids.sct_batch_inc_radio @@ -929,7 +955,7 @@ impl<'a> Widget for SettingsWindow<'a> { { events.push(Event::ToggleBarNumbers(BarNumbers::Off)) } - Text::new("None") + Text::new(&self.localized_strings.get("hud.settings.none")) .right_from(state.ids.show_bar_numbers_none_button, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -960,7 +986,7 @@ impl<'a> Widget for SettingsWindow<'a> { { events.push(Event::ToggleBarNumbers(BarNumbers::Values)) } - Text::new("Values") + Text::new(&self.localized_strings.get("hud.settings.values")) .right_from(state.ids.show_bar_numbers_values_button, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -991,7 +1017,7 @@ impl<'a> Widget for SettingsWindow<'a> { { events.push(Event::ToggleBarNumbers(BarNumbers::Percent)) } - Text::new("Percentages") + Text::new(&self.localized_strings.get("hud.settings.percentages")) .right_from(state.ids.show_bar_numbers_percentage_button, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -1000,18 +1026,22 @@ impl<'a> Widget for SettingsWindow<'a> { .set(state.ids.show_bar_numbers_percentage_text, ui); // Chat Transp - Text::new("Chat") + Text::new(&self.localized_strings.get("hud.settings.chat")) .down_from(state.ids.show_bar_numbers_percentage_button, 20.0) .font_size(18) .font_id(self.fonts.cyri) .color(TEXT_COLOR) .set(state.ids.chat_transp_title, ui); - Text::new("Background Transparency") - .right_from(state.ids.chat_transp_slider, 20.0) - .font_size(14) - .font_id(self.fonts.cyri) - .color(TEXT_COLOR) - .set(state.ids.chat_transp_text, ui); + Text::new( + &self + .localized_strings + .get("hud.settings.background_transparency"), + ) + .right_from(state.ids.chat_transp_slider, 20.0) + .font_size(14) + .font_id(self.fonts.cyri) + .color(TEXT_COLOR) + .set(state.ids.chat_transp_text, ui); if let Some(new_val) = ImageSlider::continuous( chat_transp, @@ -1029,6 +1059,34 @@ impl<'a> Widget for SettingsWindow<'a> { { events.push(Event::ChatTransp(new_val)); } + + Text::new(&self.localized_strings.get("common.languages")) + .down_from(state.ids.chat_transp_slider, 20.0) + .font_size(18) + .font_id(self.fonts.cyri) + .color(TEXT_COLOR) + .set(state.ids.language_text, ui); + + let selected_language = &self.global_state.settings.language.selected_language; + let language_list = list_localizations(); + let language_list_str: Vec = language_list + .iter() + .map(|e| e.language_name.clone()) + .collect(); + let selected = language_list + .iter() + .position(|x| x.language_identifier.contains(selected_language)); + + if let Some(clicked) = DropDownList::new(&language_list_str, selected) + .down_from(state.ids.language_text, 8.0) + .w_h(200.0, 22.0) + .color(MENU_BG) + .label_color(TEXT_COLOR) + .label_font_id(self.fonts.cyri) + .set(state.ids.languages_list, ui) + { + events.push(Event::ChangeLanguage(language_list[clicked].to_owned())); + } } // 2) Gameplay Tab -------------------------------- @@ -1049,7 +1107,7 @@ impl<'a> Widget for SettingsWindow<'a> { self.imgs.settings_button_press }) .right_from(state.ids.interface, 0.0) - .label("Gameplay") + .label(&self.localized_strings.get("common.gameplay")) .label_font_size(14) .label_color(TEXT_COLOR) .set(state.ids.gameplay, ui) @@ -1064,7 +1122,7 @@ impl<'a> Widget for SettingsWindow<'a> { let display_zoom = self.global_state.settings.gameplay.zoom_sensitivity; // Mouse Pan Sensitivity - Text::new("Pan Sensitivity") + Text::new(&self.localized_strings.get("hud.settings.pan_sensitivity")) .top_left_with_margins_on(state.ids.settings_content, 10.0, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -1096,7 +1154,7 @@ impl<'a> Widget for SettingsWindow<'a> { .set(state.ids.mouse_pan_value, ui); // Mouse Zoom Sensitivity - Text::new("Zoom Sensitivity") + Text::new(&self.localized_strings.get("hud.settings.zoom_sensitivity")) .down_from(state.ids.mouse_pan_slider, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -1145,13 +1203,17 @@ impl<'a> Widget for SettingsWindow<'a> { )); } - Text::new("Invert Scroll Zoom") - .right_from(state.ids.mouse_zoom_invert_button, 10.0) - .font_size(14) - .font_id(self.fonts.cyri) - .graphics_for(state.ids.button_help) - .color(TEXT_COLOR) - .set(state.ids.mouse_zoom_invert_label, ui); + Text::new( + &self + .localized_strings + .get("hud.settings.invert_scroll_zoom"), + ) + .right_from(state.ids.mouse_zoom_invert_button, 10.0) + .font_size(14) + .font_id(self.fonts.cyri) + .graphics_for(state.ids.button_help) + .color(TEXT_COLOR) + .set(state.ids.mouse_zoom_invert_label, ui); // Mouse Y Inversion let mouse_y_inverted = ToggleButton::new( @@ -1171,13 +1233,17 @@ impl<'a> Widget for SettingsWindow<'a> { )); } - Text::new("Invert Mouse Y Axis") - .right_from(state.ids.mouse_y_invert_button, 10.0) - .font_size(14) - .font_id(self.fonts.cyri) - .graphics_for(state.ids.button_help) - .color(TEXT_COLOR) - .set(state.ids.mouse_y_invert_label, ui); + Text::new( + &self + .localized_strings + .get("hud.settings.invert_mouse_y_axis"), + ) + .right_from(state.ids.mouse_y_invert_button, 10.0) + .font_size(14) + .font_id(self.fonts.cyri) + .graphics_for(state.ids.button_help) + .color(TEXT_COLOR) + .set(state.ids.mouse_y_invert_label, ui); } // 3) Controls Tab -------------------------------- @@ -1198,7 +1264,7 @@ impl<'a> Widget for SettingsWindow<'a> { self.imgs.settings_button_press }) .right_from(state.ids.gameplay, 0.0) - .label("Controls") + .label(&self.localized_strings.get("common.controls")) .label_font_size(14) .label_color(TEXT_COLOR) .set(state.ids.controls, ui) @@ -1210,94 +1276,12 @@ impl<'a> Widget for SettingsWindow<'a> { // Contents if let SettingsTab::Controls = self.show.settings_tab { let controls = &self.global_state.settings.controls; - Text::new( - "Free Cursor\n\ - Toggle Help Window\n\ - Toggle Interface\n\ - Toggle FPS and Debug Info\n\ - Take Screenshot\n\ - Toggle Nametags\n\ - Toggle Fullscreen\n\ - \n\ - \n\ - Move Forward\n\ - Move Left\n\ - Move Right\n\ - Move Backwards\n\ - \n\ - Jump\n\ - \n\ - Glider - \n\ - Dodge\n\ - \n\ - Roll\n\ - \n\ - Climb\n\ - \n\ - Climb down\n\ - \n\ - Auto Walk\n\ - \n\ - Sheathe/Draw Weapons\n\ - \n\ - Put on/Remove Helmet\n\ - \n\ - Sit\n\ - \n\ - Mount\n\ - \n\ - Interact\n\ - \n\ - \n\ - Basic Attack\n\ - Secondary Attack/Block/Aim\n\ - \n\ - \n\ - Skillbar Slot 1\n\ - Skillbar Slot 2\n\ - Skillbar Slot 3\n\ - Skillbar Slot 4\n\ - Skillbar Slot 5\n\ - Skillbar Slot 6\n\ - Skillbar Slot 7\n\ - Skillbar Slot 8\n\ - Skillbar Slot 9\n\ - Skillbar Slot 10\n\ - \n\ - \n\ - Pause Menu\n\ - Settings\n\ - Social\n\ - Map\n\ - Spellbook\n\ - Character\n\ - Questlog\n\ - Bag\n\ - \n\ - \n\ - \n\ - Send Chat Message\n\ - Scroll Chat\n\ - \n\ - \n\ - Chat commands: \n\ - \n\ - /alias [Name] - Change your Chat Name \n\ - /tp [Name] - Teleports you to another player \n\ - /jump - Offset your position \n\ - /goto - Teleport to a position \n\ - /kill - Kill yourself \n\ - /pig - Spawn pig NPC \n\ - /wolf - Spawn wolf NPC \n\ - /help - Display chat commands - ", - ) - .color(TEXT_COLOR) - .top_left_with_margins_on(state.ids.settings_content, 5.0, 5.0) - .font_id(self.fonts.cyri) - .font_size(18) - .set(state.ids.controls_text, ui); + Text::new(&self.localized_strings.get("hud.settings.control_names")) + .color(TEXT_COLOR) + .top_left_with_margins_on(state.ids.settings_content, 5.0, 5.0) + .font_id(self.fonts.cyri) + .font_size(18) + .set(state.ids.controls_text, ui); // TODO: Replace with buttons that show actual keybinds and allow the user to change them. Text::new(&format!( "{}\n\ @@ -1446,7 +1430,7 @@ impl<'a> Widget for SettingsWindow<'a> { self.imgs.settings_button_press }) .right_from(state.ids.controls, 0.0) - .label("Video") + .label(&self.localized_strings.get("common.video")) .parent(state.ids.settings_r) .label_font_size(14) .label_color(TEXT_COLOR) @@ -1459,7 +1443,7 @@ impl<'a> Widget for SettingsWindow<'a> { // Contents if let SettingsTab::Video = self.show.settings_tab { // View Distance - Text::new("View Distance") + Text::new(&self.localized_strings.get("hud.settings.view_distance")) .top_left_with_margins_on(state.ids.settings_content, 10.0, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -1494,7 +1478,7 @@ impl<'a> Widget for SettingsWindow<'a> { .set(state.ids.vd_value, ui); // Max FPS - Text::new("Maximum FPS") + Text::new(&self.localized_strings.get("hud.settings.maximum_fps")) .down_from(state.ids.vd_slider, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -1561,7 +1545,7 @@ impl<'a> Widget for SettingsWindow<'a> { .set(state.ids.fov_value, ui); // AaMode - Text::new("AntiAliasing Mode") + Text::new(&self.localized_strings.get("hud.settings.antialiasing_mode")) .down_from(state.ids.fov_slider, 8.0) .font_size(14) .font_id(self.fonts.cyri) @@ -1602,15 +1586,24 @@ impl<'a> Widget for SettingsWindow<'a> { } // CloudMode - Text::new("Cloud Rendering Mode") - .down_from(state.ids.aa_mode_list, 8.0) - .font_size(14) - .font_id(self.fonts.cyri) - .color(TEXT_COLOR) - .set(state.ids.cloud_mode_text, ui); + Text::new( + &self + .localized_strings + .get("hud.settings.cloud_rendering_mode"), + ) + .down_from(state.ids.aa_mode_list, 8.0) + .font_size(14) + .font_id(self.fonts.cyri) + .color(TEXT_COLOR) + .set(state.ids.cloud_mode_text, ui); let mode_list = [CloudMode::None, CloudMode::Regular]; - let mode_label_list = ["None", "Regular"]; + let mode_label_list = [ + &self.localized_strings.get("common.none"), + &self + .localized_strings + .get("hud.settings.cloud_rendering_mode.regular"), + ]; // Get which cloud rendering mode is currently active let selected = mode_list @@ -1629,15 +1622,26 @@ impl<'a> Widget for SettingsWindow<'a> { } // FluidMode - Text::new("Fluid Rendering Mode") - .down_from(state.ids.cloud_mode_list, 8.0) - .font_size(14) - .font_id(self.fonts.cyri) - .color(TEXT_COLOR) - .set(state.ids.fluid_mode_text, ui); + Text::new( + &self + .localized_strings + .get("hud.settings.fluid_rendering_mode"), + ) + .down_from(state.ids.cloud_mode_list, 8.0) + .font_size(14) + .font_id(self.fonts.cyri) + .color(TEXT_COLOR) + .set(state.ids.fluid_mode_text, ui); let mode_list = [FluidMode::Cheap, FluidMode::Shiny]; - let mode_label_list = ["Cheap", "Shiny"]; + let mode_label_list = [ + &self + .localized_strings + .get("hud.settings.fluid_rendering_mode.cheap"), + &self + .localized_strings + .get("hud.settings.fluid_rendering_mode.shiny"), + ]; // Get which fluid rendering mode is currently active let selected = mode_list @@ -1675,7 +1679,7 @@ impl<'a> Widget for SettingsWindow<'a> { }) .right_from(state.ids.video, 0.0) .parent(state.ids.settings_r) - .label("Sound") + .label(&self.localized_strings.get("common.sound")) .label_font_size(14) .label_color(TEXT_COLOR) .set(state.ids.sound, ui) @@ -1687,7 +1691,7 @@ impl<'a> Widget for SettingsWindow<'a> { // Contents if let SettingsTab::Sound = self.show.settings_tab { // Music Volume ----------------------------------------------------- - Text::new("Music Volume") + Text::new(&self.localized_strings.get("hud.settings.music_volume")) .top_left_with_margins_on(state.ids.settings_content, 10.0, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -1712,12 +1716,16 @@ impl<'a> Widget for SettingsWindow<'a> { } // SFX Volume ------------------------------------------------------- - Text::new("Sound Effects Volume") - .down_from(state.ids.audio_volume_slider, 10.0) - .font_size(14) - .font_id(self.fonts.cyri) - .color(TEXT_COLOR) - .set(state.ids.sfx_volume_text, ui); + Text::new( + &self + .localized_strings + .get("hud.settings.sound_effect_volume"), + ) + .down_from(state.ids.audio_volume_slider, 10.0) + .font_size(14) + .font_id(self.fonts.cyri) + .color(TEXT_COLOR) + .set(state.ids.sfx_volume_text, ui); if let Some(new_val) = ImageSlider::continuous( self.global_state.settings.audio.sfx_volume, @@ -1739,7 +1747,7 @@ impl<'a> Widget for SettingsWindow<'a> { // Audio Device Selector -------------------------------------------- let device = &self.global_state.audio.device; let device_list = &self.global_state.audio.device_list; - Text::new("Audio Device") + Text::new(&self.localized_strings.get("hud.settings.audio_device")) .down_from(state.ids.sfx_volume_slider, 10.0) .font_size(14) .font_id(self.fonts.cyri) diff --git a/voxygen/src/hud/skillbar.rs b/voxygen/src/hud/skillbar.rs index 4cf7bb433c..1f86ff48c8 100644 --- a/voxygen/src/hud/skillbar.rs +++ b/voxygen/src/hud/skillbar.rs @@ -2,7 +2,9 @@ use super::{ img_ids::Imgs, BarNumbers, Fonts, ShortcutNumbers, XpBar, CRITICAL_HP_COLOR, /*FOCUS_COLOR, RAGE_COLOR,*/ HP_COLOR, LOW_HP_COLOR, MANA_COLOR, TEXT_COLOR, XP_COLOR, }; +use crate::i18n::{i18n_asset_key, VoxygenLocalization}; use crate::GlobalState; +use common::assets::load_expect; use common::comp::{ item::Debug, item::Tool, ActionState, CharacterState, ControllerInputs, Energy, ItemKind, Stats, }; @@ -189,6 +191,10 @@ impl<'a> Widget for Skillbar<'a> { let hp_ani = (self.pulse * 4.0/*speed factor*/).cos() * 0.5 + 0.8; //Animation timer let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani); + let localized_strings = load_expect::(&i18n_asset_key( + &self.global_state.settings.language.selected_language, + )); + // Stamina Wheel /* let stamina_percentage = @@ -277,34 +283,30 @@ impl<'a> Widget for Skillbar<'a> { .set(state.ids.level_down, ui); // Death message if self.stats.is_dead { - Text::new("You Died") + Text::new(&localized_strings.get("hud.you_died")) .middle_of(ui.window) .font_size(50) .font_id(self.fonts.cyri) .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) .set(state.ids.death_message_1_bg, ui); - Text::new(&format!( - "Press {:?} to respawn at your Waypoint.\n\ - \n\ - Press Enter, type in /waypoint and confirm to set it here.", - self.global_state.settings.controls.respawn + Text::new(&localized_strings.get("hud.press_key_to_respawn").replace( + "{key}", + &format!("{:?}", self.global_state.settings.controls.respawn), )) .mid_bottom_with_margin_on(state.ids.death_message_1_bg, -120.0) .font_size(30) .font_id(self.fonts.cyri) .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) .set(state.ids.death_message_2_bg, ui); - Text::new("You Died") + Text::new(&localized_strings.get("hud.you_died")) .bottom_left_with_margins_on(state.ids.death_message_1_bg, 2.0, 2.0) .font_size(50) .font_id(self.fonts.cyri) .color(CRITICAL_HP_COLOR) .set(state.ids.death_message_1, ui); - Text::new(&format!( - "Press {:?} to respawn at your Waypoint.\n\ - \n\ - Press Enter, type in /waypoint and confirm to set it here.", - self.global_state.settings.controls.respawn + Text::new(&localized_strings.get("hud.press_key_to_respawn").replace( + "{key}", + &format!("{:?}", self.global_state.settings.controls.respawn), )) .bottom_left_with_margins_on(state.ids.death_message_2_bg, 2.0, 2.0) .font_size(30) diff --git a/voxygen/src/hud/social.rs b/voxygen/src/hud/social.rs index 9cdea0e1c3..9922a74e15 100644 --- a/voxygen/src/hud/social.rs +++ b/voxygen/src/hud/social.rs @@ -1,5 +1,7 @@ use super::{img_ids::Imgs, Fonts, Show, TEXT_COLOR, TEXT_COLOR_3}; +use crate::i18n::VoxygenLocalization; +use client::{self, Client}; use conrod_core::{ color, widget::{self, Button, Image, Rectangle, Scrollbar, Text}, @@ -7,8 +9,6 @@ use conrod_core::{ Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon, }; -use client::{self, Client}; - widget_ids! { pub struct Ids { social_frame, @@ -41,26 +41,31 @@ pub struct Social<'a> { client: &'a Client, imgs: &'a Imgs, fonts: &'a Fonts, + localized_strings: &'a std::sync::Arc, + #[conrod(common_builder)] common: widget::CommonBuilder, } impl<'a> Social<'a> { - pub fn new(show: &'a Show, client: &'a Client, imgs: &'a Imgs, fonts: &'a Fonts) -> Self { + pub fn new( + show: &'a Show, + client: &'a Client, + imgs: &'a Imgs, + fonts: &'a Fonts, + localized_strings: &'a std::sync::Arc, + ) -> Self { Self { - show: show, + show, + client, imgs, - client: client, - fonts: fonts, + fonts, + localized_strings, common: widget::CommonBuilder::default(), } } } -/*pub struct State { - ids: Ids, -}*/ - pub enum Event { Close, ChangeSocialTab(SocialTab), @@ -113,7 +118,7 @@ impl<'a> Widget for Social<'a> { } // Title - Text::new("Social") + Text::new(&self.localized_strings.get("hud.social")) .mid_top_with_margin_on(ids.social_frame, 6.0) .font_id(self.fonts.cyri) .font_size(14) @@ -159,7 +164,7 @@ impl<'a> Widget for Social<'a> { self.imgs.social_button_press }) .top_left_with_margins_on(ids.align, 4.0, 0.0) - .label("Online") + .label(&self.localized_strings.get("hud.social.online")) .label_font_size(14) .parent(ids.frame) .label_color(TEXT_COLOR) @@ -183,12 +188,17 @@ impl<'a> Widget for Social<'a> { .resize(count, &mut ui.widget_id_generator()) }) } - Text::new(&format!("{} player(s) online\n", count)) - .top_left_with_margins_on(ids.content_align, -2.0, 7.0) - .font_size(14) - .font_id(self.fonts.cyri) - .color(TEXT_COLOR) - .set(ids.online_title, ui); + Text::new( + &self + .localized_strings + .get("hud.social.play_online_fmt") + .replace("{nb_player}", &format!("{:?}", count)), + ) + .top_left_with_margins_on(ids.content_align, -2.0, 7.0) + .font_size(14) + .font_id(self.fonts.cyri) + .color(TEXT_COLOR) + .set(ids.online_title, ui); for (i, (_, player_alias)) in self.client.player_list.iter().enumerate() { Text::new(player_alias) .down(3.0) @@ -218,7 +228,7 @@ impl<'a> Widget for Social<'a> { self.imgs.social_button }) .right_from(ids.online_tab, 0.0) - .label("Friends") + .label(&self.localized_strings.get("hud.social.friends")) .label_font_size(14) .parent(ids.frame) .label_color(TEXT_COLOR_3) @@ -231,7 +241,7 @@ impl<'a> Widget for Social<'a> { // Contents if let SocialTab::Friends = self.show.social_tab { - Text::new("Not yet available") + Text::new(&self.localized_strings.get("hud.social.not_yet_available")) .middle_of(ids.content_align) .font_size(18) .font_id(self.fonts.cyri) @@ -248,7 +258,7 @@ impl<'a> Widget for Social<'a> { if Button::image(button_img) .w_h(30.0 * 4.0, 12.0 * 4.0) .right_from(ids.friends_tab, 0.0) - .label("Faction") + .label(&self.localized_strings.get("hud.social.faction")) .parent(ids.frame) .label_font_size(14) .label_color(TEXT_COLOR_3) @@ -261,7 +271,7 @@ impl<'a> Widget for Social<'a> { // Contents if let SocialTab::Faction = self.show.social_tab { - Text::new("Not yet available") + Text::new(&self.localized_strings.get("hud.social.not_yet_available")) .middle_of(ids.content_align) .font_size(18) .font_id(self.fonts.cyri) diff --git a/voxygen/src/hud/spell.rs b/voxygen/src/hud/spell.rs index e2282de982..97bbec29cf 100644 --- a/voxygen/src/hud/spell.rs +++ b/voxygen/src/hud/spell.rs @@ -8,6 +8,8 @@ use conrod_core::{ use client::{self, Client}; +use crate::i18n::VoxygenLocalization; + widget_ids! { pub struct Ids { spell_frame, @@ -25,17 +27,26 @@ pub struct Spell<'a> { imgs: &'a Imgs, fonts: &'a Fonts, + localized_strings: &'a std::sync::Arc, + #[conrod(common_builder)] common: widget::CommonBuilder, } impl<'a> Spell<'a> { - pub fn new(show: &'a Show, _client: &'a Client, imgs: &'a Imgs, fonts: &'a Fonts) -> Self { + pub fn new( + show: &'a Show, + _client: &'a Client, + imgs: &'a Imgs, + fonts: &'a Fonts, + localized_strings: &'a std::sync::Arc, + ) -> Self { Self { _show: show, - imgs, _client, - fonts: fonts, + imgs, + fonts, + localized_strings, common: widget::CommonBuilder::default(), } } @@ -93,7 +104,7 @@ impl<'a> Widget for Spell<'a> { // Title // TODO: Use an actual character name. - Text::new("Spell") + Text::new(&self.localized_strings.get("hud.spell")) .mid_top_with_margin_on(state.spell_frame, 6.0) .font_id(self.fonts.cyri) .font_size(14) diff --git a/voxygen/src/i18n.rs b/voxygen/src/i18n.rs new file mode 100644 index 0000000000..aeb4447b7e --- /dev/null +++ b/voxygen/src/i18n.rs @@ -0,0 +1,113 @@ +use common::assets; +use common::assets::{load_expect, load_glob, Asset}; +use deunicode::deunicode; +use ron::de::from_reader; +use serde_derive::*; +use std::collections::{HashMap, HashSet}; +use std::fs::File; +use std::io::BufReader; + +/// The reference language, aka the more up-to-date localization data. +/// Also the default language at first startup. +pub const REFERENCE_LANG: &str = "en"; + +/// How a language can be described +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct LanguageMetadata { + /// A human friendly language name (e.g. "English (US)") + pub language_name: String, + + /// A short text identifier for this language (e.g. "en_US") + /// + /// On the opposite of `language_name` that can change freely, + /// `language_identifier` value shall be stable in time as it + /// is used by setting components to store the language + /// selected by the user. + pub language_identifier: String, +} + +/// Store internationalization data +/// +/// TODO: store the font locations here (Font asset path for instance) +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct VoxygenLocalization { + /// A map storing the localized texts + /// + /// Localized content can be access using a String key + pub string_map: HashMap, + + /// Either to convert the input text encoded in UTF-8 + /// into a ASCII version by using the `deunicode` crate. + pub convert_utf8_to_ascii: bool, + + pub metadata: LanguageMetadata, +} + +impl VoxygenLocalization { + /// Get a localized text from the given key + /// + /// If the key is not present in the localization object + /// then the key is returned. + pub fn get(&self, key: &str) -> String { + match self.string_map.get(key) { + Some(localized_text) => localized_text.to_owned(), + None => key.to_string(), + } + } + + /// Return the missing keys compared to the reference language and return + /// them + pub fn list_missing_entries(&self) -> HashSet { + let reference_localization = + load_expect::(i18n_asset_key(REFERENCE_LANG).as_ref()); + let reference_keys: HashSet<_> = + reference_localization.string_map.keys().cloned().collect(); + let current_keys: HashSet<_> = self.string_map.keys().cloned().collect(); + + reference_keys.difference(¤t_keys).cloned().collect() + } + + /// Log missing entries (compared to the reference language) as warnings + pub fn log_missing_entries(&self) { + for missing_key in self.list_missing_entries() { + log::warn!( + "[{:?}] Missing key {:?}", + self.metadata.language_identifier, + missing_key + ); + } + } +} + +impl Asset for VoxygenLocalization { + const ENDINGS: &'static [&'static str] = &["ron"]; + + /// Load the translations located in the input buffer and convert them + /// into a `VoxygenLocalization` object. + fn parse(buf_reader: BufReader) -> Result { + let mut asked_localization: VoxygenLocalization = from_reader(buf_reader).unwrap(); + + // Update the text if UTF-8 to ASCII conversion is enabled + if asked_localization.convert_utf8_to_ascii { + for value in asked_localization.string_map.values_mut() { + *value = deunicode(value); + } + } + asked_localization.metadata.language_name = + deunicode(&asked_localization.metadata.language_name); + + Ok(asked_localization) + } +} + +/// Load all the available languages located in the Voxygen asset directory +pub fn list_localizations() -> Vec { + let voxygen_locales_assets = "voxygen.i18n.*"; + let lang_list = load_glob::(voxygen_locales_assets).unwrap(); + lang_list.iter().map(|e| (*e).metadata.clone()).collect() +} + +/// Return the asset associated with the language_id +pub fn i18n_asset_key(language_id: &str) -> String { + "voxygen.i18n.".to_string() + language_id +} diff --git a/voxygen/src/main.rs b/voxygen/src/main.rs index d156e89bbe..c06abd440a 100644 --- a/voxygen/src/main.rs +++ b/voxygen/src/main.rs @@ -9,6 +9,7 @@ pub mod audio; mod ecs; pub mod error; pub mod hud; +pub mod i18n; pub mod key_state; mod logging; pub mod menu; @@ -26,8 +27,14 @@ pub mod window; pub use crate::error::Error; use crate::{ - audio::AudioFrontend, menu::main::MainMenuState, meta::Meta, settings::Settings, window::Window, + audio::AudioFrontend, + i18n::{i18n_asset_key, VoxygenLocalization}, + menu::main::MainMenuState, + meta::Meta, + settings::Settings, + window::Window, }; +use common::assets::load_expect; use log::{debug, error}; use std::{mem, panic, str::FromStr}; @@ -131,6 +138,12 @@ fn main() { info_message: None, }; + // Try to load the localization and log missing entries + let localized_strings = load_expect::(&i18n_asset_key( + &global_state.settings.language.selected_language, + )); + localized_strings.log_missing_entries(); + // Set up panic handler to relay swish panic messages to the user let default_hook = panic::take_hook(); panic::set_hook(Box::new(move |panic_info| { diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs index 10b3b6ed5f..770e4fda9f 100644 --- a/voxygen/src/menu/char_selection/mod.rs +++ b/voxygen/src/menu/char_selection/mod.rs @@ -1,6 +1,7 @@ mod scene; mod ui; +use crate::i18n::{i18n_asset_key, VoxygenLocalization}; use crate::{ session::SessionState, window::Event as WinEvent, Direction, GlobalState, PlayState, PlayStateResult, @@ -108,15 +109,15 @@ impl PlayState for CharSelectionState { .render(global_state.window.renderer_mut(), self.scene.globals()); // Tick the client (currently only to keep the connection alive). + let localized_strings = assets::load_expect::(&i18n_asset_key( + &global_state.settings.language.selected_language, + )); if let Err(err) = self.client.borrow_mut().tick( comp::ControllerInputs::default(), clock.get_last_delta(), |_| {}, ) { - global_state.info_message = Some( - "Connection lost!\nDid the server restart?\nIs the client up to date?" - .to_owned(), - ); + global_state.info_message = Some(localized_strings.get("common.connection_lost")); error!("[session] Failed to tick the scene: {:?}", err); return PlayStateResult::Pop; diff --git a/voxygen/src/menu/char_selection/ui.rs b/voxygen/src/menu/char_selection/ui.rs index f5049f4040..d9aa9d4ba4 100644 --- a/voxygen/src/menu/char_selection/ui.rs +++ b/voxygen/src/menu/char_selection/ui.rs @@ -1,5 +1,6 @@ use crate::window::{Event as WinEvent, PressState}; use crate::{ + i18n::{i18n_asset_key, VoxygenLocalization}, meta::CharacterData, render::{Consts, Globals, Renderer}, ui::{ @@ -9,6 +10,7 @@ use crate::{ GlobalState, }; use client::Client; +use common::assets::load_expect; use common::comp::{self, humanoid}; use conrod_core::{ color, @@ -256,6 +258,7 @@ pub struct CharSelectionUi { fonts: Fonts, character_creation: bool, info_content: InfoContent, + selected_language: String, //deletion_confirmation: bool, pub character_name: String, pub character_body: humanoid::Body, @@ -287,6 +290,7 @@ impl CharSelectionUi { info_content: InfoContent::None, //deletion_confirmation: false, character_creation: false, + selected_language: global_state.settings.language.selected_language.clone(), character_name: "Character Name".to_string(), character_body: humanoid::Body::random(), character_tool: Some(STARTER_SWORD), @@ -302,6 +306,9 @@ impl CharSelectionUi { env!("CARGO_PKG_VERSION"), common::util::GIT_VERSION.to_string() ); + let localized_strings = + load_expect::(&i18n_asset_key(&self.selected_language)); + // Tooltip let tooltip_human = Tooltip::new({ // Edge images [t, b, r, l] @@ -337,7 +344,7 @@ impl CharSelectionUi { match self.info_content { InfoContent::None => unreachable!(), InfoContent::Deletion(character_index) => { - Text::new("Permanently delete this Character?") + Text::new(&localized_strings.get("char_selection.delete_permanently")) .mid_top_with_margin_on(self.ids.info_frame, 40.0) .font_size(24) .font_id(self.fonts.cyri) @@ -349,7 +356,7 @@ impl CharSelectionUi { .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) .label_y(Relative::Scalar(2.0)) - .label("No") + .label(&localized_strings.get("common.no")) .label_font_id(self.fonts.cyri) .label_font_size(18) .label_color(TEXT_COLOR) @@ -364,8 +371,7 @@ impl CharSelectionUi { .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) .label_y(Relative::Scalar(2.0)) - .label("Yes") - .label_font_id(self.fonts.cyri) + .label(&localized_strings.get("common.yes")) .label_font_size(18) .label_color(TEXT_COLOR) .set(self.ids.info_ok, ui_widgets) @@ -435,7 +441,7 @@ impl CharSelectionUi { .parent(self.ids.charlist_bg) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Change Server") + .label(&localized_strings.get("char_selection.change_server")) .label_color(TEXT_COLOR) .label_font_id(self.fonts.cyri) .label_font_size(18) @@ -452,7 +458,7 @@ impl CharSelectionUi { .w_h(250.0, 60.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Enter World") + .label(&localized_strings.get("char_selection.enter_world")) .label_color(TEXT_COLOR) .label_font_size(26) .label_font_id(self.fonts.cyri) @@ -469,7 +475,7 @@ impl CharSelectionUi { .w_h(150.0, 40.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Logout") + .label(&localized_strings.get("char_selection.logout")) .label_font_id(self.fonts.cyri) .label_color(TEXT_COLOR) .label_font_size(20) @@ -486,7 +492,7 @@ impl CharSelectionUi { .w_h(270.0, 50.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Create Character") + .label(&localized_strings.get("char_selection.create_charater")) .label_font_id(self.fonts.cyri) .label_color(TEXT_COLOR) .label_font_size(20) @@ -599,7 +605,7 @@ impl CharSelectionUi { .w_h(386.0, 80.0) .hover_image(self.imgs.selection_hover) .press_image(self.imgs.selection_press) - .label("Create New Character") + .label(&localized_strings.get("char_selection.create_new_charater")) .label_color(Color::Rgba(0.38, 1.0, 0.07, 1.0)) .label_font_id(self.fonts.cyri) .image_color(Color::Rgba(0.38, 1.0, 0.07, 1.0)) @@ -619,7 +625,7 @@ impl CharSelectionUi { .w_h(150.0, 40.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Back") + .label(&localized_strings.get("common.back")) .label_font_id(self.fonts.cyri) .label_color(TEXT_COLOR) .label_font_size(20) @@ -635,7 +641,7 @@ impl CharSelectionUi { .w_h(150.0, 40.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Create") + .label(&localized_strings.get("common.create")) .label_font_id(self.fonts.cyri) .label_color(TEXT_COLOR) .label_font_size(20) @@ -704,7 +710,7 @@ impl CharSelectionUi { .set(self.ids.selection_scrollbar, ui_widgets); // Male/Female/Race Icons - Text::new("Character Creation") + Text::new(&localized_strings.get("char_selection.character_creation")) .mid_top_with_margin_on(self.ids.creation_alignment, 10.0) .font_size(24) .font_id(self.fonts.cyri) @@ -794,7 +800,12 @@ impl CharSelectionUi { .middle_of(self.ids.human) .hover_image(self.imgs.icon_border_mo) .press_image(self.imgs.icon_border_press) - .with_tooltip(tooltip_manager, "Human", "", &tooltip_human) + .with_tooltip( + tooltip_manager, + &localized_strings.get("common.races.human"), + "", + &tooltip_human, + ) /*.tooltip_image( if let humanoid::BodyType::Male = self.character_body.body_type { self.imgs.human_m @@ -822,7 +833,12 @@ impl CharSelectionUi { .middle_of(self.ids.orc) .hover_image(self.imgs.icon_border_mo) .press_image(self.imgs.icon_border_press) - .with_tooltip(tooltip_manager, "Orc", "", &tooltip_human) + .with_tooltip( + tooltip_manager, + &localized_strings.get("common.races.orc"), + "", + &tooltip_human, + ) .set(self.ids.race_2, ui_widgets) .was_clicked() { @@ -842,7 +858,12 @@ impl CharSelectionUi { .middle_of(self.ids.dwarf) .hover_image(self.imgs.icon_border_mo) .press_image(self.imgs.icon_border_press) - .with_tooltip(tooltip_manager, "Dwarf", "", &tooltip_human) + .with_tooltip( + tooltip_manager, + &localized_strings.get("common.races.dwarf"), + "", + &tooltip_human, + ) .set(self.ids.race_3, ui_widgets) .was_clicked() { @@ -862,7 +883,12 @@ impl CharSelectionUi { .middle_of(self.ids.elf) .hover_image(self.imgs.icon_border_mo) .press_image(self.imgs.icon_border_press) - .with_tooltip(tooltip_manager, "Elf", "", &tooltip_human) + .with_tooltip( + tooltip_manager, + &localized_strings.get("common.races.elf"), + "", + &tooltip_human, + ) .set(self.ids.race_4, ui_widgets) .was_clicked() { @@ -882,7 +908,12 @@ impl CharSelectionUi { .middle_of(self.ids.undead) .hover_image(self.imgs.icon_border_mo) .press_image(self.imgs.icon_border_press) - .with_tooltip(tooltip_manager, "Undead", "", &tooltip_human) + .with_tooltip( + tooltip_manager, + &localized_strings.get("common.races.undead"), + "", + &tooltip_human, + ) .set(self.ids.race_5, ui_widgets) .was_clicked() { @@ -902,7 +933,12 @@ impl CharSelectionUi { .middle_of(self.ids.danari) .hover_image(self.imgs.icon_border_mo) .press_image(self.imgs.icon_border_press) - .with_tooltip(tooltip_manager, "Danari", "", &tooltip_human) + .with_tooltip( + tooltip_manager, + &localized_strings.get("common.races.danari"), + "", + &tooltip_human, + ) .set(self.ids.race_6, ui_widgets) .was_clicked() { @@ -924,7 +960,12 @@ impl CharSelectionUi { .middle_of(self.ids.hammer) .hover_image(self.imgs.icon_border_mo) .press_image(self.imgs.icon_border_press) - .with_tooltip(tooltip_manager, "Hammer", "", &tooltip_human) + .with_tooltip( + tooltip_manager, + &localized_strings.get("common.weapons.hammer"), + "", + &tooltip_human, + ) .set(self.ids.hammer_button, ui_widgets) .was_clicked() { @@ -949,7 +990,12 @@ impl CharSelectionUi { .middle_of(self.ids.bow) .hover_image(self.imgs.icon_border_mo) .press_image(self.imgs.icon_border_press) - .with_tooltip(tooltip_manager, "Bow", "", &tooltip_human) + .with_tooltip( + tooltip_manager, + &localized_strings.get("common.weapons.bow"), + "", + &tooltip_human, + ) .set(self.ids.bow_button, ui_widgets) .was_clicked() { @@ -972,7 +1018,12 @@ impl CharSelectionUi { .middle_of(self.ids.staff) .hover_image(self.imgs.icon_border_mo) .press_image(self.imgs.icon_border_press) - .with_tooltip(tooltip_manager, "Staff", "", &tooltip_human) + .with_tooltip( + tooltip_manager, + &localized_strings.get("common.weapons.staff"), + "", + &tooltip_human, + ) .set(self.ids.staff_button, ui_widgets) .was_clicked() { @@ -995,7 +1046,12 @@ impl CharSelectionUi { .middle_of(self.ids.sword) .hover_image(self.imgs.icon_border_mo) .press_image(self.imgs.icon_border_press) - .with_tooltip(tooltip_manager, "Sword", "", &tooltip_human) + .with_tooltip( + tooltip_manager, + &localized_strings.get("common.weapons.sword"), + "", + &tooltip_human, + ) .set(self.ids.sword_button, ui_widgets) .was_clicked() { @@ -1038,7 +1094,12 @@ impl CharSelectionUi { .middle_of(self.ids.axe) .hover_image(self.imgs.icon_border_mo) .press_image(self.imgs.icon_border_press) - .with_tooltip(tooltip_manager, "Axe", "", &tooltip_human) + .with_tooltip( + tooltip_manager, + &localized_strings.get("common.weapons.axe"), + "", + &tooltip_human, + ) .set(self.ids.axe_button, ui_widgets) .was_clicked() { @@ -1056,13 +1117,13 @@ impl CharSelectionUi { self.imgs.slider_range, ); let char_slider = move |prev_id, - text, + text: String, text_id, max, selected_val, slider_id, ui_widgets: &mut UiCell| { - Text::new(text) + Text::new(&text.clone()) .down_from(prev_id, 22.0) .align_middle_x_of(prev_id) .font_size(18) @@ -1081,7 +1142,7 @@ impl CharSelectionUi { // Hair Style if let Some(new_val) = char_slider( self.ids.creation_buttons_alignment_2, - "Hair Style", + localized_strings.get("char_selection.hair_style"), self.ids.hairstyle_text, self.character_body .race @@ -1096,7 +1157,7 @@ impl CharSelectionUi { // Hair Color if let Some(new_val) = char_slider( self.ids.hairstyle_slider, - "Hair Color", + localized_strings.get("char_selection.hair_color"), self.ids.haircolor_text, self.character_body.race.num_hair_colors() as usize - 1, self.character_body.hair_color as usize, @@ -1108,7 +1169,7 @@ impl CharSelectionUi { // Skin if let Some(new_val) = char_slider( self.ids.haircolor_slider, - "Skin", + localized_strings.get("char_selection.skin"), self.ids.skin_text, self.character_body.race.num_skin_colors() as usize - 1, self.character_body.skin as usize, @@ -1121,7 +1182,7 @@ impl CharSelectionUi { let current_eyebrows = self.character_body.eyebrows; if let Some(new_val) = char_slider( self.ids.skin_slider, - "Eyebrows", + localized_strings.get("char_selection.eyebrows"), self.ids.eyebrows_text, humanoid::ALL_EYEBROWS.len() - 1, humanoid::ALL_EYEBROWS @@ -1136,7 +1197,7 @@ impl CharSelectionUi { // EyeColor if let Some(new_val) = char_slider( self.ids.eyebrows_slider, - "Eye Color", + localized_strings.get("char_selection.eye_color"), self.ids.eyecolor_text, self.character_body.race.num_eye_colors() as usize - 1, self.character_body.eye_color as usize, @@ -1149,7 +1210,7 @@ impl CharSelectionUi { let _current_accessory = self.character_body.accessory; if let Some(new_val) = char_slider( self.ids.eyecolor_slider, - "Accessories", + localized_strings.get("char_selection.accessories"), self.ids.accessories_text, self.character_body .race @@ -1170,7 +1231,7 @@ impl CharSelectionUi { { if let Some(new_val) = char_slider( self.ids.accessories_slider, - "Beard", + localized_strings.get("char_selection.beard"), self.ids.beard_text, self.character_body .race @@ -1183,7 +1244,7 @@ impl CharSelectionUi { self.character_body.beard = new_val as u8; } } else { - Text::new("Beard") + Text::new(&localized_strings.get("char_selection.beard")) .mid_bottom_with_margin_on(self.ids.accessories_slider, -40.0) .font_size(18) .font_id(self.fonts.cyri) @@ -1203,7 +1264,7 @@ impl CharSelectionUi { let current_chest = self.character_body.chest; if let Some(new_val) = char_slider( self.ids.beard_slider, - "Chest Color", + localized_strings.get("char_selection.chest_color"), self.ids.chest_text, humanoid::ALL_CHESTS.len() - 1, humanoid::ALL_CHESTS diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index 511341e14e..15df6e4da6 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -4,11 +4,14 @@ mod ui; use super::char_selection::CharSelectionState; use crate::{ - singleplayer::Singleplayer, window::Event, Direction, GlobalState, PlayState, PlayStateResult, + i18n::{i18n_asset_key, VoxygenLocalization}, + singleplayer::Singleplayer, + window::Event, + Direction, GlobalState, PlayState, PlayStateResult, }; use argon2::{self, Config}; use client_init::{ClientInit, Error as InitError}; -use common::{clock::Clock, comp}; +use common::{assets::load_expect, clock::Clock, comp}; use log::warn; #[cfg(feature = "singleplayer")] use std::time::Duration; @@ -147,9 +150,13 @@ impl PlayState for MainMenuState { } } } + let localized_strings = load_expect::(&i18n_asset_key( + &global_state.settings.language.selected_language, + )); if let Some(info) = global_state.info_message.take() { - self.main_menu_ui.show_info(info); + self.main_menu_ui + .show_info(info, localized_strings.get("common.okay")); } // Draw the UI to the screen. diff --git a/voxygen/src/menu/main/ui.rs b/voxygen/src/menu/main/ui.rs index d03150abd4..44c1706fb3 100644 --- a/voxygen/src/menu/main/ui.rs +++ b/voxygen/src/menu/main/ui.rs @@ -1,3 +1,4 @@ +use crate::i18n::{i18n_asset_key, VoxygenLocalization}; use crate::ui::Graphic; use crate::{ render::Renderer, @@ -219,13 +220,11 @@ impl MainMenuUi { const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); const TEXT_COLOR_2: Color = Color::Rgba(1.0, 1.0, 1.0, 0.2); //const INACTIVE: Color = Color::Rgba(0.47, 0.47, 0.47, 0.47); - let intro_text: &'static str = "Information on the Login Process:\n\ - \n\ - The name you put in will be your character name ingame.\n\ - \n\ - Character names and appearances will be saved on your computer.\n\ - \n\ - Levels/Items are not saved yet."; + + let localized_strings = load_expect::(&i18n_asset_key( + &global_state.settings.language.selected_language, + )); + let intro_text = &localized_strings.get("main.login_process"); // Tooltip let _tooltip = Tooltip::new({ @@ -349,39 +348,18 @@ impl MainMenuUi { .scroll_kids_vertically() .set(self.ids.disc_window, ui_widgets); - Text::new("Disclaimer") + Text::new(&localized_strings.get("common.disclaimer")) .top_left_with_margins_on(self.ids.disc_window, 30.0, 40.0) .font_size(35) .font_id(self.fonts.alkhemi) .color(TEXT_COLOR) .set(self.ids.disc_text_1, ui_widgets); - Text::new( - "Welcome to the alpha version of Veloren!\n\ - \n\ - \n\ - Before you dive into the fun, please keep a few things in mind:\n\ - \n\ - - This is a very early alpha. Expect bugs, extremely unfinished gameplay, unpolished mechanics, and missing features. \n\ - \n\ - -If you have constructive feedback or bug reports, you can contact us via Reddit, GitLab, or our community Discord server.\n\ - \n\ - - Veloren is licensed under the GPL 3 open-source licence. That means you're free to play, modify, and redistribute the game however you wish \n\ - (provided derived work is also under GPL 3). - \n\ - - Veloren is a non-profit community project, and everybody working on it is a volunteer.\n\ - If you like what you see, you're welcome to join the development or art teams! - \n\ - - 'Voxel RPG' is a genre in its own right. First-person shooters used to be called Doom clones.\n\ - Like them, we're trying to build a niche. This game is not a clone, and its development will diverge from existing games in the future.\n\ - \n\ - Thanks for taking the time to read this notice, we hope you enjoy the game!\n\ - \n\ - ~ The Veloren Devs") - .top_left_with_margins_on(self.ids.disc_window, 110.0, 40.0) - .font_size(26) - .font_id(self.fonts.cyri) - .color(TEXT_COLOR) - .set(self.ids.disc_text_2, ui_widgets); + Text::new(&localized_strings.get("main.notice")) + .top_left_with_margins_on(self.ids.disc_window, 110.0, 40.0) + .font_size(26) + .font_id(self.fonts.cyri) + .color(TEXT_COLOR) + .set(self.ids.disc_text_2, ui_widgets); if Button::image(self.imgs.button) .w_h(300.0, 50.0) .mid_bottom_with_margin_on(self.ids.disc_window, 30.0) @@ -407,8 +385,8 @@ impl MainMenuUi { self.connect = true; self.connecting = Some(std::time::Instant::now()); self.popup = Some(PopupData { - msg: "Connecting...".to_string(), - button_text: "Cancel".to_string(), + msg: localized_strings.get("main.connecting") + "...", + button_text: localized_strings.get("common.cancel"), popup_type: PopupType::ConnectionInfo, }); @@ -445,8 +423,8 @@ impl MainMenuUi { self.connect = true; self.connecting = Some(std::time::Instant::now()); self.popup = Some(PopupData { - msg: "Creating World...".to_string(), - button_text: "Cancel".to_string(), + msg: localized_strings.get("main.creating_world") + "...", + button_text: localized_strings.get("common.cancel"), popup_type: PopupType::ConnectionInfo, }); }; @@ -565,7 +543,7 @@ impl MainMenuUi { .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) .label_y(Relative::Scalar(2.0)) - .label("Close") + .label(&localized_strings.get("common.close")) .label_font_size(20) .label_font_id(self.fonts.cyri) .label_color(TEXT_COLOR) @@ -610,7 +588,7 @@ impl MainMenuUi { .w_h(258.0, 55.0) .down_from(self.ids.address_bg, 20.0) .align_middle_x_of(self.ids.address_bg) - .label("Multiplayer") + .label(&localized_strings.get("common.multiplayer")) .label_font_id(self.fonts.cyri) .label_color(TEXT_COLOR) .label_font_size(22) @@ -637,7 +615,7 @@ impl MainMenuUi { .w_h(258.0, 55.0) .down_from(self.ids.login_button, 20.0) .align_middle_x_of(self.ids.address_bg) - .label("Singleplayer") + .label(&localized_strings.get("common.singleplayer")) .label_font_id(self.fonts.cyri) .label_color(TEXT_COLOR) .label_font_size(22) @@ -655,7 +633,7 @@ impl MainMenuUi { .bottom_left_with_margins_on(ui_widgets.window, 60.0, 30.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Quit") + .label(&localized_strings.get("common.quit")) .label_font_id(self.fonts.cyri) .label_color(TEXT_COLOR) .label_font_size(20) @@ -672,7 +650,7 @@ impl MainMenuUi { .up_from(self.ids.quit_button, 8.0) //.hover_image(self.imgs.button_hover) //.press_image(self.imgs.button_press) - .label("Settings") + .label(&localized_strings.get("common.settings")) .label_font_id(self.fonts.cyri) .label_color(TEXT_COLOR_2) .label_font_size(20) @@ -689,7 +667,7 @@ impl MainMenuUi { .up_from(self.ids.settings_button, 8.0) .hover_image(self.imgs.button_hover) .press_image(self.imgs.button_press) - .label("Servers") + .label(&localized_strings.get("common.servers")) .label_font_id(self.fonts.cyri) .label_color(TEXT_COLOR) .label_font_size(20) @@ -705,10 +683,10 @@ impl MainMenuUi { events } - pub fn show_info(&mut self, msg: String) { + pub fn show_info(&mut self, msg: String, button_text: String) { self.popup = Some(PopupData { msg, - button_text: "Okay".to_string(), + button_text: button_text, popup_type: PopupType::Error, }); self.connecting = None; diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index de9b462ecf..e33a377942 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -1,3 +1,4 @@ +use crate::i18n::{i18n_asset_key, VoxygenLocalization}; use crate::{ ecs::MyEntity, hud::{DebugInfo, Event as HudEvent, Hud}, @@ -9,6 +10,8 @@ use crate::{ }; use client::{self, Client, Event::Chat}; use common::{ + assets::load_watched, + assets::watch, clock::Clock, comp, comp::{Pos, Vel}, @@ -132,6 +135,14 @@ impl PlayState for SessionState { } } + // Keep a watcher on the language + let mut localization_watcher = watch::ReloadIndicator::new(); + let mut localized_strings = load_watched::( + &i18n_asset_key(&global_state.settings.language.selected_language), + &mut localization_watcher, + ) + .unwrap(); + // Game loop let mut current_client_state = self.client.borrow().get_client_state(); while let ClientState::Pending | ClientState::Character = current_client_state { @@ -363,10 +374,7 @@ impl PlayState for SessionState { // Perform an in-game tick. if let Err(err) = self.tick(clock.get_avg_delta()) { - global_state.info_message = Some( - "Connection lost!\nDid the server restart?\nIs the client up to date?" - .to_owned(), - ); + global_state.info_message = Some(localized_strings.get("common.connection_lost")); error!("[session] Failed to tick the scene: {:?}", err); return PlayStateResult::Pop; @@ -376,7 +384,7 @@ impl PlayState for SessionState { global_state.maintain(clock.get_last_delta().as_secs_f32()); // Extract HUD events ensuring the client borrow gets dropped. - let hud_events = self.hud.maintain( + let mut hud_events = self.hud.maintain( &self.client.borrow(), global_state, DebugInfo { @@ -407,6 +415,11 @@ impl PlayState for SessionState { clock.get_last_delta(), ); + // Look for changes in the localization files + if localization_watcher.reloaded() { + hud_events.push(HudEvent::ChangeLanguage(localized_strings.metadata.clone())); + } + // Maintain the UI. for event in hud_events { match event { @@ -560,6 +573,16 @@ impl PlayState for SessionState { global_state.settings.graphics.fluid_mode = new_fluid_mode; global_state.settings.save_to_file_warn(); } + HudEvent::ChangeLanguage(new_language) => { + global_state.settings.language.selected_language = + new_language.language_identifier; + localized_strings = load_watched::( + &i18n_asset_key(&global_state.settings.language.selected_language), + &mut localization_watcher, + ) + .unwrap(); + localized_strings.log_missing_entries(); + } } } diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index 980b1c1417..072201f376 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -1,6 +1,8 @@ use crate::{ hud::{BarNumbers, CrosshairType, Intro, ShortcutNumbers, XpBar}, - render::{AaMode, CloudMode, FluidMode}, + i18n, + render::AaMode, + render::{CloudMode, FluidMode}, ui::ScaleMode, window::KeyMouse, }; @@ -234,6 +236,20 @@ impl Default for AudioSettings { } } +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(default)] +pub struct LanguageSettings { + pub selected_language: String, +} + +impl Default for LanguageSettings { + fn default() -> Self { + Self { + selected_language: i18n::REFERENCE_LANG.to_string(), + } + } +} + /// `Settings` contains everything that can be configured in the settings.ron file. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] @@ -248,6 +264,7 @@ pub struct Settings { pub send_logon_commands: bool, // TODO: Remove at a later date, for dev testing pub logon_commands: Vec, + pub language: LanguageSettings, } impl Default for Settings { @@ -262,6 +279,7 @@ impl Default for Settings { show_disclaimer: true, send_logon_commands: false, logon_commands: Vec::new(), + language: LanguageSettings::default(), } } }