Developer

Localizing your Xamarin Cross-Platform Application

Dec, 2016

It is rare that I have complaints about Windows Phone. The issues we encounter with the Microsoft are miniscule compared to those we encounter on Android, and are minimal compared to the masochistic hoops that Apple makes you jump through to develop and publish on their platform. But this time, Microsoft really F’ed things up.

Prior to Windows Store apps and the Universal Runtime framework. All localization was performed in Resx files. Microsoft moved the model to Resw with the Windows Store apps. I’m not going to debate if this was a good or bad decision. However the implementation was terrible.

Ignore the fact that Resw files no longer generate a class giving you properties for all your string localization. Some don’t like the code generation, but personally I prefer not to sprinkle magic strings throughout my code, and would like to see a compile time error if the name of a localized string changes.

The big problem comes from using Portable Class Libraries. This is where Microsoft broke cross platform development. Resx files are still “supported” within a portable class library, however they don’t work when used from a Windows 8.1 Universal Runtime app. This pretty much breaks the notion of “portable” class library.

But the worst part is that Resx files do work, when debugging in Visual Studio. Once you switch to Release mode, they stop working. WTF?!? This creates a very challenging issue to troubleshoot. If you are like us, we used a localized string for “Initializing” to show during the load phase, after the Splash screen. This made our app crash at startup, but only in Release mode.

Using Xamarin or Xamarin Forms with a Windows Phone Universal Runtime

We are using Xamarin Forms for our application development. Resx files in portable class libraries work fine for Andorid and iOS, as you would expect. However the Resx issue on Windows Phone is exacerbated by Microsoft’s implementation of Navigation.

In the Xamarin template you will find this method as part of the initial application initialization that loads your first Xamarin Page. This is the Microsoft navigation method for Windows Phone.

                    
if (!rootFrame.Navigate(typeof(MainPage), e.Arguments))
{
    throw new Exception("Failed to create initial page");
}
                        
                    

This loads your first Windows Phone page, which in turn calls the Xamarin method “LoadApplication” in the constructor of the MainPage. Windows’ Navigate method traps all exceptions thrown and instead returns a Boolean value of false. As a result, the exception thrown in the page setup is trapped and discarded. This is the situation you will find yourself in.

  1. You have an exception that is only thrown at runtime in release mode. You will not hit an exception breakpoint when debugging this in Visual Studio.
  2. The exception that is thrown is trapped and discarded, so it will not be written to the dump log.

To get around this situation, we moved all our page setup code into a method that was called on a timer (executed once). This delayed execution meant that the page navigation would succeed. Then 1 second later the page setup code would fire. This code would fail due to the issue with Resx not being supported in the Universal Runtime, and finally a dump log would appear on the phone to show us that a Resource was not available.

NOTE:

When I say that Resx is not supported, what I mean to say is that Resx is not supported correctly. Windows Phone 8.1 Universal runtime migrates the Resx files that are defined in a Portable Class Library (PCL) to Resw files (at compile time) so that the localized strings are available to the application. But here is where the implementation is broken. Resw files cannot be read by Portable Class Libraries.

As a result, the generated code from your Resx file (example Resources.resx created Resources.cs) is still available and compiles. However, at runtime (only in release mode) the attempts to retrieve the values from the xml files containing your localization strings will fail. In effect, Visual Studio is building code that can never work at runtime, when in release mode.

This means the code within the PCL cannot access the resources defined in the PCL. The localized string have been migrated to the Windows Phone project, which is completely inaccessible and unknown to the PCL. Do you see the problem? The Resources class can’t read its own resources, which it defined.

Fixing the Microsoft Mistake

While I am going to speak within the context of Xamarin Forms, this solution would work for any developer who need to re-use a PCL with a Windows Phone/Store application and a legacy style app that uses Resx files. This probably is more likely to hit a Xamarin Forms developer for two reasons.

  1. Xamarin Forms is implemented in a portable class library.
  2. Xamarin Forms builds the UI, which is where you need your localized strings the most, and you are most likely to use localized strings as part of UI initialization, causing the app to crash on startup with no dump log (as mentioned above).
We used ApplicationStrings.resx as the name of the localization file. Going forward my examples will use this name, instead of the Resources.resx that Visual Studio will create by default. We use this name because it is more explicit that this resx file contains the text for the application. Remember resx files have the ability to hold more than strings.

If you are like us, you sprinkled access to your strings all throughout your Portable class libraries.

                    
MyLabel.Text = ApplicationStrings.WelcomeMessage;
                        
                    

We were looking for a solution that avoided the need to go through our entire application and change the 700+ string references. We found a few resources that would inject a substitution method into the generated ApplicationsStrings.cs object, using reflection to change a private variable. While this was certainly the least amount of code it wasn’t simple for non-expert developers to understand and ran the risk of modifying a Visual Studio generated file, who’s content/format could change some day.

We also wanted to add additional robustness to the poorly implemented Microsoft solution. A failure to obtain a resource string because of a cross-platform issue, or because a localization file in another language was missing the value, should have a default value instead of crashing the application.

Step By Step

Step 1

Ensure that every project in your application contains this directive

                    
[assembly: NeutralResourcesLanguage("en")]
                        
                    

For us, this directive was already in our PCL projects inside the AssembliyInfo.cs (under Properties), but we added it to the Windows Phone project. We are unsure if this is a “required” change, but every thread we found regarding the Resx issue listed this as a step to perform.

Step 2

Create a new resx file. This is necessary if you want to keep all your existing code pointing at “ApplicationStrings” to keep working. It is easier to select all and copy and paste all the strings into a new resx file, than to do a global search and replace (refactor) though your code. It will keep your source control cleaner since you won’t be modifying hundreds of files just for a class rename.

We created a new Resx file called “StringCatalog.resx” and copied in all our localization strings. Change the access modifier to “public” at the top of the page that shows your string values. This is required. Visual Studio will not create the Resw file from the Resx at compile time if the class is not public.

Step 3

Delete your ApplicationStrings.resx file

Step 4

Create a T4 template include file in the root of your solution folder. Name this file “ResxToAccessorClass.txt”

This file isn’t the actual T4 template that will be executed. It will be imported into a .tt file. We used this approach for reusability. It is possible that we may want to run this transformation on multiple resx files. Perhaps the help content (currently online) may one day be included in the app, and it may be desirable to keep that separate from the UI strings.

You would not need to create a separate file for this T4 code, and you would not need to locate it at the root of the solution. This is your preference. You could locate the file anywhere, or include the content of the txt file directly in your T4 template file.

Copy this content into the new ResxToAccessorClass.txt file. (You may need to correct line wrapping issues.)

                    
<#@ output extension=".cs" #>
<#@ template language="C#" hostSpecific="true" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="System.Xml.Linq" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml.Linq" #>
<#@ import namespace="System.Xml.XPath" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.Globalization" #><#+ public void Process(string resxRelativePath, string namespaceName)
                                                  {
                                                  WriteLine("using System;");
                                                  WriteLine("namespace " + namespaceName);
                                                  WriteLine("{");
                                                  WriteLine("////////////////////////////////////////THIS IS A GENERATED FILE. NO DOT MODIFY.////////////////////////////////////////////");
    WriteLine("\tpublic class ApplicationStrings {");
	WriteLine("\t\tpublic static ILocalizationHelper LocalizationHelper { get; set; }");
	WriteLine("\t\tstatic ApplicationStrings()");
	WriteLine("\t\t{");
	WriteLine("\t\t\tLocalizationHelper = new ResxLocalizationHelper(); //Initialize to resx, Windows Phone/Store will need to inject replacement");
	WriteLine("\t\t}");
	WriteLine("");
 
    //throw new Exception(Directory.GetCurrentDirectory());
    XDocument document;
    try
    {
        document = XDocument.Load(resxRelativePath);
    }
    catch (FileNotFoundException)
    {
        WriteLine("<!-- File not found: " + resxRelativePath + " -->");
        WriteLine("}}");
        return;
    }
 
    IEnumerable<XElement> dataElements = document.XPathSelectElements("//root/data");
 
    foreach (XElement element in dataElements)
    {
        string elementName = element.Attribute("name").Value;
        string elementValue;
 
        XElement valueElement = element.Element("value");
 
        if (valueElement != null)
        {
            elementValue = valueElement.Value;
        }
        else
        {
            continue;
        }
 
        elementValue = elementValue.Replace("\"", "\\\"");
 
        WriteLine(string.Format("\t\tpublic static string {0}", elementName));
        WriteLine("\t\t{");
        WriteLine("\t\t\tget");
        WriteLine("\t\t\t{");
        WriteLine("\t\t\t\tstring val = null;");
        WriteLine("\t\t\t\ttry");
        WriteLine("\t\t\t\t{");
		WriteLine(string.Format("\t\t\t\t\t\tval = LocalizationHelper.GetString(\"{0}\");", elementName));
 
        WriteLine("\t\t\t\t} catch (Exception exc) {");
        WriteLine("\t\t\t\t\tif (System.Diagnostics.Debugger.IsAttached){ System.Diagnostics.Debug.WriteLine(\"LOCALIZE ERROR: \" + exc.Message); }");
        WriteLine("\t\t\t\t}");
        WriteLine("\t\t\t\tif (string.IsNullOrEmpty(val))");
        WriteLine("\t\t\t\t{");
        WriteLine(string.Format("\t\t\t\t\tval = \"{0}\".ToUpper(); //Uppercase will be a clue that localization is broken, but allow app to keep working", elementValue));
        WriteLine("\t\t\t\t}");
        WriteLine("\t\t\t\treturn val;");
        WriteLine("\t\t\t}");
        WriteLine("\t\t}");
    }
 
    WriteLine("\t}\r\n}");
}
#>
                        
                    

Step 5

Create an ApplicationStrings.tt file in the same folder where ApplicationStrings.resx was located.

Step 6

Copy this text into ApplicationStrings.tt

                    
<#@ include file="..\ResxToAccessorClass.txt" #>
<#@ template language="C#" hostSpecific="true" #>
<# 
    Process(Path.GetDirectoryName(Host.TemplateFile) + "./StringsCatalog.resx" , "Qonqr.Client.Localization" ); 
#>
                        
                    

Step 7

Change the last parameter in the Process method in ApplicationStrings.tt file to match your namespace.

Step 8

Create a new file in the same directory as the new Resx and T4 files called “ResxLocalizationHelper.cs”.

Step 9

Copy this text into ResxLocalizationHelper.cs (fix the namespace to match your namespace)

                    
using System;
namespace Qonqr.Client.Localization
{
    public interface ILocalizationHelper
    {
        string GetString(string name);
        void ChangeLanguage(string lang);
    }
 
    public class ResxLocalizationHelper : ILocalizationHelper
    {
        public string GetString(string name)
        {
            //don't try catch on this method, let the error bubble
            return StringsCatalog.ResourceManager.GetString(name, StringsCatalog.Culture);
        }
 
        public void ChangeLanguage(string lang)
        {
            try
            {
                StringsCatalog.Culture = new System.Globalization.CultureInfo(lang);
            } 
            catch(Exception exc)
            {
                if (System.Diagnostics.Debugger.IsAttached)
                {
                    System.Diagnostics.Debug.WriteLine("LOCALIZATION ERROR: Failed to change language for " + (lang ?? "[NULL]"));
                    System.Diagnostics.Debugger.Break();
                }
            }
        }
    }
}
                        
                    

Step 10

In the Windows Phone project, create a file called “ReswLocalizationHelper.cs”

Step 11

Copy this text into the ReswLocalizationHelper.cs file (fix the namespace to match your namespace)

                    
using Qonqr.Client.Localization;
using System;
using System.Globalization;
using Windows.ApplicationModel.Resources;
 
namespace Qonqr.Wp.Blue.Helpers
{
    public class ReswLocalizationHelper : ILocalizationHelper
    {
        private ResourceLoader _resourceLoader;
 
        public ReswLocalizationHelper()
        {
            SetLoader();
        }
 
        private void SetLoader()
        {
            _resourceLoader = ResourceLoader.GetForViewIndependentUse(typeof(StringsCatalog).FullName);
        }
 
        public string GetString(string name)
        {
            //don't try catch on this method, let the error bubble
            var text = _resourceLoader.GetString(name);
            return text;
        }
 
        public void ChangeLanguage(string lang)
        {
            try
            {
                var culture = new CultureInfo(lang);
                Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = culture.Name;
                CultureInfo.DefaultThreadCurrentCulture = culture;
                CultureInfo.DefaultThreadCurrentUICulture = culture;
                SetLoader();
            }
            catch (Exception exc)
            {
                if (System.Diagnostics.Debugger.IsAttached)
                {
                    System.Diagnostics.Debug.WriteLine("LOCALIZATION ERROR: Failed to change language for " + (lang ?? "[NULL]"));
                    System.Diagnostics.Debugger.Break();
                }
            }
        }
    }
}
                        
                    

Step 12

From the Windows Phone project, inject the Resw Localizer into ApplicationStrings before any strings are used. For us, we did this in the constructor of App.xaml.cs in the Windows Phone project.

                        
ApplicationStrings.LocalizationHelper = new ReswLocalizationHelper();
                        
                    

Step 13

Going forward, add any new strings to StringCatalog.resx. Then, right click on ApplicationStrings.tt and select “Run Custom Tool”. You must do this now, to create the initial ApplicationStrings.cs file. All your existing references in your code that previously accessed ApplicationStrings.MyLocalizedText should still be valid. You must re-run the T4 template every time you modify StringCatalog.resx.

Solution Review

By implementing this solution, you are generating a wrapper class, for your localization strings. This wrapper class will allow you to continue accessing your localized strings as you had before, but will create a solution that also allows all your PCL code to continue working with your resources in Windows Phone/Store apps.

DO NOT write code that access StringsCatalog.cs directly. Make all calls to ApplicationStrings.cs. It is unfortunate the StringsCatalog generated class will be in your solution because it will create an opportunity for your developers to mistakenly access this file. However the resx file must be marked public so that Resw migration will happen for Windows Phone projects.

For us, we completely encapsulated our localization solution into its own project. If you have done the same, your project should look like this.

This may be overkill, but encapsulating the localization into its own project gives us the chance to have a dedicated unit test project to make sure every future language file has a translation for every string, lets us send off the entire project to an outsourced team to manage our localization, and makes it easy to exclude this whole mess from an obfuscator. Who knows what might break when obfuscated, and do you really need to encrypt strings that will be shown in the UI anyway.

Code Review

The T4 template will create the injection spot for the custom Localizer and initialize it to the Resx version, which will be used by Android and iOS. The Windows Phone project will replace the localizer in App.xaml.cs

                    
public static ILocalizationHelper LocalizationHelper { get; set; }
static ApplicationStrings()
{
	LocalizationHelper = new ResxLocalizationHelper(); //Initialize to resx, Windows Phone/Store will need to inject replacement
}
                        
                    

The T4 template file should generate a getter for every string in the resx file that looks like this example.

                    
public static string MarketplacePrompt
{
	get
	{
		string val = null;
		try
		{
				val = LocalizationHelper.GetString("MarketplacePrompt");
		} catch (Exception exc) {
			if (System.Diagnostics.Debugger.IsAttached){ System.Diagnostics.Debug.WriteLine("LOCALIZE ERROR: " + exc.Message); }
		}
		if (string.IsNullOrEmpty(val))
		{
			val = "Encourage others to join the battle by giving QONQR a 5-Star review.".ToUpper(); 
			//Uppercase will be a clue that localization is broken, but allow app to keep working
		}
		return val;
	}
}
                        
                    
  • All errors are trapped. No more exceptions crashing the application at runtime because of a localization problem
  • Debug statements warn that localization issues were encountered
  • The text value from the default localization file is returned if there was a problem obtaining the requested value. This ensures that if another language is missing a localized string, the default language will be shown.
  • If the default text is used, it will be in uppercase. This is a signal to the developers (and support people) that localization has failed on this device/platform/language, while still keeping the app usable to end users. You can change the T4 template not to upper case defaulted values. Perhaps you prefer to add a “-” to the end of every string that is default as a visual hint there was a problem, while still maintaining application functionality.

Multiple Languages

The solution outlined above, only solves the problem for the default language (in this case English). If you want to support multiple languages you need to take one more step. In this case we added a German localization file in the portable class library.

In order for the Windows Phone 8.1 Universal Runtime project to know that the German localization is available you need to trick it into updating its manifest file. To do this create a resw files in the Windows Phone project for each language. In our case both of these files only include one placeholder string called “Placeholder”. You can put anything in these files, just add something so the files exist and contains at least 1 dummy value (that won’t be used).

Also, make sure your resx files are set to a build action of “EmbeddedResource” and the resw files have a build action of “PRIResouce”. If you copy and paste the files through windows explorer to make all the files (or generate them through an online localization service), then select “Include in Project” to add them to your solution, they will most likely have the wrong build action, causing an entirely different runtime issue.

Troubleshooting

If you application does not generate the resw files in the WP8.1 project (obj folder), open the Package.appxmanifest file, and add the languages manually. Manually delete all bin & obj folders, and rebuild.

                        
<Resources>
    <Resource Language="x-generate" />
    <Resource Language="fr" />
    <Resource Language="pt" />
</Resources>
                        
                    
Set “CopyLocal” on the reference to the PCL containing the Resx files

Summary

Normally, I am Microsoft’s biggest cheerleader. In the 4 years we have been publishing on Windows Phone 7 and iPhone, we have earned twice as much revenue on Windows Phone as on iPhone. Windows Phone makes up 2/3 of our active user base thanks to discoverability available to us in the Windows Phone store, which simply is impossible to achieve in iTunes for an indie game studio with limited marketing budgets.

However this problem cost us more than a week of productivity. It took days to figure out why our app was crashing due to the trapped and discarded error messages, then even longer to realize that Portable Class Libraries don’t have access to their own resources, but only when running in Release mode. The fact Visual Studio performs two dramatically different behaviors when run in debug vs release mode is an intolerable mistake. Not to mention this breaks the concept of “portable” in the PCL libraries. Once we finally figured out what was happening, I found blog posts more than a year old that described possible work arounds. It is unacceptable that Microsoft has not fixed this debug/release difference in over a year. This nightmare has cost us thousands of dollars in lost productivity. That is a big impact for a small business like ours.

I hope this solution helps someone else avoid the same costly experience we encountered. Windows Phone is truly a great mobile market for publishing your apps. Sadly, I worry that hitting the same crash on startup might cause many to abandon the platform and consider it an issue with Xamarin, when it is in fact a Microsoft issue.

Best of Luck.