Using Satellite Assemblies to Isolate Localised Resources
By Alan Dean
Published: 11/17/2003
Reader Level: Intermediate Expert
Rated: 3.00 by 3 member(s).
Tell a Friend
Rate this Article
Printable Version
Discuss in the Forums

I have been developing shrinkwrapped software products for most of my professional career. Therefore, I have an abiding respect for internationalization and localization. In fact, it is something of a running joke to warn new developers who join not to kick me off on the subject. Of course, they will hear from me soon enough....

Shrinkwrapped products can end up in a bewildering set of environments and countries. Simply saying that your product will only run on {insert OS} with {insert language, usually US English} is not a recipe for success. One of the aspects of .NET that I really like is the sheer pervasiveness of support for globalization (as it is known in .NET).

Utilizing the support in .NET for satellite assemblies is a powerful way to approach this problem. The essential idea behind satellite assemblies is to isolate localizable resources from your main application, and from each other. If you thoroughly isolate your resources, then you will not need to recompile your application code in order to support any new culture. (Essentially a culture represents a language, plus country or region). This is important when you consider that most of the localization work for an application follows after the main development period.

I have looked at a number of references, but nothing seemed to explicitly set out exactly how to localize a standard class. So I dug into the localization model of WinForms to understand it (thank you ILDASM) and this is a distillation of what I learned.

Important: The technique described here is only appropriate for class libraries. Applications require resources to be embedded.

Part 1: The Default Resource

First, let me show the implementation of a default resource. It is imperative that your application has a default resource. This is one culture for the framework to fallback on gracefully if the appropriate satellite assemblies are missing.

This example contains a base Person class with a single accessor Name, and a derived Employee class:

Figure 1: The design model

Figure 2: The code 1.0 (boiler plate classes)

[C#]

using System;

namespace CompanyName.SatelliteAssemblyDemo
{
   // Base class
   public class Person
   {
      // Member variable
      private string theName;

      // Accessors
      public string Name
      {
         get
         {
            return theName;
         }
         set
         {
            theName = value;
         }
      }
   }

   // Derived class
   public class Employee: Person
   {
   }

}

[VB]

' Base class
Public Class Person

      ' Member variable
      Private theName As String

      ' Accessor
      Public Property Name()
         Get
            Return Me.theName
         End Get
         Set(ByVal Value)
            Me.theName = Value
         End Set
      End Property

End Class

' Derived class
Public Class Employee : Inherits Person

End Class

The obvious way to implement a default name value is to construct the member variable with a constant string value.

Figure 3: The code 1.1 (constant default value)

[C#]

   // Base class
   public class Person
   {
      ...

      // Constant
      private const string theDefaultName = "New Person";

      // Constructors
      public Person(): this(theDefaultName)
      {}
      public Person(string aName)
      {
         this.Name = aName;
      }

      ...
   }

   // Derived class
   public class Employee: Person
   {
      // Constant
      private const string theDefaultName = "New Employee";

      // Constructors
      public Employee(): base(theDefaultName)
      {}
      public Employee(string aName): base(aName)
      {}
   }

[VB]

' Base class
Public Class Person

   ' Constant
   Private Const theDefaultName As String = "New Person"
   ...

   ' Constructors
   Public Sub New()
      Me.New(theDefaultName)
   End Sub
   Public Sub New(ByVal aName As String)
      Me.theName = aName
   End Sub
   ...

End Class

' Derived class
Public Class Employee : Inherits Person

   ' Constant
   Private Const theDefaultName As String = "New Employee"
   ...

   ' Constructors
   Public Sub New()
      Me.New(theDefaultName)
   End Sub
   Public Sub New(ByVal aName As String)
      MyBase.theName = aName
   End Sub
   ...

End Class

There are a number of problems with this:

  • The values are embedded in the code, which this makes maintenance difficult.
  • The values will be the same regardless of the culture. The French are certain to notice that every Nouvelle Personne is being called a New Person.
  • The temptation will be to implement in-line code to handle support for localization to any new cultures.

A better approach is to use the globalization support built into the .NET framework. The following code demonstrates the use of the ResourceManagerclass:

Figure 4: The code 1.2 (implement ResourceManager)

[C#]

...
using System.Reflection;
using System.Resources;
...

   // Base class
   public class Person
   {
      ...

      // Constant
      private const string theDefaultName = "New Person";
      private const string theResourceBaseName = "CompanyName.SatelliteAssemblyDemo.Person";

      // Static method
      private static string defaultName()
      {
         ResourceManager _ResourceManager = new ResourceManager(theResourceBaseName, Assembly.GetExecutingAssembly());
         return _ResourceManager.GetString("defaultName", Thread.CurrentThread.CurrentUICulture);
      }

      // Constructors
      public Person(): this(theDefaultName)
      public Person(): this(defaultName())
      {}

      ...
   }

   // Derived class
   public class Employee: Person
   {
      // Constant
      private const string theDefaultName = "New Employee";
      private const string theResourceBaseName = "CompanyName.SatelliteAssemblyDemo.Employee";

      // Static method
      private static string defaultName()
      {
         ResourceManager _ResourceManager = new ResourceManager(theResourceBaseName, Assembly.GetExecutingAssembly());
         return _ResourceManager.GetString("defaultName", Thread.CurrentThread.CurrentUICulture);
      }

      // Constructors
      public Employee(): base(theDefaultName)
      public Employee(): base(defaultName())
      {}
   }

[VB]

...
Imports System.Reflection;
Imports System.Resources;
...

' Base class
Public Class Person

   ' Constant
   Private Const theDefaultName As String = "New Person"
   Private Const theResourceBaseName As String = "Demo.Logic.Person"

   ' Static method
   Private Shared Function defaultName() As String
      Dim _ResourceManager As ResourceManager
      _ResourceManager = New ResourceManager(theResourceBaseName, [Assembly].GetExecutingAssembly())
      Return _ResourceManager.GetString("defaultName")
   End Function

   ' Constructors
   Public Sub New()
      Me.New(theDefaultName)
      Me.New(defaultName())
   End Sub
   ...

End Class

' Derived class
Public Class Employee : Inherits Person

   ' Constant
   Private Const theDefaultName As String = "New Employee"
   Private Const theResourceBaseName As String = "Demo.Logic.Employee"

   ' Static method
   Private Shared Function defaultName() As String
      Dim _ResourceManager As ResourceManager
      _ResourceManager = New ResourceManager(theResourceBaseName, [Assembly].GetExecutingAssembly())
      Return _ResourceManager.GetString("defaultName")
   End Function

   ' Constructors
   Public Sub New()
      Me.New(theDefaultName)
      Me.New(defaultName())
   End Sub
   ...

End Class

In order to 'complete' this code, the minimum needed is to have an embedded resource for the assembly. This is in order to have at least one resource set available for the ResourceManager class. This resource is known as the culture-neutral resource, and it is used as the failsafe for resource operations. Simply add a new Assembly Resource File called Person.resx to the project and add the following entry into the data table:

       name = defaultName
       value = New Person

Then add an Employee.resx to the project and add

       name = defaultName
       value = New Employee

These are now the default resources for the Person and Employee classes.

For each new class requiring localization, add an assembly resource file to the solution with the same name as the class.

If you view the compiled assembly using ILDASM, there will be a section for each resource in the manifest:

Figure 5: Assembly culture-neutral resources viewed in ILDASM

   .mresource public CompanyName.SatelliteAssemblyDemo.Person.resources
   {
   }
   .mresource public CompanyName.SatelliteAssemblyDemo.Employee.resources
   {
   }

Note that from a practical point of view, this is enough to carry on with your main development, extending the resource data table as required. Of course, data tables can contain other types than strings. I have just demonstrated a simple example.

Part 2 will show how to manage and compile the resources for additional cultures. The important point is that you will not need to open up vs .NET to do this, so you won't need to recompile your code when the sales team comes over to tell you that they want a Klingon version of the software (don't laugh too hard, Unicode has a Klingon range...)

Part 2: The Satellite Resources

Time has passed, development has continuedn and now you have to support localization. Please note that you do not have to run vs .NET at all for this part.

If you look at the contents of your .resx files, you will find that internally they are simply XML. The sections created in Part 1 will look like the following:

Figure 6: Resources file XML

   ...
   <data name="defaultName">
      <value>New Person</value>
   </data>
   ...

Currently there are only the default resources, so in order to localize you will need to make new .resx files.

The naming pattern for resx files is

       ClassName.Culture.resx

The first localization is language. I find it easiest to use my culture-neutral resources file as a template, then edit it to a culture. So, for our example,

       Copy Person.resx to Person.en.resx,    Person.fr.resx, etc.
       Copy Employee.resx to Employee.en.resx,    Employee.fr.resx, etc.

The second localization is to country or region. So for our example,

       Copy Person.en.resx to Person.en-GB.resx,    Person.en-US.resx, etc.
       Copy Person.fr.resx to Person.fr-FR.resx,    Person.fr-CA.resx, etc.
       and so on...

These files can then be distributed to localizers for translation. The files then need to be compiled to the appropriate satellite assemblies.

Compilation is a two-phase process:

  1. Compile the .resx file to a .resource file with the RESGEN (resource generator) utility.
  2. Compile one or more .resource files to an assmbly with the AL (assembly linker) utility.

The assemblies have to be emitted to correctly named subdirectories of the main assembly. Each subdirectory is simply named by culture (e.g., ...\en\, ...\en-GB\, ...\fr\, ...\fr-FR\, etc.). The satellite assembly has the same name as the main assembly, but with .resources immediately before the file extension.

For our example, the file structure will look like the following:

  • ApplicationDir\CompanyName.ResourceManagerDemonstration.dll
  • ApplicationDir\en\CompanyName.ResourceManagerDemonstration.resources.dll
  • ApplicationDir\en-GB\CompanyName.ResourceManagerDemonstration.resources.dll
  • ApplicationDir\en-US\CompanyName.ResourceManagerDemonstration.resources.dll
  • ApplicationDir\fr\CompanyName.ResourceManagerDemonstration.resources.dll
  • ApplicationDir\fr-CA\CompanyName.ResourceManagerDemonstration.resources.dll
  • ApplicationDir\fr-FR\CompanyName.ResourceManagerDemonstration.resources.dll

RESGEN and AL have fairly complex switches available, so here is just an example batch file for compilation of English resources for the Person and Employee classes:

Figure 7: Sample compilation batch file

   MD "bin\Release\en"
   RESGEN Person.en.resx obj\Release\CompanyName.ResourceManagerDemonstration.Person.en.resources
   RESGEN Employee.en.resx obj\Release\CompanyName.ResourceManagerDemonstration.Employee.en.resources
   AL /t:lib /culture:en
      /embed:obj\Release\CompanyName.ResourceManagerDemonstration.Person.en.resources,CompanyName.ResourceManagerDemonstration.Person.en.resources
      /embed:obj\Release\CompanyName.ResourceManagerDemonstration.Employee.en.resources,CompanyName.ResourceManagerDemonstration.Employee.en.resources
      /template:bin\Release\CompanyName.ResourceManagerDemonstration.dll
      /out:bin\Release\en\CompanyName.ResourceManagerDemonstration.resources.dll

When these satellite assemblies are present, the ResourceManager class will be able to provide the correct resource set for the thread culture. If the appropriate culture is missing, then the framework will gracefully fall back to the default culture.

The sample code in both VB and C# is available here


Marketplace
(Sponsored Links)
What are the green links?
   



 
Copyright © 2007 CMP Tech LLC |
Privacy Policy (4/10/06) | Your California Privacy Rights (4/10/06) | Terms of Service | Advertising Info | About Us | Help