# String Localization Issues for C#

Rhys Weatherley, rweather@southern-storm.com.au.
Last Modified: $Date: 2001/11/07 01:17:55 $

Copyright © 2001 Southern Storm Software, Pty Ltd.
Permission to distribute unmodified copies of this work is hereby granted.

## 1. Introduction

The ECMA specifications for C# have fairly extensive support for localizing the strings that applications use. However, the mechanisms are not very "natural" to use for more than a small handful of strings.

This document discusses various alternatives, with a view to devising a common localization framework for use by both Portable.NET and Mono. This framework may also be submitted to ECMA for consideration.

> Note: we explicitly limit ourselves to string localization issues in this document. We do not consider dates, times, localized bitmaps, and the other issues that an application must be aware of.

Each alternative is evaluated based on the following criteria:

- The degree of "naturalness" to the mechanism. The more natural the mechanism, the more likely programmers will use it without being "ordered" to do so.
- Whether or not new features are needed in the C# language.
- Whether or not applications that use the proposal will continue to work with existing C# library implementations.
- Whether or not the same proposal works for both applications and the "corlib" runtime library.
- The ease of gatewaying to pre-existing localization systems such as GNU gettext.
- The ease of automatically extracting a list of strings to be translated into a foreign language.
- The behaviour when a localized string is not present.
- The ease of calling other house-keeping methods related to localization, beyond the simple "get string" API.

We do not judge which of these criteria is desirable or not at this stage. Extending the C# language may be drastic, and hence may be unattractive, but it could also lead to cleaner code, and hence be desirable. The decision as to which proposal to adopt is not the purpose of this document. This document is a starting point for discussion.

## 2. Definitions

corlib
  The common C# runtime library, which is called "mscorlib.dll" in the Portable.NET and Microsoft implementations, and "corlib.dll" in the Mono implementation.
String-Based
  A resource system that uses the English string as the key. GNU gettext is an example of such a system.
Tag-Based
  A resource system that uses a synthetic identifier as the key. e.g. "`Arg_MustBeBoolean`"

We generally lean towards string-based systems in the following sections, because it is easier to provide fallbacks when translations are missing. However, the techniques discussed remain mostly the same if tag-based systems are used. The primary difference is that the programmer must extract the strings for translation manually.

## 3. Existing Functionality

Microsoft's "mscorlib.dll" contains a class called "`System.Resources.ResourceManager`" which can be used to access the string resources that are embedded in the manifest resource section of an assembly.

> Note: this class does not yet appear in the ECMA specifications, but that is probably an oversight rather than a deliberate omission. We expect that the "`System.Resources`" namespace will eventually be standardised.

To access localized strings, an application creates an instance of "`ResourceManager`" and then calls the "`GetString`" method to access strings using their assigned "tag".

The most immediate problem with this API is that it requires that the resource manager be created first. Since creating a new manager for every string access is expensive, Microsoft has adopted a convention in their own code for getting around the API's limitations.

Each of Microsoft's assemblies contains an "`internal`" class or method that caches the resource manager for that assembly in a static field. After the first string access, all subsequent accesses are relatively efficient. Microsoft's "mscorlib.dll" uses "`Environment.GetResourceString`" and their other assemblies typically have a class called "`SR`" within that assembly's namespace.

There is no consistency to Microsoft's approach: every assembly must come up with its own manager caching strategy. None of these classes are accessible to other assemblies to improve code reuse.

Using our evaluation criteria:

- The sequence "`Environment.GetResourceString("Tag")`" is not very natural, and quickly becomes burdensome.
- No new features are present in the C# language.
- Applications do work with existing implementations, as this is how existing implementations have done it.
- The mechanism is different for every assembly.
- Gatewaying is theoretically possible, if one were to change the implementation of "`ResourceManager`".
- Automatic extraction of strings is impossible. Programmers must manually extract the strings, assign a tag, and then be diligent enough to keep the extracted versions up to date.
- If a localized version of a string is not present, the API returns `null`, which isn't very useful as a default. This is exacerbated by the previous point, because it is very easy for the programmer to forget to assign a fallback English string in the separate resource file.
- Accessing house-keeping API's is similar in difficulty to accessing the "get string" API's.

The use of the "`SR`" class in so many assemblies is quite odd. It may be the result of massive cut-and-paste, or it may be the result of some undocumented feature of Microsoft's tools that automatically generates a class called "`SR`" in each assembly that requires it.

The "`SR`" class contains a number of standard methods such as "`GetString`", "`GetObject`", etc. It usually also contains hundreds of fields that hold tag names for all of the strings that are used by the assembly.

We cannot find any evidence in the .NET Beta2 Framework documentation that suggests that the C# tools are doing this automatically (perhaps it is a feature of Visual Studio.NET?). But it seems strange that Microsoft's programmers would create classes with that many fields on purpose, and then do it consistently across dozens of assemblies and thousands of classes, unless there was some kind of tool support.

If Microsoft is indeed writing the "`SR`" classes by hand, then this would seem an obvious place to introduce some automation.

## 4. GNU gettext

We describe the GNU gettext system in this section, to provide an example of an existing localization framework that is well-deployed and solves many of the problems we are trying to address.

The system is very natural for programmers to use. Strings can be marked for localization as follows:

> `_("Hello World!")`

The C pre-processor takes care of expanding this to an appropriate call to `gettext`, or `dgettext` in the case of libraries. On systems without `gettext`, the '`_`' macro expands to its argument and the program behaves correctly, albeit only in the original language.

> Note: gettext also has the "`N_`" macro, which is used to mark static strings. Because C# strings are always created dynamically, we do not require an equivalent to this.

Because the overhead of localization is so low, it increases the chance that programmers will use it and use it consistently. Automated tools can scan C source code for uses of the '`_`' macro and create English resource files ready for translation.

If a translation is not available, the macro's argument provides a meaningful English fallback. Synthetic tag names or null strings can never be displayed to the user by accident.

If the programmer wishes to gateway to a non-gettext system, they need only modify the definition of the macro and recompile. Usually this isn't necessary because GNU gettext already supports most of the popular message catalog formats.

## 5. New C# Libraries

Because C# lacks a rich pre-processor, it isn't possible to adopt the GNU gettext approach as-is. This leaves us with two options: add new libraries or add keywords to the language. This section explores the first option.

We can create a new namespace within the C# library called "`System.I18N`", that contains a number of classes that manage resources for any assemblies that use it. The following is a simplified example of implementing a gettext-style system:

>     namespace System.I18N
>     {

>     using System.Collections;
>     using System.Reflection;
>     using System.Resources;

>     public sealed class GetText
>     {
>         private static Hashtable managers = new Hashtable();

>         public static String _(String value)
>         {
>             Assembly assembly = Assembly.GetCallingAssembly();
>             ResourceManager mgr = (ResourceManager)(managers[assembly]);
>             if(mgr == null)
>             {
>                 mgr = new ResourceManager(assembly.FullName, assembly);
>                 managers[assembly] = mgr;
>             }
>             String str = mgr.GetString(value);
>             if(str != null)
>             {
>                 return str;
>             }
>             else
>             {
>                 return value;
>             }
>         }

>     } // class GetText

>     } // namespace System.I18N

A real implementation would need additional house-keeping methods, culture support, and thread synchronization. A single resource block per assembly may insufficient; thus requiring the introduction of gettext-style "domains". We have omitted these details for brevity.

The effect of C's '`_`' macro could be achieved as follows:

>     using I = System.I18N.GetText;

>     class Hello
>     {
>         public static void Main()
>         {
>             System.Console.WriteLine(I._("Hello World!"));
>         }
>     }

The usage of "`I._`" is a little ugly. If we were willing to modify the core system library, we could do the following:

>     namespace System
>     {

>     using System.I18N;

>     public class Object
>     {
>         ...

>         protected static String _(String value)
>         {
>             return System.I18N.GetText.GetString(value);
>         }

>         ...
>     } // class Object

>     } // namespace System

This would make the definition available to all classes uniformly. However, it wouldn't be backwards compatible with pre-existing C# system libraries.

Using our evaluation criteria:

- The mechanism is almost as natural to use as GNU gettext.
- No new features are needed in the C# language.
- Applications that use this will not work with existing C# libraries that lack definitions for "`System.I18N`". However, that namespace could be implemented in a separate assembly that is distributed with applications.
- See below regarding uniformity.
- Gatewaying to other implementations requires changes to the "`System.I18N`" code, but not to any applications that use it.
- Automatically extracting strings is easy.
- The behaviour when a localized string is not present is straight-forward: return the argument.
- House-keeping API's are simply extra methods within the "`System.I18N`" classes.

Placing "`System.I18N`" into a different assembly will improve interoperability with existing C# libraries. However, it will be inaccessible to our own implementation of "corlib". Thus, we will need a different mechanism for "corlib".

However, this may not be too bad. We can create a specialized "`internal`" class within our "corlib" that has a similar API to "`System.I18N.GetText`", and then do the following:

>     namespace System
>     {

>     using I = System.Private.GetText;

>     public class FormatException : SystemException
>     {

>         // Constructors.
>         public FormatException()
>             : base(I._("The supplied value did not have the correct format")) {}
>         public FormatException(String msg)
>             : base(msg) {}
>         public FormatException(String msg, Exception inner)
>             : base(msg, inner) {}

>     } // class FormatException

>     } // namespace System

The usage of "`I._`" is consistent with that used by other assemblies, but is redirected to a different class. This would allow automated extraction tools to work consistently on "corlib" and other assemblies.

The main drawback of this approach is that gateways must be implemented in two places within the code. This is still better than Microsoft's approach.

## 6. New C# Keywords

We could envisage adding a new '`_`' keyword to the language as follows:

> string-literal:
> `_` `(` string-literal `)`
> ...

This would be legal anywhere that a string literal is currently permitted, except in serialized attributes. The compiler converts the construct into a call on an appropriate system-supplied library.

The "`System.Object`" definition trick from the previous section can be used to provide an implementation of '`_`' for compilers that lack the keyword.

Using our evaluation criteria, everything is the same as the "new library" case, except that we have now altered the C# language. Interoperability with existing C# libraries remains a problem, but could be addressed by once again treating "corlib" as a special case.

## 7. Internal Libraries

Even with a keyword-enhanced compiler, and a rich supporting library, we still have some problems.

If we place "`System.I18N`" into "corlib", then programs that use it will not run against existing C# libraries, even if they were compiled with a keyword-enhanced compiler.

If we place "`System.I18N`" into a separate assembly, then all programs that use it will need to redistribute that DLL with their program. And the code will be inaccessible to our own "corlib", which would need to be treated as a special case.

Going back to the example of Microsoft's "`SR`" class, and assuming that we can modify the compiler, we may be able to do better. The compiler could automatically insert an "`internal`" class into any assembly that uses the '`_`' keyword in its source.

This class will be responsible for acquiring a resource manager, caching it in a static field, and handling all resource requests for that assembly. The compiler will cause the '`_`' keyword to call this internal library rather than one that is assumed to be present elsewhere in the system.

Using our evaluation criteria:

- The mechanism is just as natural to use as GNU gettext.
- A new keyword is needed in the C# language, and the compiler must know how to insert the special class automatically.
- Applications that use this will mechanism work with existing C# libraries.
- The mechanism is the same for "corlib" and other assemblies.
- Gatewaying to other implementations is more difficult.
- Automatically extracting strings is easy.
- The behaviour when a localized string is not present is straight-forward: return the argument.
- House-keeping API's are more difficult to access, unless the compiler gives the class a fixed name, with a pre-declared interface.

A drawback of this approach is that it makes gatewaying harder. Interfacing to a different resource system will involve recompiling all assemblies that have the internal library, unless one has the luxury to modify "`ResourceManager`".

One way to solve the gateway issue may be to provide an "escape hatch" in the internal library. This would use reflection to search for an alternative resource manager before falling back to the standard one. For example:

>     private static ResourceManager GetManager(Assembly assembly)
>     {
>         Type type = Type.GetType
>             ("System.I18N.ReplacementResourceManager", false);
>         if(type != null)
>         {
>             return (type.GetConstructor(Type.EmptyTypes)).Invoke(null);
>         }
>         return new ResourceManager(assembly.FullName, assembly);
>     }

This would allow future versions of the C# library to provide an implementation of "`System.I18N.ReplacementResourceManager`" that overrides the manager's behaviour with gateway functionality. Existing C# library implementations won't have this class, and so the code will fallback to a standard resource manager.

This "internal library" solution introduces a lot of complexity into the compiler, to get around deficiencies in existing C# libraries. It may not be worth the effort.

## 8. Acknowledgements

Miguel de Icaza suggested the "`using`" construct as a way to shorten the method name to "`I._`", while avoiding language extensions.

## 9. Revision History

7 November 2001: Original version