Introduction
While creating my first Windows Phone 7 application named InputStudio, I went into the Globalization and Localization issues we all face if we want to support at least the standard/minimum languages as English, Spanish, Italian, French, and German.
Depending on the audience you want to reach (only the native country or the whole world) with your application, and of course how disciplined you are while coding, or even based on your preferences, you may support not only your native language but also others you want to reach since the very beginning of coding.
This simple article and code brings you one of many ways that exist to localize not only your application enumerated data types but also helps you localize your application when you have already finished your project in your native language (neutral language).
Background
Localization is the customization of applications for a given culture or locale. Globalization is the design and development of applications that support localized user interfaces and regional data for users in multiple cultures.
This article is the third part of the series: Journey of a Windows Phone 7 application (from ideas to deployment).
Main page in English
| Enumerated data types in English
|
| |
Main page in German
| Enumerated data types in German
|
| |
Code Explanation
The demo application was created from the MVVM Light Windows Phone 7 template within MS Visual Studio and contains two projects: MvvmLocalized
and the useful XAMLQueryLib.
Enumerated Data Types Localization
Let's start by adding a resource wrapper class for the string
resources binding.
1
2
3 public sealed class ResourceWrapper {
4 private static ApplicationStrings
applicationStrings = new applicationStrings();
5
6 public ApplicationStrings Strings {
7 get {
8 return applicationStrings;
9 }
10 }
11 }
As you can see in the code below, the MainViewModel
class is quite simple, just to show you how to create the actual list of enumerations based on the current device (phone) language.
1
2 public class MainViewModel : ViewModelBase {
3
4 public MainViewModel() {
5 if (IsInDesignMode) {
6
7 }
8 else {
9
10 }
11 }
12
13 private List <eDataType> _dataTypes = new List<eDataType>();
14 public List<eDataType> DataTypes {
15 get {
16 if (_dataTypes.Count.Equals(0))
17 _dataTypes =
EnumsGeneric<eDataType>.EnumAsList(eDataType.Undefined);
18
19 return _dataTypes;
20 }
21 }
22 }
Now add the MainViewModel
class from the ViewModelLocator
with the key "Locator
" as the data context and the ResourcesWrapper
class with the key "StringsLocator
" for accessing our localized language string
s in the application resources.
1 <application.resources>
2 <vm:viewmodellocator x:key="Locator" d:isdatasource="True"/>
3 <local:resourcewrapper x:key="StringsLocator"/>
4 </application.resources>
Let's now see the Language
attribute so we can decorate our enumerated data types with attributes. This will be the base to localize our enumerated data types. The Ordinal
member is used to sort the enum
s by value.
1 [Flags]
2 public enum eLanguage
3 {
4 enUS = 1,
5 frFR = 2,
6 deDE = 4,
7 itIT = 8,
8 esES = 16
9 }
10
11 [AttributeUsage(AttributeTargets.All)]
12 public class LanguageAttribute : Attribute
13 {
14 public eLanguage Language;
15 public int Ordinal;
16
17 public LanguageAttribute(eLanguage language, int ordinal)
18 {
19 this.Language = language;
20 this.Ordinal = ordinal;
21 }
22 }
It is now time to define our enumerated data types:
1 public enum eDataType {
2 [Language(eLanguage.enUS, 0)]
3 Undefined,
4 [Language(eLanguage.enUS | eLanguage.frFR |
eLanguage.deDE | eLanguage.itIT, 1)]
5 Boolean,
6 [Language(eLanguage.enUS, 2)]
7 DateTime,
8 [Language(eLanguage.enUS, 3)]
9 Decimal,
10 [Language(eLanguage.enUS | eLanguage.deDE, 4)]
11 Integer,
12 [Language(eLanguage.enUS | eLanguage.deDE, 5)]
13 ListOf,
14 [Language(eLanguage.enUS | eLanguage.frFR |
eLanguage.deDE | eLanguage.esES, 6)]
15
16
17 String,
18 [Language(eLanguage.enUS | eLanguage.frFR |
eLanguage.itIT | eLanguage.esES, 7)]
19 Url,
20 [Language(eLanguage.enUS | eLanguage.frFR | eLanguage.deDE, 8)]
21 Image,
22
23 [Language(eLanguage.frFR, 0)]
24 Indéfini,
25 [Language(eLanguage.frFR, 1)]
26 DateHeure,
27 [Language(eLanguage.frFR, 2)]
28 Décimal,
29 [Language(eLanguage.frFR, 3)]
30 Entier,
31 [Language(eLanguage.frFR, 4)]
32 Listedes,
33
34 [Language(eLanguage.deDE, 0)]
35 Undefiniert,
36 [Language(eLanguage.deDE, 1)]
37 DatumUhrzeit,
38 [Language(eLanguage.deDE, 2)]
39 Dezimal,
40 [Language(eLanguage.deDE, 3)]
41 URL,
42
43 [Language(eLanguage.itIT, 0)]
44 Indefinito,
45 [Language(eLanguage.itIT, 1)]
46 DataOra,
47 [Language(eLanguage.itIT, 2)]
48 Decimale,
49 [Language(eLanguage.itIT, 3)]
50 Intero,
51 [Language(eLanguage.itIT, 4)]
52 Elencodi,
53 [Language(eLanguage.itIT, 5)]
54 Stringa,
55 [Language(eLanguage.itIT, 6)]
56 Immagine,
57
58 [Language(eLanguage.esES, 0)]
59 Indefinido,
60 [Language(eLanguage.esES, 1)]
61 Boleano,
62 [Language(eLanguage.esES, 2)]
63 FechaHora,
64 [Language(eLanguage.esES, 3)]
65 Decimales,
66 [Language(eLanguage.esES, 4)]
67 Entero,
68 [Language(eLanguage.esES, 5)]
69 Listade,
70 [Language(eLanguage.esES, 6)]
71 Imagen,
72 }
All of the 'magic' in the enumerated data types creation is because of the Language
attribute and the current culture name taken from the current thread.
1 public class EnumsGeneric<t> {
2 public static List<t> EnumAsList(T t) {
3 List<t> items = new List<t>();
4
5 var fields = from field in t.GetType().GetFields()
6 let attribute =
field.GetAttributes<languageattribute>(true).FirstOrDefault()
7 where attribute != null &&
attribute.Language.ToString().StartsWith(
Thread.CurrentThread.CurrentCulture.Name.Substring(0,2))
8 orderby attribute.Ordinal
9 select field;
10 foreach (FieldInfo fi in fields)
11 items.Add((T)fi.GetValue(t));
12
13 return items;
14 }
15 }
This is all the necessary code for the enumerated data types creation.
Gathering Text Strings from All of Your Pages for Localization
This code was created based on the assumption that you have already finished your project and all of your text string
s were written in your native (neutral) language. Now, we need to localize all of these string
s to support at least the standard languages (mentioned above).
I've created a base class for all pages in the project (even the main page). This class will attach the Load
event to all of your pages and will handle the saving of your text string
s into the IsolatedStorage
files for being emailed later to you (when loading the Main Page).
As this project has only one page (Main Page), you will need to quit the application and enter again to email the XML text strings.
Note that the text string files will be generated when you tap the page title.
1 public class PhoneApplicationPageBase : PhoneApplicationPage {
2 public PhoneApplicationPageBase()
3 : base() {
4 this.Loaded += new RoutedEventHandler(PhoneApplicationPageBase_Loaded);
5 }
6
7 void PhoneApplicationPageBase_Loaded(object sender, RoutedEventArgs e) {
8 if (true )
9 this.ManipulationStarted += new EventHandler
10 <manipulationstartedeventargs>(
PhoneApplicationPageBase_ManipulationStarted);
11 }
12
13 void PhoneApplicationPageBase_ManipulationStarted(object sender,
ManipulationStartedEventArgs e) {
14 if (e.OriginalSource is TextBlock &&
((TextBlock)e.OriginalSource).RenderSize.Height.Equals(96))
15 BuildStringResources(this.GetType(), this);
16 }
17
18 public MainViewModel model { get { return this.DataContext as MainViewModel; } }
19
20 public static void BuildStringResources(Type type, FrameworkElement container) {
21 try {
22 using (IsolatedStorageFile file =
IsolatedStorageFile.GetUserStoreForApplication()) {
23 string fileName = string.Format("{0}.xml", type.ToString());
24 if (!file.FileExists(fileName)) {
25 StringBuilder sb = new StringBuilder();
26 ControlSet textBlocks = XamlQuery.ByType<textblock>(container);
27 textBlocks.ForEach(delegate(DependencyObject item) {
28 TextBlock tb = item as TextBlock;
29
30 sb.AppendFormat("<{1}>{0}",
tb.Text, "string");
31 });
32
33 using (XmlWriter w = XmlWriter.Create(file.OpenFile(fileName,
FileMode.Append, FileAccess.Write),
34 new XmlWriterSettings() { Indent = true, OmitXmlDeclaration = true })) {
35 w.WriteRaw(string.Format("<{1}>{0}",
sb.ToString(), "strings"));
36 }
37
38 MessageBox.Show(ApplicationStrings.ResourcesSaved,
ApplicationStrings.Done, MessageBoxButton.OK);
39 }
40 }
41 }
42 catch (IsolatedStorageException e) {
43 MessageBox.Show(e.StackTrace, e.Message, MessageBoxButton.OK);
44 }
45 }
46 }
Once you have received the email with the XML text strings, you can apply the XSLT transformation to generate the XML text, you can add to ApplicationStrings.<language>
; at the end of the day, these resource files are just XML files.
See the files in the Xml folder of the solution.
- ApplicationStrings.xslt is the stylesheet transformation
- input.xml is the actual file sample you will email yourself
- output.xml is the transformed XML, you can copy/paste it into your resource files
Points of Interest
If you have to do this gathering task for more than two projects, you should create a Visual Studio add-in to automate this process. This add-in should also use the Microsoft Translation API or the Simple Resx Editor to automatically translate your resource strings.
History
This is my first post at CodeProject, so it is an honor to be here, since I have always been following CodeProject articles.
Code update: When using CurrentCulture.Name
, use only the first two characters to avoid problems with other subcultures (en, en-US, en-AU, etc.).