Satellite assemblies for custom cultures

Pre-defined Cultures

When dealing with pre-defined cultures Visual Studio does all the work for you. Add a resource file with the right culture name and build your project. A satellite assembly is created behind the scenes and placed in the output directory.

files

Notice how having the file Resources.es-VE.resx results in ResourceSpikeTest.resources.dll being created in the Debug folder, under the right sub-directory for the es-VE culture.

Also notice how the default resource file Resource.resx is not embedded in a separate assembly. This resource file (in a binary format) will be embedded in the project’s dll itself.

The project I’m working with here is a Test project since I want to write some unit tests around the satellite assembly. At this point the satellite assembly is correctly placed in the output directory, but the interesting thing about Test projects is that they don’t run from the target directory but from the TestResults folder instead.

We therefore have to deploy the satellite assembly so that it is available when the test runs.

[TestMethod]
[DeploymentItem( @"es-VE\ResourceSpikeTest.resources.dll", "es-VE")]
public void TestPredefinedCulture()
{
  Thread.CurrentThread.CurrentUICulture =
     new CultureInfo("es-VE");
  Assert.AreEqual("epale", Resource.msg);
}

The DeploymentItem attribute causes our satellite assembly to be copied from the target directory to an es-VE sub-folder inside TestResults (more specifically inside the ‘Out’ folder for each test run). With the satellite assembly correctly deployed the test will pass.

Custom Cultures

Custom cultures are a different beast. Visual Studio will not generate the satellite assembly for you. You have to do it by hand using resgen.exe an al.exe. There is plenty of information around on how to use these tools. Adding the following code to your project’s post-build events will do the trick:

mkdir "$(TargetDir)%cultureName%"

set path=%path%;"$(FrameworkSDKDir)"bin\
set baseName=Resource
set cultureName=x-en-ClientA
set namespace=ResourceSpikeTest

resgen $(ProjectDir)%baseName%.%cultureName%.resx  ^
$(ProjectDir)%namespace%.%baseName%.%cultureName%.resources

AL /t:lib ^
/out:"$(TargetDir)%cultureName%\%namespace%.resources.dll" ^
/c:%cultureName%  ^
/embed:"$(ProjectDir)%namespace%.%baseName%.%cultureName%.resources"

Now after building the project we have a satellite assembly for the x-en-ClientA culture in our target directory, just like we did for the pre-defined culture.

To be able to run a similar unit test as the previous one we must ensure that the custom culture has been properly registered in the machine where the test is running.

public UnitTest1()
{
  if (!CustomCultureIsNotRegistered("x-en-ClientA"))
  {
    var cib = new CultureAndRegionInfoBuilder("x-en-ClientA",
                    CultureAndRegionModifiers.None);

    var ci = new CultureInfo("en-AU");
    cib.LoadDataFromCultureInfo(ci);
    cib.Parent = ci;

    var ri = new RegionInfo("AU");
    cib.LoadDataFromRegionInfo(ri);
    cib.Register();
 }
}

private bool CustomCultureIsNotRegistered(string name)
{
  return CultureInfo.GetCultures(CultureTypes.UserCustomCulture)
     .ToList().Any(c => c.Name.Equals(name));
 }

And once again we need to deploy the satellite assembly using the DeploymentItem attribute in our test method.

[TestMethod]
[DeploymentItem(@"x-en-ClientA\ResourceSpikeTest.resources.dll", "x-en-ClientA")]
public void TestPredefinedCulture()
{
  Thread.CurrentThread.CurrentUICulture =
     new CultureInfo("x-en-ClientA");
  Assert.AreEqual("ClientA rocks!", Resource.msg);
}

It’s worth pointing out that when we want to reference a resource we use the strongly-typed default Resource file.  Any other resource file that we add to the project we can set to ‘No code generation’.  The strongly-typed resource file uses a ResourceManager to find the resource based on the current culture.

Advertisements