Introduction
ASP.NET application internationalization uses ResX file to handle translation of labels. This article describes a mocking technique that enables "automatic" translation for any resource contained in resource folders without the need to create and maintain a set of automatically translated resx files. The resource is presented embraced with a culture specific tag if current culture is different from default culture.
French version:
English version:
ASP.NET Resource Folders
ASP.NET Framework handles two kinds of resources:
- Global resources can be used at any level of the web application. The resx file has to be stored inside the
App_GlobalResources
at the root of the application.
- Local resources only do work at the page level. Each web page can define an associated resx file with string resources. The resx file must be stored in
App_LocalResources
at the same hierarchical level as the page and its name has to match web page name with a .resx extension of course.
For more information, here is a link on the MSDN dedicated page.
Building a Custom ResourceProviderFactory
Resource handling is made through a resource provider that enables reading the specific resource. Resource providers are created by a ResourceProviderFactory which propose two methods:
We will implement our own factory in order to create our custom resource providers.
using System.Web.Compilation;
namespace MyProject.Resources {
public class CulturePrefixedResourceProviderFactory : ResourceProviderFactory {
public override IResourceProvider CreateGlobalResourceProvider(string classKey){
return new CulturePrefixedGlobalResXResourceProvider(classKey);
}
public override IResourceProvider CreateLocalResourceProvider(string virtualPath){
return new CulturePrefixedLocalResXResourceProvider(virtualPath);
}
}
IResourceProvider Overview
Resource providers need to implement the IResourceProvider interface which defines:
- ResourceReader property, returns a reader that enables resource reading
- GetObject method, returns a resource value from its key and culture
Our local and global resource providers will override class AbstractResXResourceProvider
that provide a bit abstraction with the implementation of IResourceProvider.GetObject
method that creates the translation mocking aspect.
using System.Globalization;
using System.Resources;
using System.Web.Compilation;
namespace MyProject.Resources {
public abstract class AbstractResXResourceProvider : IResourceProvider {
protected AbstractResXResourceProvider() { }
protected abstract ResourceManager ResourceManager {
get;
}
public virtual object GetObject(string resourceKey, CultureInfo culture) {
if (ResourceManager == null) {
return null;
}
if (culture == null) {
culture = CultureInfo.CurrentUICulture;
}
object item = ResourceManager.GetObject(resourceKey, culture);
string strItem = item as string;
if (strItem != null && culture.TwoLetterISOLanguageName != "fr" &&
culture != CultureInfo.InvariantCulture) {
return string.Format(CultureInfo.InvariantCulture,
"[{0}]{1}[/{0}]", culture.Name, strItem);
}
return item;
}
}
Build a Global Resource Provider
The global resource provider extends the AbstractResXResourceProvider
and implements access to the resource manager located in the dedicated assembly for global resources. This one can be retrieved with the BuildManager.AppResourcesAssembly
property.
My solution works with reflection as I didn't find any existing public entry point in the .NET API to do this.
protected virtual Assembly AppResourcesAssembly {
get {
Type buildManagerType = typeof(BuildManager);
PropertyInfo property = buildManagerType.GetProperty("AppResourcesAssembly",
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.GetField);
return (Assembly)property.GetValue(null, null);
}
}
For global resources, every resx file is embedded as a resource with the AppResourcesAssembly
with a "Resources.
" prefix. The access to the ResourceManager
is very easy once you have access to the AppResourcesAssembly
.
protected override ResourceManager ResourceManager {
get {
string baseName = "Resources." + this._classKey;
ResourceManager manager = new ResourceManager(baseName, AppResourcesAssembly);
manager.IgnoreCase = true;
return manager;
}
}
Build a Local Resource Provider
The solution I implemented also uses a lot of reflection because of the actual lack of entry point in the .NET API to do this operation.
protected string VirtualPathFileName {
get {
PropertyInfo fileNameProperty = _virtualPathType.GetProperty("FileName");
return (string)fileNameProperty.GetValue(_virtualPathInstance, null);
}
}
protected object VirtualPathParent {
get {
PropertyInfo parentProperty = _virtualPathType.GetProperty("Parent");
return parentProperty.GetValue(_virtualPathInstance, null);
}
}
protected override ResourceManager ResourceManager {
get {
Assembly localResourceAssembly = LocalResourceAssembly;
if (localResourceAssembly == null) {
throw new InvalidOperationException
("Unable to find the matching resource file.");
}
ResourceManager manager = new ResourceManager
(VirtualPathFileName, localResourceAssembly);
manager.IgnoreCase = true;
return manager;
}
protected virtual Assembly LocalResourceAssembly {
get {
string localResourceAssemblyName =
(string)_methodGetLocalResourcesAssemblyName.Invoke(null, new object[]
{ VirtualPathParent });
object buildResultFromCache = _methodGetBuildResultFromCache.Invoke
(null, new object[] { localResourceAssemblyName });
if (buildResultFromCache != null) {
return (Assembly)_propertyResultAssembly.GetValue(buildResultFromCache, null);
}
return null;
}
}
Web Application Configuration
As usual, in ASP.NET, the web.config file allows you to modify the ResourceProviderFactoryType
used for globalization.
<configuration>
<system.web>
<globalization resourceProviderFactoryType=
"MyProject.Resources.CulturePrefixedResourceProviderFactory"
culture="fr-FR"
uiCulture="fr-FR" />
</system.web>
</configuration>
What's Next?
As you can see, creating custom resource providers for ASP.NET applications requires a little work. I use this tool during the testing of an internationalized application in order to ensure that every label is correctly filled within a resx file.
Another very classical resource provider implementation allows the resources to be retrieved from a database.
History
- 27th August, 2010: Initial post