Skip to content

Commit

Permalink
Add support for loading multiple languages at once
Browse files Browse the repository at this point in the history
  • Loading branch information
pomianowski committed Feb 17, 2022
1 parent b72f8b4 commit 166f3c0
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>1.0.0</Version>
<Version>1.1.0</Version>
<LangVersion>latest</LangVersion>
<Authors>lepo.co</Authors>
<Company>lepo.co</Company>
Expand Down
15 changes: 14 additions & 1 deletion Lepo.i18n.Demo/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<string, string>
{
{"en_US", langPath + "en_US.yaml"},
{"pl_PL", langPath + "pl_PL.yaml"},
{"de_DE", langPath + "de_DE.yaml"},
}
);

Translator.SetLanguage("en_US");
}
}
}
2 changes: 1 addition & 1 deletion Lepo.i18n.Demo/Lepo.i18n.Demo.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="WPF-UI" Version="1.2.2-prerelease133" />
<PackageReference Include="WPF-UI" Version="1.2.2-prerelease141" />
</ItemGroup>

<ItemGroup>
Expand Down
13 changes: 7 additions & 6 deletions Lepo.i18n.Demo/Views/Main.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Reflection;
using System.Windows;
using System.Windows;
using System.Windows.Controls;

namespace Lepo.i18n.Demo.Views
Expand All @@ -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;
}

Expand Down
3 changes: 2 additions & 1 deletion Lepo.i18n/AssemblyLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ internal class AssemblyLoader
/// <exception cref="ArgumentException"></exception>
public static IDictionary<uint, string> 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.");

Expand Down
2 changes: 1 addition & 1 deletion Lepo.i18n/TranslateExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private string FixSpecialCharacters(string markupedText)
return markupedText
.Trim()
.Replace("&apos;", "\'")
.Replace(" &quot;", "\"")
.Replace("&quot;", "\"")
.Replace("&lt;", "<")
.Replace("&gt;", ">")
.Replace("&amp;", "&");
Expand Down
96 changes: 91 additions & 5 deletions Lepo.i18n/Translator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -20,18 +22,47 @@ public static class Translator
public static string Current => TranslationStorage.CurrentLanguage ?? "";

/// <summary>
/// Defines the currently used language of the application. By itself, it does not update rendered views.
/// Loads all specified languages into global memory.
/// </summary>
/// <param name="applicationAssembly">Main application <see cref="Assembly"/>. You can use <see cref="Assembly.GetExecutingAssembly"/></param>
/// <param name="languagesCollection">A dictionary containing a key pair with the structure: <c>language_code</c> > <c>path_to_the_embedded_resource</c></param>
/// <param name="reload">If the file was previously loaded, it will be reloaded.</param>
public static bool LoadLanguages(Assembly applicationAssembly, IDictionary<string, string> languagesCollection, bool reload = false)
{
if (languagesCollection == null || !languagesCollection.Any())
return false;

foreach (KeyValuePair<string, string> singleLanguagePair in languagesCollection)
LoadLanguage(applicationAssembly, singleLanguagePair.Key, singleLanguagePair.Value, reload);

return true;
}

/// <summary>
/// Asynchronously loads all specified languages into global memory.
/// </summary>
/// <param name="applicationAssembly">Main application <see cref="Assembly"/>. You can use <see cref="Assembly.GetExecutingAssembly"/></param>
/// <param name="languagesCollection">A dictionary containing a key pair with the structure: <c>language_code</c> > <c>path_to_the_embedded_resource</c></param>
/// <param name="reload">If the file was previously loaded, it will be reloaded.</param>
public static async Task<bool> LoadLanguagesAsync(Assembly applicationAssembly, IDictionary<string, string> languagesCollection, bool reload = false)
{
return await Task.Run<bool>(() => LoadLanguages(applicationAssembly, languagesCollection, reload));
}

/// <summary>
/// Loads the specified language into memory and allows you to use it globally.
/// </summary>
/// <param name="applicationAssembly">Main application <see cref="Assembly"/>. You can use <see cref="Assembly.GetExecutingAssembly"/></param>
/// <param name="language">The language code to which you would like to assign the selected file. i.e: <i>pl_PL</i></param>
/// <param name="embeddedResourcePath">Path to the YAML file, i.e: <i>MyApp.Assets.Strings.pl_PL.yaml</i></param>
/// <param name="reload">If the file was previously loaded, it will be reloaded.</param>
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);

Expand All @@ -53,13 +84,68 @@ public static bool SetLanguage(Assembly applicationAssembly, string language, st
return true;
}

/// <summary>
/// Asynchronously loads the specified language into memory and allows you to use it globally.
/// </summary>
/// <param name="applicationAssembly">Main application <see cref="Assembly"/>. You can use <see cref="Assembly.GetExecutingAssembly"/></param>
/// <param name="language">The language code to which you would like to assign the selected file. i.e: <i>pl_PL</i></param>
/// <param name="embeddedResourcePath">Path to the YAML file, i.e: <i>MyApp.Assets.Strings.pl_PL.yaml</i></param>
/// <param name="reload">If the file was previously loaded, it will be reloaded.</param>
public static async Task<bool> LoadLanguageAsync(Assembly applicationAssembly, string language, string embeddedResourcePath, bool reload = false)
{
return await Task.Run<bool>(() => LoadLanguage(applicationAssembly, language, embeddedResourcePath, reload));
}

/// <summary>
/// Changes the currently used language, assuming that they have already been loaded using LoadLanguages.
/// </summary>
/// <param name="language">The language code to which you would like to assign the selected file. i.e: <i>pl_PL</i></param>
/// <returns><see langword="false"/> if the language could not be defined from the dictionary. <para>ATTENTION, the <see cref="TranslationStorage.CurrentLanguage"/> will be updated whether or not the language has been changed.</para></returns>
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;
}

/// <summary>
/// Asynchronously changes the currently used language, assuming that they have already been loaded using LoadLanguages.
/// </summary>
/// <param name="language">The language code to which you would like to assign the selected file. i.e: <i>pl_PL</i></param>
/// <returns><see langword="false"/> if the language could not be defined from the dictionary. <para>ATTENTION, the <see cref="TranslationStorage.CurrentLanguage"/> will be updated whether or not the language has been changed.</para></returns>
public static async Task<bool> SetLanguageAsync(string language)
{
return await Task.Run<bool>(() => SetLanguage(language));
}

/// <summary>
/// Defines the currently used language of the application. By itself, it does not update rendered views.
/// </summary>
/// <param name="applicationAssembly">Main application <see cref="Assembly"/>. You can use <see cref="Assembly.GetExecutingAssembly"/></param>
/// <param name="language">The language code to which you would like to assign the selected file. i.e: <i>pl_PL</i></param>
/// <param name="embeddedResourcePath">Path to the YAML file, i.e: <i>MyApp.Assets.Strings.pl_PL.yaml</i></param>
/// <param name="reload">If the file was previously loaded, it will be reloaded.</param>
public static bool SetLanguage(Assembly applicationAssembly, string language, string embeddedResourcePath, bool reload = false)
{
LoadLanguage(applicationAssembly, language, embeddedResourcePath, reload);

return SetLanguage(language);
}

/// <summary>
/// Asynchronously defines the currently used language of the application. By itself, it does not update rendered views.
/// </summary>
/// <param name="applicationAssembly">Main application <see cref="Assembly"/>. You can use <see cref="Assembly.GetExecutingAssembly"/></param>
/// <param name="language">The language code to which you would like to assign the selected file. i.e: <i>pl_PL</i></param>
/// <param name="embeddedResourcePath">Path to the YAML file, i.e: <i>MyApp.Assets.Strings.pl_PL.yaml</i></param>
/// <param name="reload">If the file was previously loaded, it will be reloaded.</param>
public static async Task<bool> SetLanguageAsync(Assembly applicationAssembly, string language, string embeddedResourcePath, bool reload = false)
{
return await Task.Run<bool>(() => SetLanguage(applicationAssembly, language, embeddedResourcePath, reload));
Expand Down
29 changes: 19 additions & 10 deletions Lepo.i18n/Yaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,46 @@ namespace Lepo.i18n
/// </summary>
internal class Yaml
{
/// <summary>
/// Used to calculate a simple hash of a key in a dictionary to make searches faster.
/// </summary>
private static readonly MD5 Hasher = MD5.Create();

/// <summary>
/// Creates a hashed <see langword="int"/> representation of <see langword="string"/>.
/// </summary>
/// <param name="value">Value to be hashed.</param>
/// <returns></returns>
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
);
}

/// <summary>
/// Creates new collection of mapped keys with translated values.
/// </summary>
/// <param name="yamlContent">String containing Yaml.</param>
public static IDictionary<uint, string> FromString(string yamlContent)
/// <param name="rawYamlContent">String containing Yaml.</param>
public static IDictionary<uint, string> FromString(string rawYamlContent)
{
Dictionary<uint, string> 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;
Expand Down

0 comments on commit 166f3c0

Please sign in to comment.