Introduction
I was looking for a way to localize my application in a way that would be
simple to handle, both for the developer and for the translator, and since I
couldn't find any, I found this very handy solution that I would like to share with you.
My goals were:
- Application localisation.
- Dynamic application language change (at run time).
- Simple to add/modify a language for the developer or for the user (no compilation or such).
- Design time support in Visual Studio / Blend in any language.
And those goals are met...
..but... an important remark: this library, so far, does not work with ClickOnce applications. Look at the "To ClickOnce Users : the folder issue" part of this article for details. I am open to comments to improve this.
In this article I explain how the library works, and how to use it. I also present a tool that allows to handle
easily the localised strings for the developer or the user.
I released a demo in both C# and VB, so no WPF lover is left behind.
Background
My solution uses Resource Dictionaries, which are, as expected, dictionaries: they associate a Key to an Object (for localisation purposes: a String). In WPF,
those
resource dictionaries can be used within several scopes: Application, Window, Control, and even at
FrameWorkElement
level, so almost anything can have its own resources.
A feature of the WPF ResourceDictionary
is that it can contain other ResourceDictionary
s, which allows to split the dictionary into multiple smaller
ones. Those inner dictionaries are called MergedDictionary
s.
The same key can be used in several merged RDs. In this case, the value returned for this key is the latest added.
In case the ResourceDictionary
is changed, a notification is issued to all
XAML controls using this key as a DynamicResource (controls using StaticResource
remains unaffected).
So the idea is simple:
- Store all application strings for each language within a
ResourceDictionary
.
- Keep a fallback language RD in the application merged dictionary.
- Only use dynamic reference to a resource string within the application.
- On language change requested, we add or remove a RD, ensuring that: The
fallback RD is always loaded. - The latest RD is the language requested.
So on language change all the strings will be updated by the standard WPF resource management. (Rq: Step 2 is done to avoid any bad surprise in case you forget to add a key in a foreign RD. In this case, the fallback language string will be shown.)
Using the library
First step: Create the Resource Dictionaries
Create a resource dictionary (a standard WPF item) for each language, using the same
keys.
Rq : It is wise to use a suffix to prevent resource names from colliding:
I use 'Str'
as suffix.
For example, for English, with three strings having HelloStr
,
DemoModeStr
, and RealModeStr
as keys, the RD looks like :
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<s:String x:Key="HelloStr" > Hello </s:String>
<s:String x:Key="DemoModeStr" > Currently in demo mode </s:String>
<s:String x:Key="RealModeStr" > Currently in real mode </s:String>
</ResourceDictionnary>
Same dictionary for French (only localised text changes, use copy paste to create other versions):
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<s:String x:Key="HelloStr" > Bonjour </s:String>
<s:String x:Key="DemoModeStr" > Actuellement en mode démo </s:String>
<s:String x:Key="RealModeStr" > Actuellement en mode réel </s:String>
</ResourceDictionnary>
Now you must save those RDs in separate files with a naming convention, by
using "_" and the two letter ISO of the language as a suffix.
I used "InterfaceStrings" as a prefix (you can choose yours). So for instance, for
English, French, and Spanish we will have the files: InterfaceStrings_en.xaml,
InterfaceStrings_fr.xaml, InterfaceStrings_es.xaml.
In Visual Studio :
- Put all those RDs in the same folder (I used 'Languages').
- Set those RD properties 'Build Action' as 'Content', as it should be packed with your application.
- Set 'Copy to
Output Directory' to 'Copy always'.
Second step: Include one of this language dictionaries as a resource for your application
Choose your fallback language: this will be the language you want to see in design time, and the language of your application when it launches (if you don't change
language at startup).
Add the fallback dictionary inside your resources in the first
place within the App/Window/Control you want to localize. For application resources, this will look like:
<Application (...) >
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
-->
<ResourceDictionary Source="/Languages/InterfaceStrings_en.xaml"/>
-->
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
If you have other resources (like themes, styles, templates...) put them in another
ResourceDictionary
inside the MergedDictionaries
after the fallback RD. Notice that he fallback dictionary will always be kept inside the application RD, so you can rest assured that all that worked at design time will at least display in your
fallback language at run time.
It also allows you to add some keys only in the fallback language during
development, then add the other languages strings later-on.
Rq 1: In the EZLocalise
constructor you set FallBackLanguage
. You should always set its value to be the real fallback language, i.e., the language of the first
ResourceDictionary
of your
application (in the above example, "en").
Rq2: To use another language at design time, change this first
dictionary within your App resources, and update your EZLocalize
constructor's FallBackLanguage
.
Third step: Setup the library
Add a reference to EZLocalize.dll to your project. You now have access to
the EZLocalizeNS
namespace. Create a new EZLocalize
to handle your resources. The arguments are:
- The resource to localize
- The fallback language
- The folder where the application is launched; use null to auto-detect
for non-ClickOnce applications.
- The folder within the application folder where to find language files
- The base name for the localized files
So for application resources, the code is:
(C#):
MyEZLocalize = new EZLocalize(App.Current.Resources, "en", null, "Languages\\", "InterfaceStrings");
(VB):
MyEZLocalize = New EZLocalize(My.Application.Resources, "en", Nothing, "Languages/", "InterfaceStrings")
So now your project should look like:
- Rq1: Attempting to localize a resource more than once will raise an exception.
- Rq2: You can retrieve an existing
EZLocalize
for a given resource by using the static/shared GetInstanceForResource(ResourceDictionnary)
.
- Rq3:
CurrentLanguage
, FallBackLanguage
, CurrentLggPath
are properties that provides the expected information.
- Rq4: The library can help you find available language files with
getFileBaseNames
, which gives you all the file base names within a folder ("InterfaceStrings")
and LanguagesForFileBaseName
will give you all languages for a base name (LanguagesForFileBaseName("InterfaceStrings")
= { "en", "fr", "es"} ). - Rq5: use naming conventions if you localize many objects, and use Reflection.
Expl : if you localize MyUserControl, use "MyUserControl" as file base name.
If LanguagesForFileBaseName("MyUserControl")
is empty then your control is not localized.
Fourth step: Use the localised resources
Now each time you are using a string in your application :
- you must always declare it in the FallBack RD
- you
can declare it very localised RD.
- you must no longer 'hard-code' your strings : see below how to retrieve a string.
Rq : As mentioned above, a non-existing key in a 'foreign' (non-fallback) language will only result in the application displaying a fallback language string, so you can delay the translation while coding, and test at run time in a foreign language even if not every string is translated in this language.
To use your strings, use standard WPF:
In XAML, use a DynamicResource Markup extension:
<TextBlock Text="{DynamicResource HelloStr}" />
Rq: If you use StaticResource, only the fallback language will get used and
XAML strings will not get updated if you dynamically change the language, unless you change
the language during application
startup (before Windows/controls gets loaded).
In code,use standard WPF also : FindResource
/TryFindResource
, or SetResourceReference
for dynamic use on controls.
(C#):
string MyHelloString = (string)TryFindResource("HelloStr");
MyTextBlock.SetResourceReference(TextBlock.TextProperty, "HelloStr");
(VB):
Dim HelloString As String = My.Application.TryFindResource("HelloStr")
MyTextBlock.SetResourceReference(TextBlock.TextProperty, "DynamicStr")
!! I used the application resource as an example, but you must use the resource
dictionary that you localized to get your localized strings. For instance, in the code behind of a user control that you localised, use
this.TryFindResource("MyStringName")
.
!! If you are using both strings defined at application level and strings defined at a lower level, when seeking for a resource the search will start from the lower then walk up the visual tree until it finds a match. So a control won't find the Application strings unless the control is within this visual tree.
If you want to change the text of an element which is not a Framework element (such as the header
of a DataGridView
column for instance) you cannot use SetResourceRefrence
, you have to update it yourself: in this case handle the
LanguageChanged
event.
(C#):
EZLocalise.LanguageChanged += HandleLggChanged; EZLocalise.LanguageChanged -= HandleLggChanged;
private void HandleLggChanged (string newLgg) { }
(VB):
AddHandler EZLocalise.LanguageChanged, AddressOf HandleLanguageChanged
RemoveHandler EZLocalise.LanguageChanged, AddressOf HandleLanguageChanged
Private Sub HandleLanguageChanged(ByVal newLgge As String)
End Sub
Changing the language at startup/ at run time
On application startup, the default language will be the fallback language you defined in the application resources.
If you want to use the current OS language as the language on startup, you have to:
- handle the startup event (in App.xaml/Application.xaml, define
Startup="Application_Startup"
)
- in the handler, change language using current culture:
(C#):
string CurrentOSLgg = System.Globalization.CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
EZLocalise.ChangeLanguage(CurrentOSLgg);
(VB):
Dim CurrentOSLgg=CultureInfo.CurrentCulture.TwoLetterISOLanguageName
EZLocalise.ChangeLanguage(CurrentOSLgg)
As mentioned earlier, if you update language before startup, StaticResource will use that language. So if you do not wish to change language dynamically, use StaticResource to
slightly reduce overhead.
If you want to change language at any time, just call ChangeLanguage
with the new language string ("en", "fr", ...). And all your strings will update.
(C#):
string res = MyEZLocalise.ChangeLanguage("fr");
(VB):
Dim res = MyEZLocalise.ChangeLanguage("fr")
If you want to check if a language was loaded successfully, just test if the returned string is the language requested (res == "fr"
). If not,
res
is an error message in English (: "file not found" or "xaml file corrupted at line xxx").
To ClickOnce users: Folder issue
If you are either:
- Distributing your application by copying the Debug/ or Release/ folder
- Using a classical deployment project (generating setup.exe and .msi files)
- Using a third party installer
Then there is no concern for you. But, as mentioned earlier, the EZLocalize
class won't work with ClickOnce applications, that you get if you
use 'Publish' to provide your applications. The reason for that is that the ClickOnce applications are
SandBoxed when they are launched,
so there is no way to retrieve the launch folder, hence no way to get the full path for the
XAML files containing the localised
resource dictionaries.You can set the folder where to seek for languages in the library constructor, if you know where to find it .
Details : I found no reliable way to have it work on both XP and Seven,
even within MS documentation or StackOverflow posts... and if you look here: MSDN documentation,
you will see at the end of the "ClickOnce and Windows Installer Comparison Table", that the "Application installation location"' is the "ClickOnce application cache".
Using EZLocalize with larger projects
EZLocalize is very simple and small: Unless someone asks, I won't
detail the code (provided in VB and C#).
If you want to use EZLocalize
within a large project, or if you support many languages, it will soon take too much time to add each key in all supported languages.
I developed a small application, StringResourceEditor, that allows to add/remove/modify/export in
CSV/import in CSV all the key value pairs,
which is more convenient and avoids many errors. I added this app in the 'Tools' menu of Visual Studio 2010, so
I can change any string at any time during development. I also gave this tool to my customers so that they can edit the interface strings, which in fact is
useful even for small projects since
most people don't know how to edit an XAML file. So I suggest you:
- add StringResourceEditor.exe in your Languages folder.
- set its 'Build Action' to 'Content'.
- set 'Copy to Output Directory' to
'Copy Always'.
Then, to add it as a tool in VS2010 :
- Go into 'Tools' menu, 'External Tools' sub menu,
- Choose a title ('string edit'),
- Command:
$(ProjectDir)Languages\StringResourceEditor.exe,
- Argument: admin,
- Initial Directory:
$(ProjectDir).
When called with "admin" as parameter, StringResourceEditor allows to add/remove a key (developer). Otherwise those choices are hidden (customer).
Screenshot for the small demo strings:
The demo
A small demo is provided with this article, in its executable format, as a C# project and also as a VB project. Feel free to play with the controls, you can even add a language file into "Languages" folder, (expl: InterfaceStrings_de.xaml) and this language will be available on the next application startup. Screenshots:
Rq1: To create a new localisation, go into the 'Languages' folder, copy the fallback version, and rename it (expl for
Italian: copy InterfaceStrings_en.xaml
to InterfaceStrings_it.xaml). Once you have the new file, you can use StringResourceEditor.
Rq2: If you edit the strings directly with a text editor, change the content, !not the keys!, And use an UTF16 compliant editor, otherwise you won't be able to save non-English text.
Last words
Rq1: My only concern here was text but a similar scheme might be used for any type of resource (color, image, ...). expl : the flag image for each country.
Rq2: You might want to use localised strings at Window/Control/DLL/... level for more modularity. I found it more relevant at
application level,
since the same words are used and again in all parts of an application.
You might even do both (application wide strings + component specific strings).
I hope this library can help you localize your application, tell me if it did!
History
- January 8, 2103: First published.
- January 23, 2013 :Updated StringResourceEditor (a non admin could delete a key) + minor clarifications.