Pseudo-localization with ASP.NET Core
Using pseudo-localization with ASP.NET Core to help test your application is ready for globalization, internationalization and localization.
Earlier this year I read a blog post by Tim Brandall at Netflix about how they use pseudo-localization to test the User Interfaces of their various native applications for layout issues. For example, text in languages such as German and Finnish can be up to 40% longer than their English equivalents, causing text overflow in UI elements that don't account for such differences.
A simple example of pseudo-localisation would be changing the text of the sentence below.
The quick brown fox jumped over the lazy dog
With transformations to lengthen the text, apply accents and surround it in brackets applied, it becomes:
[Ţĥéẋ ǫûîçķẋẋ ƀŕöŵñẋẋ ƒöẋẋ ĵûɱþéðẋẋ öṽéŕẋẋ ţĥéẋ ļåžýẋẋ ðöĝẋẋ]
I found the approach particularly interesting, so wondered how I could look at using it in my own day-to-day work.
Before this year I'd not particularly worked on a large-scale application where its users could speak one of several different languages, but as part of my current role at Just Eat that's no longer the case.
The team I'm a member of has been working on a new updated experience for our consumer-facing website which needs to be global for all of our core markets and localised for the languages that the majority of our consumers speak in those markets. This includes variants of English for the UK, Ireland and Australia, as well as Danish, French, Italian, Norwegian and Spanish for our other core markets.
The application my team maintains that requires such localisation is an ASP.NET Core web application that uses MVC with Razor views, so to help successfully deliver a high-quality experience for all of its users regardless of the language they speak, I thought that I'd investigate what tools were available to incorporate pseudo-localisation (which from here on I'll refer to as "p16n" for ease) into our development workflow.
Before trying to re-invent the wheel, I thought I'd see what tools and processes were already available in the .NET/C# space for p16n (and localisation generally).
The first tool I found was a Windows console application written in 2012 by Anders Kaplan called Pseudolocalizer.
The tool contains five built-in transformations for p16n and can operate on existing
.resx files (the default XML-based localisation file format for .NET) to produce transformed text in a pseudo-locale with a language code of
qps-ploc (which I'll come back to later).
Unfortunately due to its age, it only supports usage for .NET Framework and Windows, and is not available for download as a pre-compiled binary, only as MIT-licensed source code in GitHub.
Reading up on
qps-ploc from Pseudolocalizer lead me to an article on MSDN about four pseudo-locales that are built into the Windows operating system itself to aid such testing.
This means that using the
"qps-ploc" culture with the .NET
CultureInfo class enables p16n to be automatically applied to string formatting operations on values of types such as
That's definitely useful, but falls far short of a full solution. After all, very few application's user interfaces consist solely of numbers and dates with no other accompanying labels or description.
The XLIFF File Format and xliff-tasks
While researching this, I also stumbled across how the .NET Core SDK tooling is localised on GitHub, which uses a file format called XLIFF, which stands for "XML Localization Interchange File Format".
This file format is an industry standard for managing translation of text, and contains metadata for things like translations' state (translated, new, in need of review etc.), which makes it easier to manipulate and maintain with tooling compared to the
Digging through the open-source .NET Core repositories on GitHub lead me to a repository for a set of MSBuild-based tasks that operate on a source
.resx file to generate
.xlf files for all the languages required that can then be submitted for translation. It's also helpfully published in the public .NET Core MyGet feed.
This tooling also detects drift between the source strings (e.g. English) and the translations. This means that if the meaning of the original text is changed, metadata can be emitted into the
.xlf file(s) warning of the drift and that the translation may need updating.
.xlf files as the compilation source, rather than
.resx file(s), the tooling then integrates with the C# compiler to produce satellite resource assemblies for each of the languages being targeted for use at runtime in the application.
This sounds useful to come back to for a full implementation in due course for a production application, so I'll talk about it again later in this post.
A Proof of Concept
So with a tool to generate p16n strings and some operating system support, I thought I'd have a go at manually generating a
.resx file for
qps-ploc and trying it out in the companion website for my Alexa skill.
I cloned Pseudolocalizer locally, compiled it and ran it against the
.resx file in my site's repository, set the request language to
qps-ploc and compiled and debugged my application to find...normal English text.
I was perplexed - the p16n text wasn't there. Half an hour of investigation later, I discovered why - the
qps-ploc satellite resource assembly wasn't there.
Looking into the source code of MSBuild (isn't open source software great? ❤️) I found that for performance reasons, the valid cultures are cached and compared against a hard-coded list. The list didn't include the pseudo-locales of Windows, so MSBuild considered the locale code invalid and ignored it from compilation.
So I had some bits of tooling, but the compiler "feature" coupled with the lack of a compiled distribution of Pseudolocalizer meant that I didn't have a workable process for usage for a production business application.
Updating the Approach
With the pieces of the puzzle I'd found all being open source, all of the problems mentioned above were surmountable. This meant it was time to get writing some code (and tests) to get a working process ready.
Forking and Modernising Pseudolocalizer
Given that Anders Kaplan seems to be pretty inactive on GitHub.com these days and that there has been no activity in the repository for nearly 6 years, I decided to fork the repository and make my own updates to the tool for the goal I wanted to achieve, rather than submit them back via a Pull Request.
You can find it here: https://github.com/martincostello/Pseudolocalizer
- Updates to target .NET Core and allow usage on Linux and macOS.
- Support for processing
- Packaging the application as a .NET Core Global Tool.
- Making the core string-transformations available as library on NuGet.
- Creating an integration with Humanizer.
- Fixing transforms for strings with formatting placeholders and embedded HTML.
Generating a qps-ploc Satellite Resource Assembly
Fixing MSBuild so that the p16n resource DLLs were generated by the compiler was fairly simple, so I submitted a Pull Request to add support for it.
The Microsoft team reviewed, gave feedback and merged the change within less than a week, and the fix itself shipped as part of Visual Studio 15.9 in November 2018.
Putting Pseudo-localization into Practice
With the launch of Visual Studio 15.9 and PseudoLocalize being available from NuGet.org, it was possible to put p16n to use in to the ASP.NET Core application my team maintains to help us with testing the globalisation and localisation of the application.
I also integrated it my Alexa skill's companion site (Pull Request).
Finally, I made a simple sample application illustrating how to add p16n to an ASP.NET Core 2.2 MVC application that demonstrates it in use.
It's available here on GitHub under the Apache-2.0 licence: https://github.com/martincostello/aspnet-core-pseudo-localization
Example and Walkthrough
The example is a simple Todo application that stores a list of tasks in memory that can be added, marked as complete and deleted. It uses Humanizer to show the age of tasks, and as it accepts user input it shows both site-provided content, which is localised, and user-provided content, which is left as-is.
Apologies if you speak French, German, Japanese or Spanish and the text isn't...quite right - all the text was generated by putting the English text into Google Translate.
Below is a screenshot of the application when set to use UK English.
Below is a screenshot of the application when the language is changed to the pseudo-locale. Notice that all of the text has been updated except for the user-provided Todo items' descriptions. Even the operating system-formatted date has been changed.
You can find the source
.resx file here and the
.xlf files here.
Below are the relevant steps for wiring p16n into the application.
Add the MyGet feed
Add the .NET Core MyGet feed to
nuget.config for the
<add key="dotnet-core" value="https://dotnet.myget.org/F/dotnet-core/api/v3/index.json" />
Add the xliff-tasks package
Install the xliff-tasks NuGet package.
<PackageReference Include="XliffTasks" Version="0.2.0-beta-63125-01" PrivateAssets="All" />
Specify the required languages
Configure the languages to generate
.xlf files for.
Configure qps-Ploc for requests
qps-Ploc as a supported request language.
Note the capitalisation of the P in the culture code here. On non-Windows platforms, this is not a known culture, so the default casing rules are applied internally to force
qps-Ploc, which can then cause issues with case-sensitive file systems such as Linux.
Using the normalised casing prevents failures on Linux and macOS due while still working as expected on Windows.
Add p16n for Humanizer
Install the NuGet package.
<PackageReference Include="PseudoLocalizer.Humanizer" Version="0.1.0" />
Configure support for
Regenerate p16n Text When Source Changed
Add a custom MSBuild task to the project file so that PseudoLocalize is invoked to regenerate the
qps-Ploc strings whenever the
.resx file is updated (e.g. by running
dotnet msbuild /t:UpdateXlf).
<ItemGroup> <_PseudloLocacalizedFiles Include="$(MSBuildThisFileDirectory)xlf\*.qps-Ploc.xlf" /> </ItemGroup> <PropertyGroup> <_PseudoLocalizeInstalledCommand Condition=" '$(OS)' == 'Windows_NT' "> where pseudo-localize </_PseudoLocalizeInstalledCommand> <_PseudoLocalizeInstalledCommand Condition=" '$(OS)' != 'Windows_NT' "> which pseudo-localize </_PseudoLocalizeInstalledCommand> </PropertyGroup> <Target Name="UpdatePseudoLocalization" AfterTargets="UpdateXlf"> <Exec Command="$(_PseudoLocalizeInstalledCommand)" ConsoleToMsBuild="true" IgnoreExitCode="true" StandardErrorImportance="Normal" StandardOutputImportance="Normal"> <Output TaskParameter="ExitCode" PropertyName="_PseudoLocalizeInstalled" /> </Exec> <Warning Condition=" $(_PseudoLocalizeInstalled) != 0 " Text="The PseudoLocalize .NET Core Global Tool is not installed." /> <Warning Condition=" $(_PseudoLocalizeInstalled) != 0 " Text="To install this tool, run the following command: dotnet tool install --global PseudoLocalize" /> <Exec Condition=" $(_PseudoLocalizeInstalled) == 0 " Command="pseudo-localize %(_PseudloLocacalizedFiles.Identity) --overwrite --force" ConsoleToMsBuild="true" StandardOutputImportance="Normal" /> </Target>
It took a few months of occasional work between the initial idea and being able to use it in a production scenario, but it took just a few days of effort to update and publish PseudoLocalize and fix the compiler support to be able to plug p16n into an ASP.NET Core application.
It's also now available for use in the application my team maintains for checking for layout issues with UI localisation.
It was interesting to learn about, and a fun challenge to get working. I hope you find the information in this blog post useful and informative for adding pseudo-localization to your own ASP.NET Core applications!