From c7d6dde67705e235097b7543d85a95d7ab04f465 Mon Sep 17 00:00:00 2001
From: Ada Lovegirls <8178784-adalovegirls@users.noreply.gitlab.com>
Date: Sat, 24 Apr 2021 14:39:35 +0000
Subject: [PATCH] Add option to load English string as fallback if string
 missing

---
 CHANGELOG.md                                |   1 +
 assets/voxygen/i18n/en/hud/hud_settings.ron |   2 +
 common/src/assets.rs                        |   1 +
 voxygen/src/hud/mod.rs                      |   8 +-
 voxygen/src/hud/prompt_dialog.rs            |   9 +-
 voxygen/src/hud/settings_window/language.rs |  48 ++++-
 voxygen/src/hud/settings_window/mod.rs      |   2 +-
 voxygen/src/i18n.rs                         | 219 +++++++++++++++-----
 voxygen/src/lib.rs                          |   6 +-
 voxygen/src/main.rs                         |   9 +-
 voxygen/src/menu/char_selection/mod.rs      |   2 +-
 voxygen/src/menu/char_selection/ui/mod.rs   |  13 +-
 voxygen/src/menu/main/mod.rs                |   9 +-
 voxygen/src/menu/main/ui/mod.rs             |  22 +-
 voxygen/src/session/mod.rs                  |   2 +-
 voxygen/src/session/settings_change.rs      |  15 +-
 voxygen/src/settings/language.rs            |   2 +
 17 files changed, 267 insertions(+), 103 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 31335a5a0e..9d88ec26da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -44,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Buoyancy is calculated from the difference in density between an entity and surrounding fluid
 - Drag is now calculated based on physical properties
 - Terrain chunks are now deflate-compressed when sent over the network.
+- Missing translations can be displayed in English.
 
 ### Changed
 
diff --git a/assets/voxygen/i18n/en/hud/hud_settings.ron b/assets/voxygen/i18n/en/hud/hud_settings.ron
index 7706b81b24..d7c5ffc949 100644
--- a/assets/voxygen/i18n/en/hud/hud_settings.ron
+++ b/assets/voxygen/i18n/en/hud/hud_settings.ron
@@ -101,6 +101,8 @@
         "hud.settings.audio_device": "Audio Device",
         "hud.settings.reset_sound": "Reset to Defaults",
 
+        "hud.settings.english_fallback": "Display English for missing translations",
+
         "hud.settings.awaitingkey": "Press a key...",
         "hud.settings.unbound": "None",
         "hud.settings.reset_keybinds": "Reset to Defaults",
diff --git a/common/src/assets.rs b/common/src/assets.rs
index cf016cdca2..737810da46 100644
--- a/common/src/assets.rs
+++ b/common/src/assets.rs
@@ -26,6 +26,7 @@ lazy_static! {
 pub fn start_hot_reloading() { ASSETS.enhance_hot_reloading(); }
 
 pub type AssetHandle<T> = assets_manager::Handle<'static, T>;
+pub type AssetGuard<T> = assets_manager::AssetGuard<'static, T>;
 pub type AssetDir<T> = assets_manager::DirReader<'static, T, source::FileSystem>;
 
 /// The Asset trait, which is implemented by all structures that have their data
diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs
index a08f66aa5a..71fa2d015c 100644
--- a/voxygen/src/hud/mod.rs
+++ b/voxygen/src/hud/mod.rs
@@ -797,7 +797,7 @@ impl Hud {
         // Load item images.
         let item_imgs = ItemImgs::new(&mut ui, imgs.not_found);
         // Load fonts.
-        let fonts = Fonts::load(&global_state.i18n.read().fonts, &mut ui)
+        let fonts = Fonts::load(global_state.i18n.read().fonts(), &mut ui)
             .expect("Impossible to load fonts!");
         // Get the server name.
         let server = &client.server_info().name;
@@ -889,7 +889,7 @@ impl Hud {
     }
 
     pub fn update_fonts(&mut self, i18n: &Localization) {
-        self.fonts = Fonts::load(&i18n.fonts, &mut self.ui).expect("Impossible to load fonts!");
+        self.fonts = Fonts::load(i18n.fonts(), &mut self.ui).expect("Impossible to load fonts!");
     }
 
     #[allow(clippy::assign_op_pattern)] // TODO: Pending review in #587
@@ -913,7 +913,7 @@ impl Hud {
         // FPS
         let fps = global_state.clock.stats().average_tps;
         let version = common::util::DISPLAY_VERSION_LONG.clone();
-        let i18n = &*global_state.i18n.read();
+        let i18n = &global_state.i18n.read();
         let key_layout = &global_state.window.key_layout;
 
         if self.show.ingame {
@@ -2438,7 +2438,7 @@ impl Hud {
                     client,
                     &self.imgs,
                     &self.fonts,
-                    i18n,
+                    &*i18n,
                     self.pulse,
                     &self.rot_imgs,
                     item_tooltip_manager,
diff --git a/voxygen/src/hud/prompt_dialog.rs b/voxygen/src/hud/prompt_dialog.rs
index 7886f8b5eb..409495d53d 100644
--- a/voxygen/src/hud/prompt_dialog.rs
+++ b/voxygen/src/hud/prompt_dialog.rs
@@ -1,11 +1,10 @@
 use super::{img_ids::Imgs, TEXT_COLOR, UI_HIGHLIGHT_0};
 use crate::{
     hud::{Event, PromptDialogSettings},
-    i18n::Localization,
+    i18n::LocalizationHandle,
     settings::Settings,
     ui::fonts::Fonts,
     window::GameInput,
-    AssetHandle,
 };
 use conrod_core::{
     widget::{self, Button, Image, Text},
@@ -32,7 +31,7 @@ pub struct PromptDialog<'a> {
     fonts: &'a Fonts,
     #[conrod(common_builder)]
     common: widget::CommonBuilder,
-    localized_strings: &'a AssetHandle<Localization>,
+    localized_strings: &'a LocalizationHandle,
     settings: &'a Settings,
     prompt_dialog_settings: &'a PromptDialogSettings,
     key_layout: &'a Option<KeyLayout>,
@@ -43,7 +42,7 @@ impl<'a> PromptDialog<'a> {
     pub fn new(
         imgs: &'a Imgs,
         fonts: &'a Fonts,
-        localized_strings: &'a AssetHandle<Localization>,
+        localized_strings: &'a LocalizationHandle,
         settings: &'a Settings,
         prompt_dialog_settings: &'a PromptDialogSettings,
         key_layout: &'a Option<KeyLayout>,
@@ -85,7 +84,7 @@ impl<'a> Widget for PromptDialog<'a> {
 
     fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
         let widget::UpdateArgs { state, ui, .. } = args;
-        let _localized_strings = self.localized_strings;
+        let _localized_strings = &self.localized_strings;
         let mut event: Option<DialogOutcomeEvent> = None;
 
         let accept_key = self
diff --git a/voxygen/src/hud/settings_window/language.rs b/voxygen/src/hud/settings_window/language.rs
index 9117124889..285a70df19 100644
--- a/voxygen/src/hud/settings_window/language.rs
+++ b/voxygen/src/hud/settings_window/language.rs
@@ -1,13 +1,13 @@
 use crate::{
     hud::{img_ids::Imgs, TEXT_COLOR},
-    i18n::list_localizations,
+    i18n::{list_localizations, Localization},
     session::settings_change::{Language as LanguageChange, Language::*},
-    ui::fonts::Fonts,
+    ui::{fonts::Fonts, ToggleButton},
     GlobalState,
 };
 use conrod_core::{
     color,
-    widget::{self, Button, Rectangle, Scrollbar},
+    widget::{self, Button, Rectangle, Scrollbar, Text},
     widget_ids, Colorable, Labelable, Positionable, Sizeable, Widget, WidgetCommon,
 };
 
@@ -15,6 +15,8 @@ widget_ids! {
     struct Ids {
         window,
         window_r,
+        english_fallback_button,
+        english_fallback_button_label,
         window_scrollbar,
         language_list[],
     }
@@ -23,15 +25,22 @@ widget_ids! {
 #[derive(WidgetCommon)]
 pub struct Language<'a> {
     global_state: &'a GlobalState,
+    localized_strings: &'a Localization,
     imgs: &'a Imgs,
     fonts: &'a Fonts,
     #[conrod(common_builder)]
     common: widget::CommonBuilder,
 }
 impl<'a> Language<'a> {
-    pub fn new(global_state: &'a GlobalState, imgs: &'a Imgs, fonts: &'a Fonts) -> Self {
+    pub fn new(
+        global_state: &'a GlobalState,
+        imgs: &'a Imgs,
+        fonts: &'a Fonts,
+        localized_strings: &'a Localization,
+    ) -> Self {
         Self {
             global_state,
+            localized_strings,
             imgs,
             fonts,
             common: widget::CommonBuilder::default(),
@@ -79,6 +88,7 @@ impl<'a> Widget for Language<'a> {
 
         // List available languages
         let selected_language = &self.global_state.settings.language.selected_language;
+        let english_fallback = self.global_state.settings.language.use_english_fallback;
         let language_list = list_localizations();
         if state.ids.language_list.len() < language_list.len() {
             state.update(|state| {
@@ -117,6 +127,36 @@ impl<'a> Widget for Language<'a> {
             }
         }
 
+        // English as fallback language
+        let show_english_fallback = ToggleButton::new(
+            english_fallback,
+            self.imgs.checkbox,
+            self.imgs.checkbox_checked,
+        )
+        .w_h(18.0, 18.0);
+        let show_english_fallback = if let Some(id) = state.ids.language_list.last() {
+            show_english_fallback.down_from(*id, 8.0)
+            //mid_bottom_with_margin_on(id, -button_h)
+        } else {
+            show_english_fallback.mid_top_with_margin_on(state.ids.window, 20.0)
+        };
+        let show_english_fallback = show_english_fallback
+            .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
+            .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
+            .set(state.ids.english_fallback_button, ui);
+
+        if english_fallback != show_english_fallback {
+            events.push(ToggleEnglishFallback(show_english_fallback));
+        }
+
+        Text::new(&self.localized_strings.get("hud.settings.english_fallback"))
+            .right_from(state.ids.english_fallback_button, 10.0)
+            .font_size(self.fonts.cyri.scale(14))
+            .font_id(self.fonts.cyri.conrod_id)
+            .graphics_for(state.ids.english_fallback_button)
+            .color(TEXT_COLOR)
+            .set(state.ids.english_fallback_button_label, ui);
+
         events
     }
 }
diff --git a/voxygen/src/hud/settings_window/mod.rs b/voxygen/src/hud/settings_window/mod.rs
index 640d01581f..9b6388c514 100644
--- a/voxygen/src/hud/settings_window/mod.rs
+++ b/voxygen/src/hud/settings_window/mod.rs
@@ -288,7 +288,7 @@ impl<'a> Widget for SettingsWindow<'a> {
                 }
             },
             SettingsTab::Lang => {
-                for change in language::Language::new(global_state, imgs, fonts)
+                for change in language::Language::new(global_state, imgs, fonts, localized_strings)
                     .top_left_with_margins_on(state.ids.settings_content_align, 0.0, 0.0)
                     .wh_of(state.ids.settings_content_align)
                     .set(state.ids.language, ui)
diff --git a/voxygen/src/i18n.rs b/voxygen/src/i18n.rs
index 6002b810b4..5b04959fd3 100644
--- a/voxygen/src/i18n.rs
+++ b/voxygen/src/i18n.rs
@@ -1,4 +1,4 @@
-use common::assets::{self, AssetExt};
+use common::assets::{self, AssetExt, AssetGuard, AssetHandle};
 use deunicode::deunicode;
 use hashbrown::{HashMap, HashSet};
 use serde::{Deserialize, Serialize};
@@ -45,7 +45,7 @@ pub type Fonts = HashMap<String, Font>;
 
 /// Raw localization data, expect the strings to not be loaded here
 /// However, metadata informations are correct
-/// See `Localization` for more info on each attributes
+/// See `Language` for more info on each attributes
 #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
 pub struct RawLocalization {
     pub sub_directories: Vec<String>,
@@ -58,7 +58,7 @@ pub struct RawLocalization {
 
 /// Store internationalization data
 #[derive(Debug, PartialEq, Serialize, Deserialize)]
-pub struct Localization {
+struct Language {
     /// A list of subdirectories to lookup for localization files
     pub sub_directories: Vec<String>,
 
@@ -83,7 +83,7 @@ pub struct Localization {
 }
 
 /// Store internationalization maps
-/// These structs are meant to be merged into a Localization
+/// These structs are meant to be merged into a Language
 #[derive(Debug, PartialEq, Serialize, Deserialize)]
 pub struct LocalizationFragment {
     /// A map storing the localized texts
@@ -97,16 +97,13 @@ pub struct LocalizationFragment {
     pub vector_map: HashMap<String, Vec<String>>,
 }
 
-impl Localization {
+impl Language {
     /// 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<'a>(&'a self, key: &'a str) -> &str {
-        match self.string_map.get(key) {
-            Some(localized_text) => localized_text,
-            None => key,
-        }
+    pub fn get<'a>(&'a self, key: &'a str) -> Option<&str> {
+        self.string_map.get(key).map(|s| s.as_str())
     }
 
     /// Get a variation of localized text from the given key
@@ -115,55 +112,32 @@ impl Localization {
     ///
     /// If the key is not present in the localization object
     /// then the key is returned.
-    pub fn get_variation<'a>(&'a self, key: &'a str, index: u16) -> &str {
-        match self.vector_map.get(key) {
-            Some(v) if !v.is_empty() => &v[index as usize % v.len()],
-            _ => key,
-        }
+    pub fn get_variation<'a>(&'a self, key: &'a str, index: u16) -> Option<&str> {
+        self.vector_map
+            .get(key)
+            .map(|v| {
+                if !v.is_empty() {
+                    Some(v[index as usize % v.len()].as_str())
+                } else {
+                    None
+                }
+            })
+            .flatten()
     }
+}
 
-    /// Return the missing keys compared to the reference language
-    fn list_missing_entries(&self) -> (HashSet<String>, HashSet<String>) {
-        let reference_localization =
-            Localization::load_expect(&i18n_asset_key(REFERENCE_LANG)).read();
-
-        let reference_string_keys: HashSet<_> =
-            reference_localization.string_map.keys().cloned().collect();
-        let string_keys: HashSet<_> = self.string_map.keys().cloned().collect();
-        let strings = reference_string_keys
-            .difference(&string_keys)
-            .cloned()
-            .collect();
-
-        let reference_vector_keys: HashSet<_> =
-            reference_localization.vector_map.keys().cloned().collect();
-        let vector_keys: HashSet<_> = self.vector_map.keys().cloned().collect();
-        let vectors = reference_vector_keys
-            .difference(&vector_keys)
-            .cloned()
-            .collect();
-
-        (strings, vectors)
-    }
-
-    /// Log missing entries (compared to the reference language) as warnings
-    pub fn log_missing_entries(&self) {
-        let (missing_strings, missing_vectors) = self.list_missing_entries();
-        for missing_key in missing_strings {
-            warn!(
-                "[{:?}] Missing string key {:?}",
-                self.metadata.language_identifier, missing_key
-            );
-        }
-        for missing_key in missing_vectors {
-            warn!(
-                "[{:?}] Missing vector key {:?}",
-                self.metadata.language_identifier, missing_key
-            );
+impl Default for Language {
+    fn default() -> Self {
+        Self {
+            sub_directories: Vec::default(),
+            string_map: HashMap::default(),
+            vector_map: HashMap::default(),
+            ..Default::default()
         }
     }
 }
-impl From<RawLocalization> for Localization {
+
+impl From<RawLocalization> for Language {
     fn from(raw: RawLocalization) -> Self {
         Self {
             sub_directories: raw.sub_directories,
@@ -195,7 +169,7 @@ impl assets::Asset for LocalizationFragment {
     const EXTENSION: &'static str = "ron";
 }
 
-impl assets::Compound for Localization {
+impl assets::Compound for Language {
     fn load<S: assets::source::Source>(
         cache: &assets::AssetCache<S>,
         asset_key: &str,
@@ -203,7 +177,7 @@ impl assets::Compound for Localization {
         let raw = cache
             .load::<RawLocalization>(&[asset_key, ".", LANG_MANIFEST_FILE].concat())?
             .cloned();
-        let mut localization = Localization::from(raw);
+        let mut localization = Language::from(raw);
 
         // Walk through files in the folder, collecting localization fragment to merge
         // inside the asked_localization
@@ -247,6 +221,139 @@ impl assets::Compound for Localization {
     }
 }
 
+/// the central data structure to handle localization in veloren
+// inherit Copy+Clone from AssetHandle
+#[derive(Debug, PartialEq, Copy, Clone)]
+pub struct LocalizationHandle {
+    active: AssetHandle<Language>,
+    fallback: Option<AssetHandle<Language>>,
+    pub use_english_fallback: bool,
+}
+
+// RAII guard returned from Localization::read(), resembles AssetGuard
+pub struct LocalizationGuard {
+    active: AssetGuard<Language>,
+    fallback: Option<AssetGuard<Language>>,
+}
+
+// arbitrary choice to minimize changing all of veloren
+pub type Localization = LocalizationGuard;
+
+impl LocalizationGuard {
+    /// 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<'a>(&'a self, key: &'a str) -> &str {
+        self.active.get(key).unwrap_or_else(|| {
+            self.fallback
+                .as_ref()
+                .map(|f| f.get(key))
+                .flatten()
+                .unwrap_or(key)
+        })
+    }
+
+    /// Get a variation of localized text from the given key
+    ///
+    /// `index` should be a random number from `0` to `u16::max()`
+    ///
+    /// If the key is not present in the localization object
+    /// then the key is returned.
+    pub fn get_variation<'a>(&'a self, key: &'a str, index: u16) -> &str {
+        self.active.get_variation(key, index).unwrap_or_else(|| {
+            self.fallback
+                .as_ref()
+                .map(|f| f.get_variation(key, index))
+                .flatten()
+                .unwrap_or(key)
+        })
+    }
+
+    /// Return the missing keys compared to the reference language
+    fn list_missing_entries(&self) -> (HashSet<String>, HashSet<String>) {
+        if let Some(ref_lang) = &self.fallback {
+            let reference_string_keys: HashSet<_> = ref_lang.string_map.keys().cloned().collect();
+            let string_keys: HashSet<_> = self.active.string_map.keys().cloned().collect();
+            let strings = reference_string_keys
+                .difference(&string_keys)
+                .cloned()
+                .collect();
+
+            let reference_vector_keys: HashSet<_> = ref_lang.vector_map.keys().cloned().collect();
+            let vector_keys: HashSet<_> = self.active.vector_map.keys().cloned().collect();
+            let vectors = reference_vector_keys
+                .difference(&vector_keys)
+                .cloned()
+                .collect();
+
+            (strings, vectors)
+        } else {
+            (HashSet::default(), HashSet::default())
+        }
+    }
+
+    /// Log missing entries (compared to the reference language) as warnings
+    pub fn log_missing_entries(&self) {
+        let (missing_strings, missing_vectors) = self.list_missing_entries();
+        for missing_key in missing_strings {
+            warn!(
+                "[{:?}] Missing string key {:?}",
+                self.metadata().language_identifier,
+                missing_key
+            );
+        }
+        for missing_key in missing_vectors {
+            warn!(
+                "[{:?}] Missing vector key {:?}",
+                self.metadata().language_identifier,
+                missing_key
+            );
+        }
+    }
+
+    pub fn fonts(&self) -> &Fonts { &self.active.fonts }
+
+    pub fn metadata(&self) -> &LanguageMetadata { &self.active.metadata }
+}
+
+impl LocalizationHandle {
+    pub fn set_english_fallback(&mut self, use_english_fallback: bool) {
+        self.use_english_fallback = use_english_fallback;
+    }
+
+    pub fn read(&self) -> LocalizationGuard {
+        LocalizationGuard {
+            active: self.active.read(),
+            fallback: if self.use_english_fallback {
+                self.fallback.map(|f| f.read())
+            } else {
+                None
+            },
+        }
+    }
+
+    pub fn load(specifier: &str) -> Result<Self, common::assets::Error> {
+        let default_key = i18n_asset_key(REFERENCE_LANG);
+        let is_default = specifier == default_key;
+        Ok(Self {
+            active: Language::load(specifier)?,
+            fallback: if is_default {
+                None
+            } else {
+                Language::load(&default_key).ok()
+            },
+            use_english_fallback: false,
+        })
+    }
+
+    pub fn load_expect(specifier: &str) -> Self {
+        Self::load(specifier).expect("Can't load language files")
+    }
+
+    pub fn reloaded(&mut self) -> bool { self.active.reloaded() }
+}
+
 #[derive(Clone, Debug)]
 struct LocalizationList(Vec<LanguageMetadata>);
 
diff --git a/voxygen/src/lib.rs b/voxygen/src/lib.rs
index c5bfd4fe60..81df2ba090 100644
--- a/voxygen/src/lib.rs
+++ b/voxygen/src/lib.rs
@@ -40,13 +40,13 @@ pub use crate::error::Error;
 use crate::singleplayer::Singleplayer;
 use crate::{
     audio::AudioFrontend,
-    i18n::Localization,
+    i18n::LocalizationHandle,
     profile::Profile,
     render::Renderer,
     settings::Settings,
     window::{Event, Window},
 };
-use common::{assets::AssetHandle, clock::Clock};
+use common::clock::Clock;
 use common_base::span;
 
 /// A type used to store state that is shared between all play states.
@@ -61,7 +61,7 @@ pub struct GlobalState {
     #[cfg(feature = "singleplayer")]
     pub singleplayer: Option<Singleplayer>,
     // TODO: redo this so that the watcher doesn't have to exist for reloading to occur
-    pub i18n: AssetHandle<Localization>,
+    pub i18n: LocalizationHandle,
     pub clipboard: Option<iced_winit::Clipboard>,
     // NOTE: This can be removed from GlobalState if client state behavior is refactored to not
     // enter the game before confirmation of successful character load
diff --git a/voxygen/src/main.rs b/voxygen/src/main.rs
index 27003f2251..63ba0d7c48 100644
--- a/voxygen/src/main.rs
+++ b/voxygen/src/main.rs
@@ -5,7 +5,7 @@
 
 use veloren_voxygen::{
     audio::AudioFrontend,
-    i18n::{self, i18n_asset_key, Localization},
+    i18n::{self, i18n_asset_key, LocalizationHandle},
     profile::Profile,
     run,
     scene::terrain::SpriteRenderContext,
@@ -15,7 +15,7 @@ use veloren_voxygen::{
 };
 
 use common::{
-    assets::{self, AssetExt},
+    assets::{self},
     clock::Clock,
 };
 use std::panic;
@@ -163,7 +163,7 @@ fn main() {
     // Load the profile.
     let profile = Profile::load();
 
-    let i18n = Localization::load(&i18n_asset_key(&settings.language.selected_language))
+    let mut i18n = LocalizationHandle::load(&i18n_asset_key(&settings.language.selected_language))
         .unwrap_or_else(|error| {
             let selected_language = &settings.language.selected_language;
             warn!(
@@ -172,9 +172,10 @@ fn main() {
                 "Impossible to load language: change to the default language (English) instead.",
             );
             settings.language.selected_language = i18n::REFERENCE_LANG.to_owned();
-            Localization::load_expect(&i18n_asset_key(&settings.language.selected_language))
+            LocalizationHandle::load_expect(&i18n_asset_key(&settings.language.selected_language))
         });
     i18n.read().log_missing_entries();
+    i18n.set_english_fallback(settings.language.use_english_fallback);
 
     // Create window
     let (mut window, event_loop) = Window::new(&settings).expect("Failed to create window!");
diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs
index b51f4bfdd1..a7825976a7 100644
--- a/voxygen/src/menu/char_selection/mod.rs
+++ b/voxygen/src/menu/char_selection/mod.rs
@@ -166,7 +166,7 @@ impl PlayState for CharSelectionState {
             }
 
             // Tick the client (currently only to keep the connection alive).
-            let localized_strings = &*global_state.i18n.read();
+            let localized_strings = &global_state.i18n.read();
 
             match self.client.borrow_mut().tick(
                 comp::ControllerInputs::default(),
diff --git a/voxygen/src/menu/char_selection/ui/mod.rs b/voxygen/src/menu/char_selection/ui/mod.rs
index 2a8b8c142d..df028ea997 100644
--- a/voxygen/src/menu/char_selection/ui/mod.rs
+++ b/voxygen/src/menu/char_selection/ui/mod.rs
@@ -1,5 +1,5 @@
 use crate::{
-    i18n::Localization,
+    i18n::{Localization, LocalizationHandle},
     render::Renderer,
     ui::{
         self,
@@ -22,7 +22,6 @@ use crate::{
 };
 use client::{Client, ServerInfo};
 use common::{
-    assets::AssetHandle,
     character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER, MAX_NAME_LENGTH},
     comp::{self, humanoid, inventory::slot::EquipSlot, Inventory, Item},
     LoadoutBuilder,
@@ -1467,7 +1466,7 @@ impl CharSelectionUi {
         let i18n = global_state.i18n.read();
 
         // TODO: don't add default font twice
-        let font = ui::ice::load_font(&i18n.fonts.get("cyri").unwrap().asset_key);
+        let font = ui::ice::load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
 
         let mut ui = Ui::new(
             &mut global_state.window,
@@ -1476,7 +1475,7 @@ impl CharSelectionUi {
         )
         .unwrap();
 
-        let fonts = Fonts::load(&i18n.fonts, &mut ui).expect("Impossible to load fonts");
+        let fonts = Fonts::load(i18n.fonts(), &mut ui).expect("Impossible to load fonts");
 
         #[cfg(feature = "singleplayer")]
         let default_name = match global_state.singleplayer {
@@ -1538,13 +1537,13 @@ impl CharSelectionUi {
         }
     }
 
-    pub fn update_language(&mut self, i18n: AssetHandle<Localization>) {
+    pub fn update_language(&mut self, i18n: LocalizationHandle) {
         let i18n = i18n.read();
-        let font = ui::ice::load_font(&i18n.fonts.get("cyri").unwrap().asset_key);
+        let font = ui::ice::load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
 
         self.ui.clear_fonts(font);
         self.controls.fonts =
-            Fonts::load(&i18n.fonts, &mut self.ui).expect("Impossible to load fonts!");
+            Fonts::load(i18n.fonts(), &mut self.ui).expect("Impossible to load fonts!");
     }
 
     pub fn set_scale_mode(&mut self, scale_mode: ui::ScaleMode) {
diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs
index 76df1f8c19..642d902871 100644
--- a/voxygen/src/menu/main/mod.rs
+++ b/voxygen/src/menu/main/mod.rs
@@ -5,7 +5,7 @@ use super::char_selection::CharSelectionState;
 #[cfg(feature = "singleplayer")]
 use crate::singleplayer::Singleplayer;
 use crate::{
-    i18n::{i18n_asset_key, Localization},
+    i18n::{i18n_asset_key, Localization, LocalizationHandle},
     render::Renderer,
     settings::Settings,
     window::Event,
@@ -18,7 +18,7 @@ use client::{
     ServerInfo,
 };
 use client_init::{ClientConnArgs, ClientInit, Error as InitError, Msg as InitMsg};
-use common::{assets::AssetExt, comp};
+use common::comp;
 use common_base::span;
 use std::{fmt::Debug, sync::Arc};
 use tokio::runtime;
@@ -295,10 +295,13 @@ impl PlayState for MainMenuState {
                 MainMenuEvent::ChangeLanguage(new_language) => {
                     global_state.settings.language.selected_language =
                         new_language.language_identifier;
-                    global_state.i18n = Localization::load_expect(&i18n_asset_key(
+                    global_state.i18n = LocalizationHandle::load_expect(&i18n_asset_key(
                         &global_state.settings.language.selected_language,
                     ));
                     global_state.i18n.read().log_missing_entries();
+                    global_state
+                        .i18n
+                        .set_english_fallback(global_state.settings.language.use_english_fallback);
                     self.main_menu_ui
                         .update_language(global_state.i18n, &global_state.settings);
                 },
diff --git a/voxygen/src/menu/main/ui/mod.rs b/voxygen/src/menu/main/ui/mod.rs
index 24d04cde1f..063845266e 100644
--- a/voxygen/src/menu/main/ui/mod.rs
+++ b/voxygen/src/menu/main/ui/mod.rs
@@ -5,7 +5,7 @@ mod login;
 mod servers;
 
 use crate::{
-    i18n::{LanguageMetadata, Localization},
+    i18n::{LanguageMetadata, LocalizationHandle},
     render::Renderer,
     ui::{
         self,
@@ -19,7 +19,7 @@ use crate::{
 use iced::{text_input, Column, Container, HorizontalAlignment, Length, Row, Space};
 //ImageFrame, Tooltip,
 use crate::settings::Settings;
-use common::assets::{self, AssetExt, AssetHandle};
+use common::assets::{self, AssetExt};
 use rand::{seq::SliceRandom, thread_rng};
 use std::time::Duration;
 
@@ -124,7 +124,7 @@ struct Controls {
     fonts: Fonts,
     imgs: Imgs,
     bg_img: widget::image::Handle,
-    i18n: AssetHandle<Localization>,
+    i18n: LocalizationHandle,
     // Voxygen version
     version: String,
     // Alpha disclaimer
@@ -169,7 +169,7 @@ impl Controls {
         fonts: Fonts,
         imgs: Imgs,
         bg_img: widget::image::Handle,
-        i18n: AssetHandle<Localization>,
+        i18n: LocalizationHandle,
         settings: &Settings,
     ) -> Self {
         let version = common::util::DISPLAY_VERSION_LONG.clone();
@@ -480,9 +480,9 @@ pub struct MainMenuUi {
 impl<'a> MainMenuUi {
     pub fn new(global_state: &mut GlobalState) -> Self {
         // Load language
-        let i18n = &*global_state.i18n.read();
+        let i18n = &global_state.i18n.read();
         // TODO: don't add default font twice
-        let font = load_font(&i18n.fonts.get("cyri").unwrap().asset_key);
+        let font = load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
 
         let mut ui = Ui::new(
             &mut global_state.window,
@@ -491,7 +491,7 @@ impl<'a> MainMenuUi {
         )
         .unwrap();
 
-        let fonts = Fonts::load(&i18n.fonts, &mut ui).expect("Impossible to load fonts");
+        let fonts = Fonts::load(&i18n.fonts(), &mut ui).expect("Impossible to load fonts");
 
         let bg_img_spec = BG_IMGS.choose(&mut thread_rng()).unwrap();
 
@@ -507,13 +507,13 @@ impl<'a> MainMenuUi {
         Self { ui, controls }
     }
 
-    pub fn update_language(&mut self, i18n: AssetHandle<Localization>, settings: &Settings) {
+    pub fn update_language(&mut self, i18n: LocalizationHandle, settings: &Settings) {
         self.controls.i18n = i18n;
-        let i18n = &*i18n.read();
-        let font = load_font(&i18n.fonts.get("cyri").unwrap().asset_key);
+        let i18n = &i18n.read();
+        let font = load_font(&i18n.fonts().get("cyri").unwrap().asset_key);
         self.ui.clear_fonts(font);
         self.controls.fonts =
-            Fonts::load(&i18n.fonts, &mut self.ui).expect("Impossible to load fonts!");
+            Fonts::load(&i18n.fonts(), &mut self.ui).expect("Impossible to load fonts!");
         let language_metadatas = crate::i18n::list_localizations();
         self.controls.selected_language_index = language_metadatas
             .iter()
diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs
index 102cfd3201..fa1b98732c 100644
--- a/voxygen/src/session/mod.rs
+++ b/voxygen/src/session/mod.rs
@@ -914,7 +914,7 @@ impl PlayState for SessionState {
             // Look for changes in the localization files
             if global_state.i18n.reloaded() {
                 hud_events.push(HudEvent::SettingsChange(
-                    ChangeLanguage(Box::new(global_state.i18n.read().metadata.clone())).into(),
+                    ChangeLanguage(Box::new(global_state.i18n.read().metadata().clone())).into(),
                 ));
             }
 
diff --git a/voxygen/src/session/settings_change.rs b/voxygen/src/session/settings_change.rs
index d46f1c4c25..3e52b32183 100644
--- a/voxygen/src/session/settings_change.rs
+++ b/voxygen/src/session/settings_change.rs
@@ -5,7 +5,7 @@ use crate::{
         BarNumbers, BuffPosition, CrosshairType, Intro, PressBehavior, ScaleChange,
         ShortcutNumbers, XpBar,
     },
-    i18n::{i18n_asset_key, LanguageMetadata, Localization},
+    i18n::{i18n_asset_key, LanguageMetadata, LocalizationHandle},
     render::RenderMode,
     settings::{
         AudioSettings, ControlSettings, Fps, GamepadSettings, GameplaySettings, GraphicsSettings,
@@ -14,7 +14,6 @@ use crate::{
     window::{FullScreenSettings, GameInput},
     GlobalState,
 };
-use common::assets::AssetExt;
 use vek::*;
 
 #[derive(Clone)]
@@ -118,6 +117,7 @@ pub enum Interface {
 #[derive(Clone)]
 pub enum Language {
     ChangeLanguage(Box<LanguageMetadata>),
+    ToggleEnglishFallback(bool),
 }
 #[derive(Clone)]
 pub enum Networking {}
@@ -470,12 +470,21 @@ impl SettingsChange {
             SettingsChange::Language(language_change) => match language_change {
                 Language::ChangeLanguage(new_language) => {
                     settings.language.selected_language = new_language.language_identifier;
-                    global_state.i18n = Localization::load_expect(&i18n_asset_key(
+                    global_state.i18n = LocalizationHandle::load_expect(&i18n_asset_key(
                         &settings.language.selected_language,
                     ));
                     global_state.i18n.read().log_missing_entries();
+                    global_state
+                        .i18n
+                        .set_english_fallback(settings.language.use_english_fallback);
                     session_state.hud.update_fonts(&global_state.i18n.read());
                 },
+                Language::ToggleEnglishFallback(toggle_fallback) => {
+                    settings.language.use_english_fallback = toggle_fallback;
+                    global_state
+                        .i18n
+                        .set_english_fallback(settings.language.use_english_fallback);
+                },
             },
             SettingsChange::Networking(networking_change) => match networking_change {},
         }
diff --git a/voxygen/src/settings/language.rs b/voxygen/src/settings/language.rs
index 48427f38a1..b8e3f8b20e 100644
--- a/voxygen/src/settings/language.rs
+++ b/voxygen/src/settings/language.rs
@@ -5,12 +5,14 @@ use serde::{Deserialize, Serialize};
 #[serde(default)]
 pub struct LanguageSettings {
     pub selected_language: String,
+    pub use_english_fallback: bool,
 }
 
 impl Default for LanguageSettings {
     fn default() -> Self {
         Self {
             selected_language: i18n::REFERENCE_LANG.to_string(),
+            use_english_fallback: true,
         }
     }
 }