Prepare all your apps for localization

No matter how lazy you are   

Loyc.Essentials has a Localize class, which is a global hook into which a string-mapping localizer can be installed. If you’re using Loyc.Essentials anyway, you should use it. It makes your program ready for translation with virtually no effort.

The idea is to convince programmers to support localization by making it dead-easy to do. By default it is not connected to any translator (it just passes strings through), so people who are only writing a program for a one-language market can easily make their code “multiligual-ready” without doing any extra work.

All you do is call the .Localized() extension method, which is actually shorter than writing the traditional string.Format(). (also: using Loyc;)

Note: Admittedly, you can’t use it with C# 6 string interpolation like $"this {...}" because C# seems to be hard-coded to translate string interpolation into calls to string.Format, whose behavior cannot be customized.

The translation system itself is separate from Localize, and connected to Localized() by a delegate, so that multiple translation systems are possible. This class should be suitable for use in any.NET program, and some programs using this utility will want to use different localizers.

Use it like this:

string result = "Hello, {0}".Localized(userName);

Or, for increased clarity, use named placeholders:

string result = "Hello, {person's name}".Localized("person's name", userName);

Whatever localizer is installed will look up the text in its database and return a translation. If no translation to the end user’s language is available, an appropriate default translation should be returned: either the original text, or a translation to some default language, e.g. English.

The localizer will need an external table of translations, conceptually like this:

Key name Language Translated text
“Hello, {0}” “es” “Hola, {0}”
“Hello, {0}” “fr” “Bonjour, {0}”
“Load” “es” “Cargar”
“Load” “fr” “Charge”
“Save” “es” “Guardar”
“Save” “fr” “Enregistrer”

Many developers use a resx file to store translations. This is supported as explained below.

Localizing Longer Strings

For longer messages, it is preferable to use a short name to represent the message so that, when the English text is edited, the translation tables for other languages do not have to change. To do this, use the Symbol method:

// The translation table will be searched for "ConfirmQuitWithoutSaving"
string result = Localize.Symbol("ConfirmQuitWithoutSaving",
    "Are you sure you want to quit without saving '{filename}'?", "filename", fileName);

// Enhanced C# syntax with symbol literal
string result = Localize.Symbol(@@ConfirmQuitWithoutSaving,
    "Are you sure you want to quit without saving '{filename}'?", "filename", fileName);

This is most useful for long strings or paragraphs of text, but I expect that some projects, as a policy, will use symbols for all localizable text.

Again, you can call this method without setting up any translation table. However, the actual message is allowed to be null. In that case, if no translator has been set up or no translation is available, Localize.Symbol returns the symbol itself (the first argument) as a last resort.

If the variable argument list is not empty, Localize.Formatter is called to build the completed string from the format string. It’s possible to do formatting separately - for example:

Console.WriteLine("{0} is {0:X} in hexadecimal".Localized(), N);

In this example, WriteLine itself does the formatting, instead of Localized.

As demonstrated above, Localize’s default formatter, StringExt.FormatCore, has an extra feature that the standard formatter doesn’t: named arguments. Here is an example:

string verb = (IsFileLoaded ? "parse" : "load").Localized();
    "Not enough memory to {load/parse} '{filename}'.".Localized(
      "load/parse", verb, "filename", FileName));

As you can see, named arguments are mentioned in the format string by specifying an argument name such as {filename} instead of a number like {0}. The variable argument list contains the same name followed by its value, e.g. "filename", FileName. This feature gives you, the developer, the opportunity to tell the person writing translations what the purpose of a particular argument is.

The translator must not change any of the arguments: the word {filename} is not to be translated.

At run-time, the format string with named arguments is converted to a “normal” format string with numbered arguments. The above example would become “Could not {1} the file: {3}” and then be passed to string.Format.

Design rationale

Many developers don’t want to spend time writing internationalization or localization code, and are tempted to write code that is only for one language. It’s no wonder, because it’s a pain in the neck compared to hard-coding strings. Microsoft suggests that code carry around a ResourceManager object and directly request strings from it:

private ResourceManager rm;
rm = new ResourceManager("RootNamespace.Resources", this.GetType().Assembly);

This approach has drawbacks:

Microsoft does address the first of these drawbacks by providing a code generator built into Visual Studio that gives you a global property for each string; see here.

Even so, you may find that this class provides a more convenient approach because your native-language strings are written right in your code, and because you are guaranteed to get a string at runtime (not null) if the desired language is not available.

Combining with ResourceManager

This class supports ResourceManager via the UseResourceManager helper method. For example, after calling Localize.UseResourceManager(resourceManager), if you write

    "Save As...".Localized()

Then resourceManager.GetString("Save As...") is called to get the translated string, or the original string if no translation was found (and yes, in your resx file you can use spaces and punctuation in the left-hand side). You can even add a “name calculator” to encode your resx file’s naming convention, e.g. by removing spaces and punctuation (for details, look at the UseResourceManager method.)

It is common in .NET programs to have one “main” resx file, e.g. Resources.resx, that contains default strings, along other files with non-English translations (e.g. for Spanish). When using Localized() you might use a slightly different approach: you still have a Resources.resx file, but you leave the string table empty (you can still use it for other resources, such as icons). This causes Visual Studio to generate a Resources class with a ResourceManager property so that you need can easily get the instance of ResourceManager that you need.

GNU gettext

I haven’t tried this, but C/C++ open source software often uses GNU gettext to do translations. Reportedly, it’s possible to use gettext with C# too.