From 166f3c05e302d22d8b3650bb065e9496eee0e691 Mon Sep 17 00:00:00 2001 From: Pomianowski Date: Thu, 17 Feb 2022 15:09:11 +0100 Subject: [PATCH] Add support for loading multiple languages at once --- .github/workflows/CI.yml | 2 +- Directory.Build.props | 2 +- Lepo.i18n.Demo/App.xaml.cs | 15 ++++- Lepo.i18n.Demo/Lepo.i18n.Demo.csproj | 2 +- Lepo.i18n.Demo/Views/Main.xaml.cs | 13 ++-- Lepo.i18n/AssemblyLoader.cs | 3 +- Lepo.i18n/TranslateExtension.cs | 2 +- Lepo.i18n/Translator.cs | 96 ++++++++++++++++++++++++++-- Lepo.i18n/Yaml.cs | 29 ++++++--- 9 files changed, 137 insertions(+), 27 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 15c5e26..f7e8cae 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -73,7 +73,7 @@ jobs: - name: Upload update to NuGet uses: rohith/publish-nuget@v2 with: - PROJECT_FILE_PATH: WPFUI/WPFUI.csproj + PROJECT_FILE_PATH: Lepo.i18n/Lepo.i18n.csproj VERSION_FILE_PATH: Directory.Build.props NUGET_KEY: ${{secrets.NUGET_API_KEY}} INCLUDE_SYMBOLS: false diff --git a/Directory.Build.props b/Directory.Build.props index 69b5452..c50e62d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 1.0.0 + 1.1.0 latest lepo.co lepo.co diff --git a/Lepo.i18n.Demo/App.xaml.cs b/Lepo.i18n.Demo/App.xaml.cs index 466ce54..aae0340 100644 --- a/Lepo.i18n.Demo/App.xaml.cs +++ b/Lepo.i18n.Demo/App.xaml.cs @@ -3,6 +3,7 @@ // Copyright (C) Leszek Pomianowski and Lepo.i18n Contributors. // All Rights Reserved. +using System.Collections.Generic; using System.Reflection; using System.Windows; @@ -17,7 +18,19 @@ protected override void OnStartup(StartupEventArgs e) { WPFUI.Theme.Watcher.Start(true, true); - Translator.SetLanguage(Assembly.GetExecutingAssembly(), "en_US", "Lepo.i18n.Demo.Strings.en_US.yaml"); + var langPath = "Lepo.i18n.Demo.Strings."; + + Translator.LoadLanguages( + Assembly.GetExecutingAssembly(), + new Dictionary + { + {"en_US", langPath + "en_US.yaml"}, + {"pl_PL", langPath + "pl_PL.yaml"}, + {"de_DE", langPath + "de_DE.yaml"}, + } + ); + + Translator.SetLanguage("en_US"); } } } diff --git a/Lepo.i18n.Demo/Lepo.i18n.Demo.csproj b/Lepo.i18n.Demo/Lepo.i18n.Demo.csproj index 6c6aecf..ea84307 100644 --- a/Lepo.i18n.Demo/Lepo.i18n.Demo.csproj +++ b/Lepo.i18n.Demo/Lepo.i18n.Demo.csproj @@ -19,7 +19,7 @@ - + diff --git a/Lepo.i18n.Demo/Views/Main.xaml.cs b/Lepo.i18n.Demo/Views/Main.xaml.cs index cadde18..c6e9652 100644 --- a/Lepo.i18n.Demo/Views/Main.xaml.cs +++ b/Lepo.i18n.Demo/Views/Main.xaml.cs @@ -1,5 +1,4 @@ -using System.Reflection; -using System.Windows; +using System.Windows; using System.Windows.Controls; namespace Lepo.i18n.Demo.Views @@ -16,20 +15,22 @@ public Main() InitializeComponent(); } - private void ButtonBase_OnClick(object sender, RoutedEventArgs e) + private async void ButtonBase_OnClick(object sender, RoutedEventArgs e) { + // The languages were loaded in the App class OnStartup method. + switch (Translator.Current) { case "pl_PL": - Translator.SetLanguage(Assembly.GetExecutingAssembly(), "de_DE", "Lepo.i18n.Demo.Strings.de_DE.yaml"); + await Translator.SetLanguageAsync("de_DE"); break; case "de_DE": - Translator.SetLanguage(Assembly.GetExecutingAssembly(), "en_US", "Lepo.i18n.Demo.Strings.en_US.yaml"); + await Translator.SetLanguageAsync("en_US"); break; default: - Translator.SetLanguage(Assembly.GetExecutingAssembly(), "pl_PL", "Lepo.i18n.Demo.Strings.pl_PL.yaml"); + await Translator.SetLanguageAsync("pl_PL"); break; } diff --git a/Lepo.i18n/AssemblyLoader.cs b/Lepo.i18n/AssemblyLoader.cs index ece57e6..cf18c09 100644 --- a/Lepo.i18n/AssemblyLoader.cs +++ b/Lepo.i18n/AssemblyLoader.cs @@ -23,7 +23,8 @@ internal class AssemblyLoader /// public static IDictionary TryLoad(Assembly applicationAssembly, string resourceStreamPath) { - if (!resourceStreamPath.EndsWith(".yaml")) + var lowerResourcePath = resourceStreamPath.ToLower().Trim(); + if (!(lowerResourcePath.EndsWith(".yml") || lowerResourcePath.EndsWith(".yaml"))) throw new ArgumentException( $"Parameter {nameof(resourceStreamPath)} in {nameof(TryLoad)} must be path to the YAML file."); diff --git a/Lepo.i18n/TranslateExtension.cs b/Lepo.i18n/TranslateExtension.cs index 00e22b6..fa6e518 100644 --- a/Lepo.i18n/TranslateExtension.cs +++ b/Lepo.i18n/TranslateExtension.cs @@ -35,7 +35,7 @@ private string FixSpecialCharacters(string markupedText) return markupedText .Trim() .Replace("'", "\'") - .Replace(" "", "\"") + .Replace(""", "\"") .Replace("<", "<") .Replace(">", ">") .Replace("&", "&"); diff --git a/Lepo.i18n/Translator.cs b/Lepo.i18n/Translator.cs index 365878d..e60792f 100644 --- a/Lepo.i18n/Translator.cs +++ b/Lepo.i18n/Translator.cs @@ -3,7 +3,9 @@ // Copyright (C) Leszek Pomianowski and Lepo.i18n Contributors. // All Rights Reserved. +using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -20,18 +22,47 @@ public static class Translator public static string Current => TranslationStorage.CurrentLanguage ?? ""; /// - /// Defines the currently used language of the application. By itself, it does not update rendered views. + /// Loads all specified languages into global memory. + /// + /// Main application . You can use + /// A dictionary containing a key pair with the structure: language_code > path_to_the_embedded_resource + /// If the file was previously loaded, it will be reloaded. + public static bool LoadLanguages(Assembly applicationAssembly, IDictionary languagesCollection, bool reload = false) + { + if (languagesCollection == null || !languagesCollection.Any()) + return false; + + foreach (KeyValuePair singleLanguagePair in languagesCollection) + LoadLanguage(applicationAssembly, singleLanguagePair.Key, singleLanguagePair.Value, reload); + + return true; + } + + /// + /// Asynchronously loads all specified languages into global memory. + /// + /// Main application . You can use + /// A dictionary containing a key pair with the structure: language_code > path_to_the_embedded_resource + /// If the file was previously loaded, it will be reloaded. + public static async Task LoadLanguagesAsync(Assembly applicationAssembly, IDictionary languagesCollection, bool reload = false) + { + return await Task.Run(() => LoadLanguages(applicationAssembly, languagesCollection, reload)); + } + + /// + /// Loads the specified language into memory and allows you to use it globally. /// /// Main application . You can use /// The language code to which you would like to assign the selected file. i.e: pl_PL /// Path to the YAML file, i.e: MyApp.Assets.Strings.pl_PL.yaml /// If the file was previously loaded, it will be reloaded. - public static bool SetLanguage(Assembly applicationAssembly, string language, string embeddedResourcePath, bool reload = false) + public static bool LoadLanguage(Assembly applicationAssembly, string language, string embeddedResourcePath, bool reload = false) { - language = language.Trim(); + if (System.String.IsNullOrEmpty(language)) + throw new ArgumentNullException(nameof(language), "The name of the language must be specified."); - // We update the language even if we do not change it, because the user may not know what is not working. - TranslationStorage.CurrentLanguage = language; + if (System.String.IsNullOrEmpty(embeddedResourcePath)) + throw new ArgumentNullException(nameof(embeddedResourcePath), "The path to the location of the embedded resource in the application must be provided."); var languageDictionary = AssemblyLoader.TryLoad(applicationAssembly, embeddedResourcePath); @@ -53,6 +84,47 @@ public static bool SetLanguage(Assembly applicationAssembly, string language, st return true; } + /// + /// Asynchronously loads the specified language into memory and allows you to use it globally. + /// + /// Main application . You can use + /// The language code to which you would like to assign the selected file. i.e: pl_PL + /// Path to the YAML file, i.e: MyApp.Assets.Strings.pl_PL.yaml + /// If the file was previously loaded, it will be reloaded. + public static async Task LoadLanguageAsync(Assembly applicationAssembly, string language, string embeddedResourcePath, bool reload = false) + { + return await Task.Run(() => LoadLanguage(applicationAssembly, language, embeddedResourcePath, reload)); + } + + /// + /// Changes the currently used language, assuming that they have already been loaded using LoadLanguages. + /// + /// The language code to which you would like to assign the selected file. i.e: pl_PL + /// if the language could not be defined from the dictionary. ATTENTION, the will be updated whether or not the language has been changed. + public static bool SetLanguage(string language) + { + // We update the language even if we do not change it, because the user may not know what is not working. + TranslationStorage.CurrentLanguage = language; + + if (TranslationStorage.TranslationsDictionary == null) + return false; + + if (!TranslationStorage.TranslationsDictionary.ContainsKey(language)) + return false; + + return true; + } + + /// + /// Asynchronously changes the currently used language, assuming that they have already been loaded using LoadLanguages. + /// + /// The language code to which you would like to assign the selected file. i.e: pl_PL + /// if the language could not be defined from the dictionary. ATTENTION, the will be updated whether or not the language has been changed. + public static async Task SetLanguageAsync(string language) + { + return await Task.Run(() => SetLanguage(language)); + } + /// /// Defines the currently used language of the application. By itself, it does not update rendered views. /// @@ -60,6 +132,20 @@ public static bool SetLanguage(Assembly applicationAssembly, string language, st /// The language code to which you would like to assign the selected file. i.e: pl_PL /// Path to the YAML file, i.e: MyApp.Assets.Strings.pl_PL.yaml /// If the file was previously loaded, it will be reloaded. + public static bool SetLanguage(Assembly applicationAssembly, string language, string embeddedResourcePath, bool reload = false) + { + LoadLanguage(applicationAssembly, language, embeddedResourcePath, reload); + + return SetLanguage(language); + } + + /// + /// Asynchronously defines the currently used language of the application. By itself, it does not update rendered views. + /// + /// Main application . You can use + /// The language code to which you would like to assign the selected file. i.e: pl_PL + /// Path to the YAML file, i.e: MyApp.Assets.Strings.pl_PL.yaml + /// If the file was previously loaded, it will be reloaded. public static async Task SetLanguageAsync(Assembly applicationAssembly, string language, string embeddedResourcePath, bool reload = false) { return await Task.Run(() => SetLanguage(applicationAssembly, language, embeddedResourcePath, reload)); diff --git a/Lepo.i18n/Yaml.cs b/Lepo.i18n/Yaml.cs index 376792a..72eeadf 100644 --- a/Lepo.i18n/Yaml.cs +++ b/Lepo.i18n/Yaml.cs @@ -15,6 +15,11 @@ namespace Lepo.i18n /// internal class Yaml { + /// + /// Used to calculate a simple hash of a key in a dictionary to make searches faster. + /// + private static readonly MD5 Hasher = MD5.Create(); + /// /// Creates a hashed representation of . /// @@ -22,30 +27,34 @@ internal class Yaml /// public static uint Map(string value) { - MD5 md5Hasher = MD5.Create(); - - byte[] hashed = md5Hasher.ComputeHash(Encoding.UTF8.GetBytes(value)); - - return BitConverter.ToUInt32(hashed, 0); + return BitConverter.ToUInt32( + Hasher.ComputeHash(Encoding.UTF8.GetBytes(value)), + 0 + ); } /// /// Creates new collection of mapped keys with translated values. /// - /// String containing Yaml. - public static IDictionary FromString(string yamlContent) + /// String containing Yaml. + public static IDictionary FromString(string rawYamlContent) { Dictionary keyValueCollection = new() { }; - string[] yamlLines = yamlContent.Split( + if (String.IsNullOrEmpty(rawYamlContent)) + return keyValueCollection; + + string[] splittedYamlLines = rawYamlContent.Split( new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None ); - if (yamlLines.Length < 1) + // TODO: Recognize tab stops as subsections + + if (splittedYamlLines.Length < 1) return keyValueCollection; - foreach (string yamlLine in yamlLines) + foreach (string yamlLine in splittedYamlLines) { if (yamlLine.StartsWith("#") || String.IsNullOrEmpty(yamlLine)) continue;