Skip to content

Commit 166f3c0

Browse files
committed
Add support for loading multiple languages at once
1 parent b72f8b4 commit 166f3c0

File tree

9 files changed

+137
-27
lines changed

9 files changed

+137
-27
lines changed

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ jobs:
7373
- name: Upload update to NuGet
7474
uses: rohith/publish-nuget@v2
7575
with:
76-
PROJECT_FILE_PATH: WPFUI/WPFUI.csproj
76+
PROJECT_FILE_PATH: Lepo.i18n/Lepo.i18n.csproj
7777
VERSION_FILE_PATH: Directory.Build.props
7878
NUGET_KEY: ${{secrets.NUGET_API_KEY}}
7979
INCLUDE_SYMBOLS: false

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project>
22
<PropertyGroup>
3-
<Version>1.0.0</Version>
3+
<Version>1.1.0</Version>
44
<LangVersion>latest</LangVersion>
55
<Authors>lepo.co</Authors>
66
<Company>lepo.co</Company>

Lepo.i18n.Demo/App.xaml.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Copyright (C) Leszek Pomianowski and Lepo.i18n Contributors.
44
// All Rights Reserved.
55

6+
using System.Collections.Generic;
67
using System.Reflection;
78
using System.Windows;
89

@@ -17,7 +18,19 @@ protected override void OnStartup(StartupEventArgs e)
1718
{
1819
WPFUI.Theme.Watcher.Start(true, true);
1920

20-
Translator.SetLanguage(Assembly.GetExecutingAssembly(), "en_US", "Lepo.i18n.Demo.Strings.en_US.yaml");
21+
var langPath = "Lepo.i18n.Demo.Strings.";
22+
23+
Translator.LoadLanguages(
24+
Assembly.GetExecutingAssembly(),
25+
new Dictionary<string, string>
26+
{
27+
{"en_US", langPath + "en_US.yaml"},
28+
{"pl_PL", langPath + "pl_PL.yaml"},
29+
{"de_DE", langPath + "de_DE.yaml"},
30+
}
31+
);
32+
33+
Translator.SetLanguage("en_US");
2134
}
2235
}
2336
}

Lepo.i18n.Demo/Lepo.i18n.Demo.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
</ItemGroup>
2020

2121
<ItemGroup>
22-
<PackageReference Include="WPF-UI" Version="1.2.2-prerelease133" />
22+
<PackageReference Include="WPF-UI" Version="1.2.2-prerelease141" />
2323
</ItemGroup>
2424

2525
<ItemGroup>

Lepo.i18n.Demo/Views/Main.xaml.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System.Reflection;
2-
using System.Windows;
1+
using System.Windows;
32
using System.Windows.Controls;
43

54
namespace Lepo.i18n.Demo.Views
@@ -16,20 +15,22 @@ public Main()
1615
InitializeComponent();
1716
}
1817

19-
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
18+
private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
2019
{
20+
// The languages were loaded in the App class OnStartup method.
21+
2122
switch (Translator.Current)
2223
{
2324
case "pl_PL":
24-
Translator.SetLanguage(Assembly.GetExecutingAssembly(), "de_DE", "Lepo.i18n.Demo.Strings.de_DE.yaml");
25+
await Translator.SetLanguageAsync("de_DE");
2526
break;
2627

2728
case "de_DE":
28-
Translator.SetLanguage(Assembly.GetExecutingAssembly(), "en_US", "Lepo.i18n.Demo.Strings.en_US.yaml");
29+
await Translator.SetLanguageAsync("en_US");
2930
break;
3031

3132
default:
32-
Translator.SetLanguage(Assembly.GetExecutingAssembly(), "pl_PL", "Lepo.i18n.Demo.Strings.pl_PL.yaml");
33+
await Translator.SetLanguageAsync("pl_PL");
3334
break;
3435
}
3536

Lepo.i18n/AssemblyLoader.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ internal class AssemblyLoader
2323
/// <exception cref="ArgumentException"></exception>
2424
public static IDictionary<uint, string> TryLoad(Assembly applicationAssembly, string resourceStreamPath)
2525
{
26-
if (!resourceStreamPath.EndsWith(".yaml"))
26+
var lowerResourcePath = resourceStreamPath.ToLower().Trim();
27+
if (!(lowerResourcePath.EndsWith(".yml") || lowerResourcePath.EndsWith(".yaml")))
2728
throw new ArgumentException(
2829
$"Parameter {nameof(resourceStreamPath)} in {nameof(TryLoad)} must be path to the YAML file.");
2930

Lepo.i18n/TranslateExtension.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ private string FixSpecialCharacters(string markupedText)
3535
return markupedText
3636
.Trim()
3737
.Replace("&apos;", "\'")
38-
.Replace(" &quot;", "\"")
38+
.Replace("&quot;", "\"")
3939
.Replace("&lt;", "<")
4040
.Replace("&gt;", ">")
4141
.Replace("&amp;", "&");

Lepo.i18n/Translator.cs

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
// Copyright (C) Leszek Pomianowski and Lepo.i18n Contributors.
44
// All Rights Reserved.
55

6+
using System;
67
using System.Collections.Generic;
8+
using System.Linq;
79
using System.Reflection;
810
using System.Threading.Tasks;
911

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

2224
/// <summary>
23-
/// Defines the currently used language of the application. By itself, it does not update rendered views.
25+
/// Loads all specified languages into global memory.
26+
/// </summary>
27+
/// <param name="applicationAssembly">Main application <see cref="Assembly"/>. You can use <see cref="Assembly.GetExecutingAssembly"/></param>
28+
/// <param name="languagesCollection">A dictionary containing a key pair with the structure: <c>language_code</c> > <c>path_to_the_embedded_resource</c></param>
29+
/// <param name="reload">If the file was previously loaded, it will be reloaded.</param>
30+
public static bool LoadLanguages(Assembly applicationAssembly, IDictionary<string, string> languagesCollection, bool reload = false)
31+
{
32+
if (languagesCollection == null || !languagesCollection.Any())
33+
return false;
34+
35+
foreach (KeyValuePair<string, string> singleLanguagePair in languagesCollection)
36+
LoadLanguage(applicationAssembly, singleLanguagePair.Key, singleLanguagePair.Value, reload);
37+
38+
return true;
39+
}
40+
41+
/// <summary>
42+
/// Asynchronously loads all specified languages into global memory.
43+
/// </summary>
44+
/// <param name="applicationAssembly">Main application <see cref="Assembly"/>. You can use <see cref="Assembly.GetExecutingAssembly"/></param>
45+
/// <param name="languagesCollection">A dictionary containing a key pair with the structure: <c>language_code</c> > <c>path_to_the_embedded_resource</c></param>
46+
/// <param name="reload">If the file was previously loaded, it will be reloaded.</param>
47+
public static async Task<bool> LoadLanguagesAsync(Assembly applicationAssembly, IDictionary<string, string> languagesCollection, bool reload = false)
48+
{
49+
return await Task.Run<bool>(() => LoadLanguages(applicationAssembly, languagesCollection, reload));
50+
}
51+
52+
/// <summary>
53+
/// Loads the specified language into memory and allows you to use it globally.
2454
/// </summary>
2555
/// <param name="applicationAssembly">Main application <see cref="Assembly"/>. You can use <see cref="Assembly.GetExecutingAssembly"/></param>
2656
/// <param name="language">The language code to which you would like to assign the selected file. i.e: <i>pl_PL</i></param>
2757
/// <param name="embeddedResourcePath">Path to the YAML file, i.e: <i>MyApp.Assets.Strings.pl_PL.yaml</i></param>
2858
/// <param name="reload">If the file was previously loaded, it will be reloaded.</param>
29-
public static bool SetLanguage(Assembly applicationAssembly, string language, string embeddedResourcePath, bool reload = false)
59+
public static bool LoadLanguage(Assembly applicationAssembly, string language, string embeddedResourcePath, bool reload = false)
3060
{
31-
language = language.Trim();
61+
if (System.String.IsNullOrEmpty(language))
62+
throw new ArgumentNullException(nameof(language), "The name of the language must be specified.");
3263

33-
// We update the language even if we do not change it, because the user may not know what is not working.
34-
TranslationStorage.CurrentLanguage = language;
64+
if (System.String.IsNullOrEmpty(embeddedResourcePath))
65+
throw new ArgumentNullException(nameof(embeddedResourcePath), "The path to the location of the embedded resource in the application must be provided.");
3566

3667
var languageDictionary = AssemblyLoader.TryLoad(applicationAssembly, embeddedResourcePath);
3768

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

87+
/// <summary>
88+
/// Asynchronously loads the specified language into memory and allows you to use it globally.
89+
/// </summary>
90+
/// <param name="applicationAssembly">Main application <see cref="Assembly"/>. You can use <see cref="Assembly.GetExecutingAssembly"/></param>
91+
/// <param name="language">The language code to which you would like to assign the selected file. i.e: <i>pl_PL</i></param>
92+
/// <param name="embeddedResourcePath">Path to the YAML file, i.e: <i>MyApp.Assets.Strings.pl_PL.yaml</i></param>
93+
/// <param name="reload">If the file was previously loaded, it will be reloaded.</param>
94+
public static async Task<bool> LoadLanguageAsync(Assembly applicationAssembly, string language, string embeddedResourcePath, bool reload = false)
95+
{
96+
return await Task.Run<bool>(() => LoadLanguage(applicationAssembly, language, embeddedResourcePath, reload));
97+
}
98+
99+
/// <summary>
100+
/// Changes the currently used language, assuming that they have already been loaded using LoadLanguages.
101+
/// </summary>
102+
/// <param name="language">The language code to which you would like to assign the selected file. i.e: <i>pl_PL</i></param>
103+
/// <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>
104+
public static bool SetLanguage(string language)
105+
{
106+
// We update the language even if we do not change it, because the user may not know what is not working.
107+
TranslationStorage.CurrentLanguage = language;
108+
109+
if (TranslationStorage.TranslationsDictionary == null)
110+
return false;
111+
112+
if (!TranslationStorage.TranslationsDictionary.ContainsKey(language))
113+
return false;
114+
115+
return true;
116+
}
117+
118+
/// <summary>
119+
/// Asynchronously changes the currently used language, assuming that they have already been loaded using LoadLanguages.
120+
/// </summary>
121+
/// <param name="language">The language code to which you would like to assign the selected file. i.e: <i>pl_PL</i></param>
122+
/// <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>
123+
public static async Task<bool> SetLanguageAsync(string language)
124+
{
125+
return await Task.Run<bool>(() => SetLanguage(language));
126+
}
127+
56128
/// <summary>
57129
/// Defines the currently used language of the application. By itself, it does not update rendered views.
58130
/// </summary>
59131
/// <param name="applicationAssembly">Main application <see cref="Assembly"/>. You can use <see cref="Assembly.GetExecutingAssembly"/></param>
60132
/// <param name="language">The language code to which you would like to assign the selected file. i.e: <i>pl_PL</i></param>
61133
/// <param name="embeddedResourcePath">Path to the YAML file, i.e: <i>MyApp.Assets.Strings.pl_PL.yaml</i></param>
62134
/// <param name="reload">If the file was previously loaded, it will be reloaded.</param>
135+
public static bool SetLanguage(Assembly applicationAssembly, string language, string embeddedResourcePath, bool reload = false)
136+
{
137+
LoadLanguage(applicationAssembly, language, embeddedResourcePath, reload);
138+
139+
return SetLanguage(language);
140+
}
141+
142+
/// <summary>
143+
/// Asynchronously defines the currently used language of the application. By itself, it does not update rendered views.
144+
/// </summary>
145+
/// <param name="applicationAssembly">Main application <see cref="Assembly"/>. You can use <see cref="Assembly.GetExecutingAssembly"/></param>
146+
/// <param name="language">The language code to which you would like to assign the selected file. i.e: <i>pl_PL</i></param>
147+
/// <param name="embeddedResourcePath">Path to the YAML file, i.e: <i>MyApp.Assets.Strings.pl_PL.yaml</i></param>
148+
/// <param name="reload">If the file was previously loaded, it will be reloaded.</param>
63149
public static async Task<bool> SetLanguageAsync(Assembly applicationAssembly, string language, string embeddedResourcePath, bool reload = false)
64150
{
65151
return await Task.Run<bool>(() => SetLanguage(applicationAssembly, language, embeddedResourcePath, reload));

Lepo.i18n/Yaml.cs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,37 +15,46 @@ namespace Lepo.i18n
1515
/// </summary>
1616
internal class Yaml
1717
{
18+
/// <summary>
19+
/// Used to calculate a simple hash of a key in a dictionary to make searches faster.
20+
/// </summary>
21+
private static readonly MD5 Hasher = MD5.Create();
22+
1823
/// <summary>
1924
/// Creates a hashed <see langword="int"/> representation of <see langword="string"/>.
2025
/// </summary>
2126
/// <param name="value">Value to be hashed.</param>
2227
/// <returns></returns>
2328
public static uint Map(string value)
2429
{
25-
MD5 md5Hasher = MD5.Create();
26-
27-
byte[] hashed = md5Hasher.ComputeHash(Encoding.UTF8.GetBytes(value));
28-
29-
return BitConverter.ToUInt32(hashed, 0);
30+
return BitConverter.ToUInt32(
31+
Hasher.ComputeHash(Encoding.UTF8.GetBytes(value)),
32+
0
33+
);
3034
}
3135

3236
/// <summary>
3337
/// Creates new collection of mapped keys with translated values.
3438
/// </summary>
35-
/// <param name="yamlContent">String containing Yaml.</param>
36-
public static IDictionary<uint, string> FromString(string yamlContent)
39+
/// <param name="rawYamlContent">String containing Yaml.</param>
40+
public static IDictionary<uint, string> FromString(string rawYamlContent)
3741
{
3842
Dictionary<uint, string> keyValueCollection = new() { };
3943

40-
string[] yamlLines = yamlContent.Split(
44+
if (String.IsNullOrEmpty(rawYamlContent))
45+
return keyValueCollection;
46+
47+
string[] splittedYamlLines = rawYamlContent.Split(
4148
new[] { "\r\n", "\r", "\n" },
4249
StringSplitOptions.None
4350
);
4451

45-
if (yamlLines.Length < 1)
52+
// TODO: Recognize tab stops as subsections
53+
54+
if (splittedYamlLines.Length < 1)
4655
return keyValueCollection;
4756

48-
foreach (string yamlLine in yamlLines)
57+
foreach (string yamlLine in splittedYamlLines)
4958
{
5059
if (yamlLine.StartsWith("#") || String.IsNullOrEmpty(yamlLine))
5160
continue;

0 commit comments

Comments
 (0)