Mittwoch, 1. Mai 2013

Using resx-files for localization in MvvmCross

With the release of Xamarin.Android 4.6.3 there was a small not in the release notes saying:
5037: Support satellite assemblies. 
This is actually awesome. (At least I think so XD). Up to now we had to use the json-localization-plugin included in MvvmCross. As I do not like to edit json and I guess any translator does not like it as well, I prefer the resx-stuff. 

In this blog-post I do want to describe how to use resx-Files all around your Xamarin.Android/Xamarin.iOS and Windows Phone 8 Solution (do not target WinRT and WPF, but they should actually be a nobrainer as they use resx by default :-)).

First of all, the fix mentioned before did not really fix the satellite assemblies-problem. You have to fix an additonal file by yourself. So start with this:
Filename: Xamarin.Android.Common.targets

At this point, thanks to Jonathan Pryor  for the awesome support on this!

This fixed, we can start by creating all the resx-stuff we need :-)

Additional Note:  I'm working in a solution already containing an android/touch/WP8 project. All of them ar up and running using the latest NuGet-Stuff provided by Stuart :-)

Step 1: Create PCL-Project for the resources
First I called this projects Test.Resources. Never do that unless you do not want to support Windows Phone.. ! Read why here:

This is why I renamed this project to Test.Localization :-)

Add a reference from each project where you need the localization to this new pcl-project.

Step 2: Create Resources Files
Add a single resources-file. I called it "Strings.resx". Add a second Resource-File called "Strings.de-DE.resx"

Make sure your first resources-file has the visibility-modifier set to public. Otherwise we will not be able to see the resources from any other assembly. All the additional translations do not need to generate the code. This means you can set them to "No code generation"




Step 3: Add a new class "ResxTextProvider" to your Core-Project.

using System.Diagnostics;
using System.Globalization;
using System.Resources;
using System.Threading;
using Cirrious.MvvmCross.Localization;
 
namespace Test.Core
{
    public class ResxTextProvider : IMvxTextProvider
    {
        private readonly ResourceManager _resourceManager;
 
        public ResxTextProvider(ResourceManager resourceManager)
        {
            _resourceManager = resourceManager;
            CurrentLanguage = Thread.CurrentThread.CurrentUICulture;
        }
 
        public CultureInfo CurrentLanguage { get; set; }
 
        public string GetText(string namespaceKey, string typeKey, string name)
        {
            string resolvedKey = name;
 
            if (!string.IsNullOrEmpty(typeKey))
            {
                resolvedKey = string.Format("{0}.{1}", typeKey, resolvedKey);
            }
 
            if (!string.IsNullOrEmpty(namespaceKey))
            {
                resolvedKey = string.Format("{0}.{1}", namespaceKey, resolvedKey);
            }
 
            return _resourceManager.GetString(resolvedKey, CurrentLanguage);
        }
 
        public string GetText(string namespaceKey, string typeKey, string name, params object[] formatArgs)
        {
            string baseText = GetText(namespaceKey, typeKey, name);
 
            if (string.IsNullOrEmpty(baseText))
            {
                return baseText;
            }
 
            return string.Format(baseText, formatArgs);
        }
    }
}

Step 4: Initialize your ResxTextProvider in your App-Class:

public class App : MvxApplication
{
    public override void Initialize()
    {
        CreatableTypes()
            .EndingWith("Service")
            .AsInterfaces()
            .RegisterAsLazySingleton();
 
        Mvx.RegisterSingleton(new ResxTextProvider(Strings.ResourceManager));
 
        RegisterAppStart();
    }
}

Here is the point where we pass the ResourceManager from our generated String-class to the ResxTextProvider.

Step 5: Extend your ViewModel with a TextSource:

public IMvxLanguageBinder TextSource
{
    get { return new MvxLanguageBinder("", GetType().Name); }
}

In this example I do not pass a Namespace to the MvxLanguageBinder. This means that the GetText-Method of our ResxTextProvider is always called with an empty first parameter. The second parameter will be the current Type. As we are in the MainViewModel, this well be "MainViewModel". 

Step 6: Register Language Converter
protected override void FillValueConverters(IMvxValueConverterRegistry registry)
{
    base.FillValueConverters(registry);
    registry.AddOrOverwrite("Language", new MvxLanguageConverter());
}
Add this line in your Setup.cs in your android app. Not sure whether this could be improved by the MvvmCross-Framework. I think it should *hint* *hint* :-)

Step 7: Start using MvxBind in your android XML-Code.
<TextView
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:textSize="40dp"
      local:MvxLang="Text test1" />
Android should now work fine, let's move to iOS:
Her is not that much to do. Simply use the MvxLanguageConverter to bind the TextSource to the Label (or whatever) and set the resx-key as CommandParameter.
You know an easier way? Leave a comment :-)
var bindingSet = this.CreateBindingSet();
    bindingSet.Bind(Label1).To(ViewModel => ViewModel.TextSource)
           .WithConversion(new MvxLanguageConverter(),"test1")
     .Apply();
 
   bindingSet.Bind(Label2).To(ViewModel => ViewModel.TextSource)
    .WithConversion(new MvxLanguageConverter(),"test2")
     .Apply();

What has to be said, when building the generated resx-stuff on Mac it fails as it can not find any of this annotation. Simply delete all of them, we do not really need them :-)

All that done, move ahead to Windows Phone. 
As resx-files is the common idea of doing the localization in WindowsPhone, I do not want to explain much on this. Just give a short compressed version of this great tutorial:

Steps on Windows Phone:


  1.  Add ressources to App.xaml (you could also add this to any other xaml file.. but when adding here we do have access to the localized stuff from every screen...)
  2. Make sure your app does support all the cultures you have translations for (right click project -> properties)
  3. Use the resoruces with a binding and StaticResource:
<TextBlock Text="{Binding Path=Strings.MainViewModel_test1, Source={StaticResource Strings}}" />



All my code is available on GitHub:

Hope I did not miss anything, in case of a not working or any improvement, please leave a comment :-)






Kommentare:

  1. Hey Stefan, good work with this. I'm facing one issue, not sure if it's my fault or something that is not implemented, but I'm not able to load .resx files without country code.

    For example, modifying your example and adding a Strings.es-ES.resx works fine, but adding changing it to String.es.resx shows the English value.

    AntwortenLöschen
    Antworten
    1. On which platform does this problem occur?
      Actually I've never tried this.. :-)

      Löschen
    2. I've been testing Windows Phone mostly. On iOS I didn't get it to work.

      Löschen
    3. Actually on WP8 it should work this way.. as this idea of resource-files is comming out of the microsoft world :-)
      But I'll check this as soon as I can find a free time-slot...

      What is not working on iOS? Errors?

      Löschen
    4. Thanks Stefan. Further testing on iOS simulator shown that it uses the region format instead of the phone language. Setting the region format and resetting the iPhone simulator shown the same behavior that WP.

      Löschen
    5. Ou crap.. missed that in my blog, sorry..

      Actually I'm not sure whether this is a bug.. in my opinion it should respect the language as well...
      But when the behaviour on WP is the same.. hmm.. really have to check this by myself on my test-project!

      Löschen
  2. Hey Stefan, this is great. Will come handy. For iOS, instead of creating a new instance of the converter, you can register it as well like you did for Android.

    protected override System.Collections.Generic.List ValueConverterAssemblies {
    get {
    var toReturn = base.ValueConverterAssemblies;
    toReturn.Add (typeof(MvxLanguageConverter).Assembly);
    return toReturn;
    }
    }

    I think you should make it available as a plugin. :)

    AntwortenLöschen
  3. Hi! If you’re interested to localize web software, PC software, mobile software or any other type of software, I warmly recommend this collaborative online localization tool: https://poeditor.com/

    AntwortenLöschen
  4. I am used to using RESX files with .NET however that meant I fell into the trap of adding my strings as "Test1" rather than "MyViewModel.Test1". I really appreciate your work on this but would ask that you make it obvious that you will be required to add an "Invalid Identiifer" and that you need to prefix your string with the ViewModel type. I know that your screenshot shows it but it is very small and I did not zoom it because I thought "I know how to use RESX files".

    This change will stop others as foolish as me getting confused :-)

    Thanks

    Pat

    AntwortenLöschen
  5. Also wanted to add (for others reading this) if you wanted to added Strings that go across ViewModels.

    1. Add a property to VM public IMvxLanguageBinder SharedSource
    2. Pass empty strings to MvxLanguageBinder folr namespaceName AND typeName. In the earlier examples you would have passed viewModelType.Name
    3. Now in the bindings AFAIK you can no longer use the local:MvxLang shorthand but that is fine as @slodge expalins the long hand in this video http://slodge.blogspot.co.uk/2013/05/n21-internationalisation-i18n-n1-days.html. It is local:MvxBind="Text SharedSource, Converter=Language,ConverterParameter=YourSharedStringId, Mode=OneWay" />

    HTH helps some one

    AntwortenLöschen
  6. Hi
    Thanks for your post. It's a while back since you wrote it and I guess there are some changes:
    1. the changes to the common.Targets is done by default. At least on my installations
    2. The code snippet for register the text provider didn't work for me. I had to make it like this:
    Mvx.RegisterSingleton(new MvxResxTextProvider(Strings.ResourceManager));

    Thanks
    Nino

    AntwortenLöschen
  7. I could not get this to work for iOS.

    AntwortenLöschen