Use a Custom Resource Markup Extension to Succeed at UI String Globalization!

Herrick Spencer

Herrick

Markup Extension for Resource lookup

I learned of this technique when I was given the opportunity to lend my efforts on a new feature for the inbox app team (a separate team on the fabulous PAX team I’m part of). Thanks to Matt Cooley on that team for introducing it to me and giving some history of this technique. The same general idea, implemented differently using XAML’s CustomResource, has been circulating internally at Microsoft for years. Originally the idea came from the XAML team that originally developed the MarkupExtension feature used in this technique (specifically, Alan Wu). Lots of other internal teams have re-implemented it independently.

Thank you XAML team and Alan! πŸ™

TLDR

Microsoft’s current documentation shows examples of how to globalize your UI facing strings in UWP/WPF XAML by using the Uid as a lookup in an included resx file. This technique works great, but does have some limitations. We want to show you the way our inbox apps team (Think Alarms/Clock/Calculator/Maps/many more) are using a custom markup extension to keep the UI development agile, as well as reduce globalization costs when redesigning their UI. πŸš€

Well… stop reading TLDR’s and continue on to find out how it’s done! πŸ‘‡ [Skip down to the ‘Background on the common technique without the markup extension‘ section if you want to review the most common technique for implementing resource strings.]

Using the markup extension

Commonly used resource string technique:

 <Button x:Uid="MyButton"/>

With the markup extension:

<Button Content="{str:ResourceString Name=ButtonText}"/>

As you see we are using a custom markup extension to bind your content strings. We are able to simply pass in the name of the resource we want to use and get the localized string back.

Now our resource file need only contain simple names without the properties, making it much cleaner and easier to read.

Name Value
ButtonText Click Me!
GreetingText Hello World! (works!)
Farewell Fails to work without property mentioned in resource name

And as opposed to the Uid technique we no longer need to specify a property in the resource name, and even duplicate uses of a single string are ok!

<Button Content="{str:ResourceString Name=ButtonText}"/>
<Button Content="{str:ResourceString Name=Farewell}"/>
<TextBlock Text="{str:ResourceString Name=Farewell}"/> <!-- dupe is ok! -->
<TextBlock Text="{str:ResourceString Name=Greeting}"/>
<TextBox Text="{str:ResourceString Name=Greeting}"/> <!-- dupe is ok! -->

The ResourceString code example would look like this:

    [MarkupExtensionReturnType(ReturnType = typeof(string))]
    public sealed class ResourceString : MarkupExtension
    {
       private static ResourceLoader resourceLoader = ResourceLoader.GetForCurrentView();

       public string Name
        {
            get; set;
        }

        protected override object ProvideValue()
        {
            return resourceLoader.GetString(this.Name);
        }
    }

Don’t forget to also add the xmlns of your new MarkupExtension to the XAML code:

    xmlns:str="using:MyProject.ResourceHelpers"

The advantages to this include:

  • Reuse of the same string in multiple places.
  • Use of the string without changes to Uid.Property naming. This enables refactoring or redesign of the XAML in a much more agile manner.
  • The custom markup extension code can potentially handle formats as well, with dynamic replacement of placeholders in bound data of XAML.
  • No need to re-localize your resx after UI changes because the resx will suffer far less churn by reusing existing strings.

Possible negative consequences using the MarkupExtension technique:

  • Relies on the MarkupExtension base, available in Windows 10 16299 (aka the Fall Creators Update or version 1709) and later. If you are supporting earlier versions, this is not an option for you.
  • I personally haven’t measured performance characteristics. In theory it’s comparable to x:Uid, and in practice we didn’t see a regression in the app-wide perf metrics for apps, but I haven’t done something like a microbenchmark to say for sure how much faster or slower it is than x:Uid.

Alternative option for native lookup

If you’re worried about performance and want to implement a native lookup solution and gain similar advantages to this setup, you can use a native binding lookup, by using Rudy Huyn’s OSS project DotNetPlus/ReswPlus Wiki on GitHub. Native binding allows you to do way more (converters, support Intellisense, etc…) and allows you to use the other features offered by Resw; including string pluralization (196 languages supported), string formatting, macro, html formatting, etc…

Background on the common technique without the markup extension

As Documented on Windows Developer Docs

As a refresher, the advice in Microsoft Dev docs is to use a resource file that contains name value pairs to add string resources for a particular locale. The Uid of a control would be the base of the name. example of resource file

Next you would add a Uid to your component to reference it from the resource file:

<Button x:Uid="MyButton"/>
<TextBlock x:Uid="Greeting"/>
<TextBlock x:Uid="Farewell"/> <!-- doesn't work due to missing property in resource name! -->

You need to reference the property of the control you are targeting in the name of the resource to direct the string appropriately.

Example:

Name Value
MyButton.Content Click Me!
Greeting.Text Hello World! (works!)
Farewell Fails to work without property mentioned in resource name

This leads to some difficult situations when the UI needs to be altered or redesigned, or when using the same string in multiple controls.

Once the App is completed using the Uid technique

You have coded up your very beautiful looking UI in XAML and did the appropriate globalization step of using Uid for controls that need localized strings, adding each string in the resx file as shown above with their appropriate property setter (.Text or .Content). This took a while to make sure that each string rendered accurately from the resource file, ensuring that no customer facing string was left un-localized.

Next, the resx file, possibly containing hundreds of strings to be localized, was sent to your Globalization/Localization team for translations into all the different locales your application supports. This step is painstaking and can take days/weeks of effort. You only want to have to do this ONCE.

Scenario one – redesigns

The app is code-complete, and all your strings are localized into a dozen different languages. Congrats! πŸŽ‰πŸΎ

Suddenly you get an email from the PM or Designer on the project. Whoa, hold up. We are overhauling the UI to accommodate new changes in your branding. Many of the views will remain the same, but lots of the controls need to be moved around or altered to entirely different controls. This causes a lot of churn on the resx file, causing all the impacts listed below

Scenario two – Changing UI without churning strings

If a developer needs to make a minor change to the XAML markup (say, changing a Hyperlink to a HyperlinkButton), a change to the resource keys .resw file is often required even if the string does not change. This triggers additional work in the localization system, which can constrain app teams’ timelines when implementing features. You might not have the same constraints Microsoft internal teams do around how the localization system works, but the change still would involve a level of translation/verification.

Scenario three – Reusing strings

A developer might want to ensure the same string is used in two places on a page. Using x:Uid, the string must be in the .resw file twice and localization teams must manually check to ensure the duplicates are handled correctly.

Scenario four – Developer productivity

It’s faster to deal with only one file. For example, code reviewers want to spot common mistakes in XAML (like forgetting to set the AutomationProperties.Name property on an element) without cross-referencing a .resw file. In the Uid technique, you will need to have a separate resx for each project you have in the solution. No sharing allowed. β›” In the Resource Markup Extension technique, you will be able to reference the strings from XAML and share across controls and projects.

Impact

What do these scenarios mean to your development time? You still could use those translated strings, but now will possibly need to edit all the Uids to appropriately reference different controls the strings target. Additionally, if the control has different presenter properties, you will now need to address this situation as well, switching myButton.Content to myTextbox.Text and making sure the Uid is updated as well in the XAML correctly.

Next, you will need to re-validate and re-test that all strings are properly localized in release build. Doubling the effort for that previously completed work item! πŸ™…β€β™€οΈ (πŸ’‘ Use lorem ipsum in design-time data for this! Let me know if you want to see a blog post on this too.)

Last, and most important, the tedious work your Localization team did to translate these strings needs to have another pass verifying nothing has changed. That process isn’t any different, but what about the context within the app? If a previously translated string was on a button, but now is in a label, it may not translate similarly in a certain language. This is something to pay attention to and to alert your team of. 😎

The Resource Markup Extension will resolve most if not all of the above mentioned issues and scenarios. Try it out and let us know what you think.

What next?

I hope you enjoyed this background on string resource globalization techniques. If there are other insights you’d like me or my team to elaborate on in future blog posts, please comment below and we’ll add them to the queue.

One of you might also want to consider adding this to the Windows Community toolkit, which already has a collection of markup extensions. Your contribution to this effort would be most appreciated! There is already a “help-wanted” feature request here for you to get more details. Please read the shiny new contribution guide to help you get started πŸ˜‹

Cheers!

3 comments

Comments are closed. Login to edit/delete your existing comments

  • Avatar
    Breece Walker

    For those of us still supporting version 1703, would it be problematic at all to use a static x:Bind function in place of a markup extension? It would seem like it would work the same, albeit with a less appealing syntax.

    • Herrick Spencer
      Herrick SpencerMicrosoft employee

      Thanks for asking Breece. So I was just going to answer ‘Yes’, but thought I’d do a bit better and look into it myself.

      The syntax is not so bad as you would think using x:Bind Func. What I did was add a second class with a static function to my ResourceHelpers namespace. As you can see it is very similar to the RME version, but is now a static function with a requires parameter. This is the code:

          public sealed class ResourceStringCreators
          {
              private static readonly ResourceLoader resourceLoader = ResourceLoader.GetForCurrentView();
      
              internal static string ProvideValue(string name)
              {
                  return resourceLoader.GetString(name);
              }
          }

      The xaml to use this is very similar too, so not a terrible option if you can’t use the RME option.

      Text="{x:Bind resources:ResourceStringCreators.ProvideValue('HelloWorld')}"
      

      I used a literal string here “HelloWorld” as the key in my resource file, but you could also simply put a value from your datacontext in its place.

      Thanks for the interesting question.

  • Herrick Spencer
    Herrick SpencerMicrosoft employee

    Bit of a edit to the code above… put the resourceLoader inside the class… (or you can use another solution that works for you).

        [MarkupExtensionReturnType(ReturnType = typeof(string))]
        public sealed class ResourceString : MarkupExtension
        {
            private static ResourceLoader resourceLoader = ResourceLoader.GetForCurrentView();
    
            public string Name
            {
                get; set;
            }
    
            protected override object ProvideValue()
            {
                return resourceLoader.GetString(this.Name);
            }
        }