So, I wanted to localize my WPF application. I thought this should be pretty standard stuff, but then I read about resx and resource dictionaries and gave up. Microsoft, why do you make it so hard to create something simple?
Coming up with a new system
Alright, so first: What do I want?
I want a simple class that can be accessed via my ViewModels that gives me a key-value-storage for retrieving localized strings.
So what I need are three things:
- A storage for the languages in non-volatile memory – in other words: Some files with data
- A simple class system that stores and manages languages
- A way to access this information in XAML
The file system
Because I already use Newtonsoft.Json in the project, I decided to use it in the localizer aswell. It makes the software feel more persistant to the user and other developers. If you do not want to use Json, you can use anything else, but I stuck with Json.
My structure looks like this and it is already very easy to see where I am going:
{
"name": "English",
"localized": "English",
"abbreviation": "en",
"data": {
"menu.file": "File",
"menu.file.new": "New",
"menu.file.open": "Open",
"menu.file.save": "Save",
"menu.file.saveAll": "Save All",
"menu.file.saveAs": "Save As",
"menu.file.close": "Close",
"menu.file.closeAll": "Close All",
"menu.file.preferences": "Preferences",
"menu.file.quit": "Quit"
}
}
We have some information about the language and then the key-value pairs that make up the language. I structured my keys in a way that makes it easy to determine where the string is used, because just translating something like “close” may be hard to translate correctly in another language.
The class system
My system uses two classes:
- A manager class called “Strings”
- A storage class called “Language”
Let’s start with the storage class. It is pretty straightforward: We store every Json property in their C# equivalents. For debugging reasons I also added the Filename, but it is not necessary and stays unused.
Also note that I use a factory pattern to create new instances of Language. It allows me to create an instance and load the data at the same time.
public class Language
{
[JsonProperty ("abbreviation")]
public string Abbreviation { get; set; }
[JsonProperty ("data")]
public Dictionary<string, string> Data { get; set; }
public string Filename { get; set; }
[JsonProperty ("localized")]
public string LocalizedName { get; set; }
[JsonProperty ("name")]
public string Name { get; set; }
public static Language Load (string file)
{
Language language = JsonConvert.DeserializeObject<Language> (File.ReadAllText (file)) ?? new Language ();
language.Filename = Path.GetFullPath (file);
return language;
}
}
Next, we have the storage class. It is a Singleton, because it needs to be accessible from everywhere and I prefer working with instances that I can pass around, instead of having a static class that can’t be accessed the way I want in XAML.
As you can see I store a Dictionary of Languages by the name of the language and the language itself. I do not store the current language in this project in this class, because I thought it would be better to have it stored in the Settings class that loads and saves the settings.
The Data-Dictionary property is just wrapping around the current language so other classes don’t need to do that.
The Load method loads the default language first (the one that gets compiled with the application) and afterwards searches for other language files that follow the pattern string.*.json (e.g. string.de.json = German localization file). Finally, it makes sure the current language is set to a valid language.
public sealed class Strings
{
public const string DEFAULT_FILE = "strings.json";
public const string FOLDER = "Localization";
private static Strings instance;
private Strings ()
{
Languages = new Dictionary<string, Language> ();
}
public static Strings Instance
{
get
{
if (instance == null)
instance = new Strings ();
return instance;
}
}
public string CurrentLanguage
{
get { return Settings.Storage.Language; }
set
{
if (Languages.ContainsKey (value))
Settings.Storage.Language = value;
}
}
public Dictionary<string, string> Data => Languages[Settings.Storage.Language].Data;
public Dictionary<string, Language> Languages { get; private set; }
public void Load ()
{
Language defaultLanguage = Language.Load (Path.Combine (FOLDER, DEFAULT_FILE).SanitizePath ());
Languages.Add (defaultLanguage.Name, defaultLanguage);
string[] files = Directory.GetFiles (FOLDER, "strings.*.json", SearchOption.AllDirectories);
foreach (string file in files)
{
Language language = Language.Load (Path.Combine (file).SanitizePath ());
Languages.Add (language.Name, language);
}
if (!Languages.ContainsKey (Settings.Storage.Language))
Settings.Storage.Language = defaultLanguage.Name;
}
}
Accessing the information in XAML
This one was tricky at first but after some testing, I think I found the best solution.
I have a class called ViewModelBase from which all my viewmodels inherit from. I added a simple line to my ViewModelBase which just wraps the Data member from the Strings class (again).
public Dictionary<string, string> LocalizedStrings => Strings.Instance.Data;
Since all my viewmodels inherit from ViewModelBase, all of them also have the same access to the localization. I can now write the following XAML and it always gets the correct strings:
<MenuItem Header="{Binding LocalizedStrings[menu.file]}">
<MenuItem Header="{Binding LocalizedStrings[menu.file.new]}" />
<MenuItem Header="{Binding LocalizedStrings[menu.file.open]}" />
<Separator />
<MenuItem Header="{Binding LocalizedStrings[menu.file.close]}" />
<MenuItem Header="{Binding LocalizedStrings[menu.file.closeAll]}" />
<Separator />
<MenuItem Header="{Binding LocalizedStrings[menu.file.save]}" />
<MenuItem Header="{Binding LocalizedStrings[menu.file.saveAll]}" />
<MenuItem Header="{Binding LocalizedStrings[menu.file.saveAs]}" />
<Separator />
<MenuItem Header="{Binding LocalizedStrings[menu.file.preferences]}" />
<Separator />
<MenuItem Header="{Binding LocalizedStrings[menu.file.quit]}" />
</MenuItem>
This is the result:
When the tools is ready, I may publish it under an open source license and everyone can use and extend it. But for now, this is all I have!
Cheers!