Tip

Localization practices for .NET 2.0: It's still about the architecture

In the interest of localization, there are plenty of details to deliberate to insure that presentation and formatting are accurate, that stored data can be retrieved for the correct culture, and that transmission of data over the wire retains integrity when processed by another system. Formatting dates, times and currency; sorting and comparing strings; storing values in transient, persistent or other storage locations; and encoding formats for wire transmissions are all important to the localization of an application.

Fortunately, the .NET Framework 2.0provides built-in support for most of these requirements, reducing the overall amount of work required to take care of the nasty details. As a result, spending time on the more granular requirements for localization is not worth your efforts until you have a well-thought-out architecture to support building and maintaining a localized application.

In this article, I'll discuss approaches to .NET 2.0 application architecture to support localizable WinForms and ASP.NET Web applications. Specifically, I'll talk about how to allocate and leverage resources and satellite assemblies, discuss versioning and deployment models, and spend some time on architectural considerations specific to ASP.NET.

The primary concern for localization of course is what to do with content. Localized content has to have a home, somewhere. A Windows application typically has several types of content that require localization, namely: control properties such as labels, button and menu captions; interactive text such as error messages and other user prompts; and other dynamic content which may be stored in a database.

Resource Allocation

XML resources are a viable storage medium for control properties and messages, and can also be useful for other forms of static content, including small graphics and non-translated resources such as configuration settings. In fact, you can use the following table as a guideline for how a typical Windows application can categorize resources:

Resource Type

Contents

Deployment

Form Content

(Form*.resx)

Localizable property settings for the form and its controls. Automatically generated through the IDE, one per form.

Application Assembly

Glossary Items

(GlossaryRes.resx)

Common terms for the application that may be shared among many forms, components and even several applications.

Shared Resource-Only Assembly

Error and String Messages

(ErrorsRes.resx, StringsRes.resx)

Standard and possibly reusable messages for common errors and interactive user dialog.

Shared Resource-Only Assembly

Other Content

(ContentRes.resx)

Content not form-specific, not common to many components, but should isolated from form-specific resources for reuse and maintainability.

Application Assembly

Configuration Settings

(ConfigRes.resx)

Content such as file names and database table names that are not part of localization kit sent to translators, rather, are edited by developers.

Application Assembly

Form content, configuration settings and other content resources are likely to be embedded in the application assembly or component to which they belong. Glossary, error message and other common string messages are examples of resources likely to be shared among several components and assemblies in the application, or possibly multiple applications. These should therefore be allocated into separate resource-only assemblies. In some cases this might mean a single common component that encapsulates all shared XML resources.

Figure 1 illustrates a possible architecture for a Windows application that relies on embedded resources and shared resources to draw content.

Figure 1: GlobalApp.exe is the main application assembly. CommonRes.dll is a resource-only assembly with shared resources. Both contain default resources. Satellite assemblies for each are resource-only assemblies that may or may not duplicate all resources from the main assembly. Ultimately, any resource entries not found in the desired culture will fall back to the main assembly.

In Figure 2, the same application is shown along with the same satellite assemblies for the main application assembly. In addition, a localized database also feeds the application. Configuration resources (ConfigRes) in this example would be responsible for indicating the correct database table from which dynamic content should be drawn.

Figure 2: Resources should be used to redirect access to localized database content.

The distribution of content between resources and the database is also an important driver of the application architecture. Automatically generated resources, error and string messages, small footprint glossary items and configuration information are generally stored in resources. Application content that feeds the user interface and includes data specific to the purpose of the application would be stored in the application database. It is still important to leverage the resource fallback process -- not only for selecting the correct resources, but also for selecting the correct database values. This can be handled by storing database or table names in resource entries that will change for different cultures. For example, Figure 2 shows localized table names for Spanish (products_es) and Italian (products_it).

NOTE: Designing the database structure to store data for a localized application is also a task with many influencing factors. Field formats (i.e., nvarchar, nchar); table, column, or other object collation settings, and more must be considered as the database is designed. This will impact how connection strings and queries are built to access localized data - therefore what is stored in resources to help in the process.

Strongly-Typed Resources

Consistent with today's practices, Visual Studio 2005 makes it really easy to generate resources for form and control properties. Code is also generated to map those resources to properties at runtime, from a single line of code:

 System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1)); this.button1 = new System.Windows.Forms.Button(); resources.ApplyResources(this.button1, "button1");

In this example, ApplyResources() sets the Button control's properties using reflection, by looking for properties that match the resource key name (i.e., button1.Text).

Adding additional XML resources to a project and entering resource values is now easier with Visual Studio 2005 thanks to a new Managed Resource Editor and strongly-typed resources. Strongly-typed resources make your development experience much easier since you can access resources through a global Resources object, benefiting from Intellisense to select values:

Thanks to strongly-typed resources, you no longer have to manually instantiate a ResourceManager for each resource type. In the .NET Framework 1.1, managing resource lifetime meant writing the following code:

 System.Resources.ResourceManager m_contentRes = new System.Resources.ResourceManager("ColorPicker.ContentRes", System.Reflection.Assembly.GetExecutingAssembly()); DialogResult result = MessageBox.Show(m_contentRes.GetString("quitapplication"), m_contentRes.GetString("quit"), MessageBoxButtons.YesNo);

You instantiate your own ResourceManager and control its lifetime. Accessing values requires you to supply the key, and they are not necessarily strongly typed.

In the .NET Framework 2.0, this code is reduced to the following:

 DialogResult result = MessageBox.Show(content.quitapplication, content.quit, MessageBoxButtons.YesNo);

When you generate resources in the .NET Framework 2.0, an internal class is generated that instantiates the ResourceManager on first access, scoping its lifetime to the lifetime of the component by default. Listing 1 shows an example of this class.

 namespace Common { [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class ErrorsRes { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute ("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] public ErrorsRes() { } [global::System.ComponentModel.EditorBrowsableAttribute (global::System.ComponentModel.EditorBrowsableState.Advanced)] public static global::System.Resources.ResourceManager ResourceManager { get { if ((resourceMan == null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Common.ErrorsRes", typeof(ErrorsRes).Assembly); resourceMan = temp; } return resourceMan; } } [global::System.ComponentModel.EditorBrowsableAttribute (global::System.ComponentModel.EditorBrowsableState.Advanced)] public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } set { resourceCulture = value; } } public static string InvalidColor { get { return ResourceManager.GetString("InvalidColor", resourceCulture); } } } }

Listing 1: A static class is generated for all strongly-typed resources.

This class and its properties must be made public in shared, resource-only assemblies, in order for them to be accessible to other components as shown in Figure 2.

Resources and Translation

Regardless of the ease of generating resources for an application, there are other practical concerns. For example, in an application with many forms, and many components, how are those resources managed through the translation process? The allocation of one resource per form is often a source of contention for this reason.

When I am approached on this subject I ask people to consider the alternative. For example, let's say that per-form resources aren't used. That means that a central resource to store form and control property values must be created and maintained by developers, as opposed to direct IDE integration. This may simplify communications with the translator, reducing the number of files to track and merge, but at the expense of developer productivity. And, for the same reduced set of resources, you will still have to create a process to manage them through the translation process, as developers make changes, while translators translate.

Managing resources during the translation process is not a cake-walk. Strict process must be in place with a dedicated project manager overseeing it. Translators are likely to work on resources while developers modify the application, and possibly add, remove or change values. Tools must be used to compare resources and perform a merge before sending revisions to the translator. Most sophisticated translators will have their own tools to facilitate this process.

In the absence of this, since resources are XML-based, custom tools can be written tailored to the needs of your development process. In favor of developer productivity, I am a proponent of manipulating resources to support translation process, rather than change the way developers work day to day.

Selecting Runtime Culture

After allocating content to resources and database storage, it's time to consider how that content will be selected based on the user's culture preference. Your application installation process can provide a way for users to select their culture however, providing configurability post-install makes the application more flexible. Other considerations include the storage of culture preference per-user, and how to manage culture settings with multithreaded applications.

Configuring Culture Settings

Each thread has two properties that affect runtime culture. The UICulture property affects the selection of resources at runtime, and the Culture property influences how currency, dates and times, and other formatting is applied. Although you may allow your application to accept the default UICulture and Culture (based on the machine's Windows installation and Control Panel settings) users must also be able to select these settings independently.

If you provide application settings for UICulture and Culture in the .NET Framework 2.0, a strongly-typed class is generated to simplify the code to access those settings. Typically you'll separate UICulture and Culture, since the former deals with content, and the latter with formatting. At times a user may prefer to work in English, for example, while still viewing currency and date based on another culture. The settings shown in Figure 3 can be used to store an application-wide preference.

Figure 3: Application settings are now configured through the IDE, which generates XML settings in the application configuration file (app.config for Windows applications, web.config for Web).

.NET Framework 2.0 settings can be application or user-based. User-based settings, however, can become quickly difficult to administer in the application configuration file, particularly since you may have a growing list of users for the application. I therefore recommend using application-based settings or creating a database for user profile settings.

Runtime Culture

The main thread's UICulture must be set to the appropriate value prior to initializing form and control properties with a ResourceManager instance. Typically you'll see this handled in the main form's constructor:

 public Form1() { System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(Properties.Settings.Default.UICulture); System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.CreateSpecificCulture (Properties.Settings.Default.Culture); InitializeComponent(); }

These settings are per-thread therefore other forms in the project need not repeat this step. What happens if a different form is selected through application configuration, to be the startup form? It would be much better to initialize the thread's UICulture and Culture when the thread is first created, and decouple it from a specific form's code. This can be handled in the Main() entry point:

 static void Main() { Application.EnableVisualStyles(); System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo (Properties.Settings.Default.UICulture); System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.CreateSpecificCulture (Properties.Settings.Default.Culture); Application.Run(new Form1()); }

This will work no matter what form is specified as the start up form. Furthermore, each form's initialization will use these settings, unless they are spun on another thread. For each subsequent thread created in the application, you'll need to pass along these values manually since they will not automatically adopt settings of the parent thread:

 Thread t = new Thread(TheThread); t.CurrentCulture=Thread.CurrentThread.CurrentCulture; t.CurrentUICulture=Thread.CurrentThread.CurrentUICulture; t.IsBackground=false; t.Start();

Resources and Assembly Versioning

Here's where we get into the real challenge: deploying and versioning application and resource assemblies ... There are a few specific items to consider when it comes to satellite assemblies:

  • Updating application assemblies
  • Updating satellite assemblies
  • Adding language support

Updating Application Assemblies

When an application and its associated resources assemblies are first deployed, their assembly version numbers should match up. But, if the application assembly is subsequently updated and versioned, you may not wish to redeploy satellite assemblies if no changes were made to affect resources. The default behavior of the runtime will expect satellite assembly versions to match their associated parent assembly version.

When satellite resources are missing, the ResourceManager will use resource fallback to find the closest matching set of resources. Ultimately, it falls back to the main assembly's embedded resource. If no matching resource entry can be found an ugly exception occurs, so you want to make sure the main assembly has a default set of resources for all entries to avoid this. Usually, the main assembly holds the English resources for the application, so you can optimize the search for English language resources by specifying the neutral culture of the assembly:

 [assembly: System.Resources.NeutralResourcesLanguage("en")]

There is clear distinction between missing satellite resources (indicating an unsupported culture), and satellite resources whose version numbers do not match the main assembly version. If you version the main assembly, without versioning its associated satellite assemblies, an exception will occur if the assemblies are strongly named. And, by the way, strongly named applications by definition must also use strongly named satellite assemblies. You can apply the SatelliteContractVersionAttribute to the main assembly avoid this problem:

 [assembly: System.Resources.SatelliteContractVersion("1.0.0.0")]

By applying this attribute, you can deploy updates to the main assembly without modifying satellite assemblies.

Updating Satellite Assemblies

What if you need to update satellite assemblies after they have been deployed? If they are updated in conjunction with an application update, in all likelihood all assemblies including satellite assemblies will carry a new version number. If a satellite assembly is singularly updated due to a translation error, things get a little tricky. You can't version the satellite assembly because its version must match that of the application assembly or the SatelliteContractVersionAttribute. One approach is to increment the AssemblyFileVersionAttribute, leaving the AssemblyVersionAttribute to its original value:

 [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.1.0.0")]

This makes it possible to detect which version is deployed. In the .NET Framework 2.0, setting file version apart from assembly version is handled by attributes set through the property configuration as shown in Figure 4.

Figure 4: The Assembly Information dialog is new to the .NET Framework 2.0, making it easier to update the AssemblyInfo.* files in your project, without hunting down attribute syntax.

When a new major release of the application is deployed, the assembly, file and satellite contract version settings can be re-unified since all assembles are likely to be deployed as a unit.

Adding Language Support

Deploying additional satellite assemblies in the .NET Framework 2.0 works the same as with .NET 1.1: compile the satellite assembly then deploy it to a subdirectory named for its culture code. Once again, for strongly named assemblies version numbers are important. It must match the SatelliteContractVersionAttribute, or the deployed assembly's version number.

From a process perspective, if you are deploying a new application assembly with additional resource support, by definition the new satellite assembly will be compiled with the same version number as the application assembly through Visual Studio. Other, previously deployed satellite assemblies should still work so long as their versions match the SatelliteContractVersionAttribute. But, when deploying new satellite assemblies without deploying an update to the application assembly, it would be best to deploy the satellite assembly with the same version as this contract version. This will help you to avoid problems when subsequent application assembly updates are deployed.

Resources and ASP.NET

Traditionally resources have not been a staple for Web applications. A popular approach to translating Web content has been to duplicate the site and place translated content in a subdirectory named by the appropriate culture code. Figure 5 illustrates this localization architecture.

Figure 5: For the most part, duplicate content sites mean a copy of page content, images and other file dependencies -- so that relative paths will work reducing the overhead of changes necessary. In some cases, shared image directories may have few files to translate, therefore store copies of images as localized filenames.

The main reason that this architecture is common stems from the fact that most sites are initially released in English, without consideration for future localization. When the topic of localization comes up, the options are a) redesign the application to use a single page and code-base, or b) duplicate the site and have translators edit pages directly. The prospect of re-architecture is often overwhelming in times of urgency, therefore option b) wins. But, there are pitfalls to the site duplication approach to localization. Duplication of pages and related code can lead to errors and inconsistencies. Worse, page redesign must be applied to several copies of the site, a time consuming and also error prone task.

Developing a single page and code base for ASP.NET 1.1 meant manually creating resources for lightweight page content, and building strategies for storing and dynamically retrieving larger blocks of HTML content. Creating resources for each page increased the development and maintenance burden since there were no tools to automate the process or manage the ResourceManager lifecycle for each request. Instead those tackling this architecture would generate a few resources grouped into categories such as GlossaryRes, ErrorsRes, ConfigRes to store lightweight content (< 100 characters) and leveraged the database for other dynamic and static application content. Figure 6 illustrates single code-base architecture for ASP.NET.

Figure 6: Once again, resources should be used to direct code to the correct localized files and database content. This illustrates an ASP.NET 1.1 deployment. For ASP.NET 2.0, .resx files may be deployed as source, or pre-compiled which means that filenames will change although the logical view is much the same with respect to resources and satellite assemblies.

Like with WinForms applications, configuration resources can be leveraged in Web applications to direct requests to pull data from the appropriate database or table. One of the requirements of this architecture for .NET 1.1 is that the division of content between resources, database and other storage is carefully contemplated along with the usage pattern of the information. The reason being that although resources provide a useful resource fallback process for selecting menu titles, messages and other page content, the effort required in coding for the ResourceManager lifecycle, in addition to creating and maintaining resources manually, represents a careful trade-off.

This story is much better with the .NET Framework 2.0.

Resource Allocation Redux

With ASP.NET 2.0 local page resources are automatically generated for content pages, master pages, and user controls. Even better, the code to instantiate the ResourceManager is also generated from new localization expressions. Global resources can also be created and shared among pages, such as glossary items. These global resources become strongly-typed resources that share in the Intellisense benefits discussed earlier.

NOTE: For more information about the process of generating local page resources, working with localization expressions, and strongly-typed global resources for ASP.NET 2.0, see my MSDN article, ASP.NET 2.0 Localization Features: A Fresh Approach to Localizing Web Applications.

Building single page and code-base sites with ASP.NET 2.0 is the more natural approach to Web application development. It means more resources will be created, since they are page-based and automatically generated, but there is also a built-in re-use model through the use of master pages and user controls. A typical ASP.NET 2.0 application will likely break resources into the following categories:

Resource Type

Contents

Deployment

Content Page, Master Page and User Control Content

(*.aspx.resx, *.master.resx, *.ascx.resx)

Localizable property settings for each page or control and its server controls. Localizable blocks of static content.

Deployed with Web Application

Glossary Items

(GlossaryRes.resx)

Controlled terms for the application that may be shared among many pages.

Deployed with Web Application

Error and String Messages

(ErrorsRes.resx, StringsRes.resx)

Standard and possibly reusable messages for common errors and interactive user dialog.

Deployed with Web Application

Other Content

(ContentRes.resx)

Content not page-specific that should isolated from page-specific resources for reuse and maintainability.

Deployed with Web Application

Configuration Settings

(ConfigRes.resx)

Content such as file names and database table names that are not part of localization kit sent to translators, rather, are edited by developers.

Deployed with Web application

Supporting backward compatibility with .NET 1.1 resources, or deploying separate resource-only components to share resources across applications is also possible with ASP.NET 2.0, with a few caveats:

  • Code to manage the ResourceManager lifecycle per request must be written in the traditional ASP.NET 1.1 way (unless you write a custom resource provider to handle it)
  • Localization expressions do not support 1.1 resources by default (but you can create custom localization expressions)
  • Only ASP.NET 2.0 global resources are strongly typed, supporting Intellisense

ResourceManager Lifecycle

Like with Windows applications in the .NET Framework 2.0, the lifecycle of the ResourceManager pointing to local page and global resources is hidden from view.

Localization expressions like this implicit expression (meta:resourcekey):

 <asp:Label ID="lblDate" Runat="server" Font-Size="14pt" meta:resourcekey="lblDateResource1" ></asp:Label>

generate code like this to access resources each time a page is loaded:

 public void Page_Load(object sender, EventArgs e) { lblDate.Text = ((string) base.GetLocalResourceObject("lblDateResource1.Text")); }

GetLocalResourceString() is responsible for finding the cached resource provider or creating it, and subsequently using (possibly cached) ResourceManager for this page to call GetResourceObject(). The ResourceManager will retrieve resources based on the executing thread's UICulture setting. Once loaded into the application domain, satellite assemblies remain in memory, readily accessible to the ResourceManager instance.

Global resources can be used through a strongly typed class, or by applying localization expressions like this:

 <asp:ImageButton ID="btnIDesign" Runat="server" ImageUrl="~/Images/idesignlogo.jpg" AlternateText='<%$ Resources:Glossary, MissionStatement%>' PostBackUrl="http://www.idesign.net"; meta:resourcekey="ImageButtonResource1" />

Code is automatically generated from this to retrieve the global resource provider, through GetGlobalResourceObject(), and subsequently find the corresponding ResourceManager to retrieve the localized value.

Cached ResourceManager objects may be accessed by more than one request thread concurrently. The default configuration of ASP.NET is usually to support 25-30 threads per process, but this can be dynamically boosted to handle request bursts, so it is important to consider thread safety alongside performance. ResourceManager types are designed to be thread-safe, but this can cause minor cost in performance per request if a caching mechanism is not also used for the application, to reduce the number of requests that require access to resources.

ASP.NET Deployment Architecture

Figure 6 illustrated the physical view of an ASP.NET 1.1 deployment. The logical view of an ASP.NET 2.0 deployment looks much the same, and can still leverage local and global resources, database content, and localized user controls for complex HTML. I refer to this as "logical" deployment because the physical deployment model can vary. You can pre-compile the entire site into assemblies, or deploy source, including .resx files and let the JIT compiler generate assemblies on the fly. The former is the preferred choice for enterprise sites, since it better protects your intellectual property and makes version control possible.

Regardless of compilation model, the resulting compiled code will consist of default resources deployed in a default resource assembly, and satellite assemblies that contain localized versions of those resources. The resource fallback process and other rules discussed earlier for Windows Forms still apply to ASP.NET deployments, except that the application assembly cannot be strongly named, therefore deploying updates to resources for the application will not cause a version conflict at runtime.

Other assemblies deployed with the ASP.NET application as dependencies should be strongly named, and follow the rules discussed earlier to manage versioning and deployment updates. There are some challenges to this process that I discuss in my article titled Sandboxing Components for Impersonation.

Selecting Culture for ASP.NET Requests

Runtime culture selection for ASP.NET is not handled in the same way as with Window Forms applications. Primarily because each request executes on its own thread, and therefore must somehow be informed of the user's preferred culture. With ASP.NET 2.0, automatic culture selection is possible based on Web browser preferences and HTTP language headers. I also described this in greater detail in my MSDN article mentioned earlier. But, other features of ASP.NET can also be leveraged to store user preferences separate from browser settings. In fact, there are a few patterns you can consider for managing the user's preferences.

  • Select the preferred culture from browser settings
  • Allow users to select culture from the user interface
  • Provide users with a registration process or persistent profile where they can select their preferred culture
  • Persist preferred culture for the session, or for longer duration using cookies

With ASP.NET 2.0, you can now very easily select the preferred culture from browser settings. The web.config <globalization> section sets this preference application-wide, so that all page requests will be executed on a thread that has been initialized for the correct UICulture and Culture:

 <globalization culture="auto" uiCulture="auto" fileEncoding="utf-8" requestEncoding="utf-8" responseEncoding="utf-8" />

You can also configure this on a per-page basis as follows:

 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" Culture="auto" UICulture="auto" meta:resourcekey="PageResource1" %>

NOTE: Beware of this in Beta 2. The @Page directive defaults to setting Culture and UICulture parameters, but if you write some code to override this based on a user profile, you must remove these settings or they get in the way. See my blog post, Gotcha! Beware of Page-Level Culture Settings, for more on this.

There are some interesting implementation challenges with manually setting the request thread to reflect culture preferences persisted for the user. First of all, storing user culture preferences in the Session object is virtually useless. Primarily because it is transient, but also because you may not have access to the session object at the time you would like to have access to the user's preferences. For example, cached pages are accessed prior to establishing a reference to the Session, yet we'd like to retrieve the page cached for the user's culture, right? That implies we are able to set the thread's UICulture property correctly first!

If a transient profile is all that is required, you can use cookies to store the user's preference. By the time the HttpApplication.BeginRequest event is fired during the request lifecycle, the browser has already initialized the Culture and UICulture based on the browser settings. At this point in the lifecycle we can already update the thread's culture settings with the persistent cookie preference:

 void Application_BeginRequest(Object sender, EventArgs e) { // create culture preference cookies if they don't exist yet // otherwise set current thread to value of those cookies HttpCookie uiCulture = this.Request.Cookies["uiCulture"] as HttpCookie; HttpCookie culture = this.Request.Cookies["culture"] as HttpCookie; if (uiCulture != null && uiCulture.Value != "") { System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(uiCulture.Value); } if (culture != null && culture.Value != "") { System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo. CreateSpecificCulture(culture.Value); }

The cookie would be created elsewhere in the application where culture selections are made:

 this.Response.Cookies.Add(new HttpCookie("uiCulture", System.Threading.Thread.CurrentThread.CurrentUICulture.Name)); this.Response.Cookies.Add(new HttpCookie("culture", System.Threading.Thread.CurrentThread.CurrentUICulture.Name));

But cookies aren't a professional approach to storing user preferences. A database profile for the user is more appropriate. Fortunately, with ASP.NET, a strongly-typed mechanism for storing profile data is available. For example, this web.config setting creates a profile that stores UICulture and Culture settings:

 <profile> <properties> <add name="UICulture" allowAnonymous="true" /> <add name="Culture" allowAnonymous="true" /> </properties> </profile>

These settings are accessible through the type-safe Profile object:

 Profile.Culture = "it"; Profile.UICulture = "it";

Using profile, we are still faced with a similar challenge as with the Session object: when can we access the Profile for the user during request processing? Unfortunately the Profile is not initialized until the session is acquired, which means we cannot access it through the object model while the cache is inspected. In order to overcome this, the data store where user settings are persisted must be accessed using custom code.

Caching Strategies for Localization

Caching is critical to Web application performance...'nuff said. So, by definition when we have a localized site, any support for caching must always consider the possibility of different cultures.

At minimum, you want to make sure your pages are cached by culture, using a VaryByCustom parameter as follows:

 <% @Outputcache Duration=86400 VaryByParam=None VaryByCustom="uiCulture" %>

This requires that you override the HttpApplication object's GetVaryByCustomString() method as follows:

 public override string GetVaryByCustomString(HttpContext context, string custom) { if (string.Compare(custom, "uiCulture", true, System.Globalization.CultureInfo.InvariantCulture) == 0 && System.Threading.Thread.CurrentThread.CurrentUICulture != null) { return System.Threading.Thread.CurrentThread.CurrentUICulture.Name; } else return base.GetVaryByCustomString(context, custom); }

This requires the thread to reflect the correct UICulture, as discussed in the previous section.

NOTE: If you want to cache by multiple custom values, for example browser and uiCulture, you must treat the VaryByCustom parameter like a key and supply the variations you would like to support (i.e., "browser;uiCulture", "uiCulture") and handle each of those separately in the GetVaryByCustomString() override.

Conclusion

After considering the concepts discussed in this article, and reading up on the resources I referred to throughout and in the resources section, you should be well equipped to form a localization strategy around your .NET Framework 2.0 applications. Just remember, consider architecture first, because many of the granular details take care of themselves thanks to the tools provided us with .NET.

About the author

Michele Leroux Bustamanteis a Chief Architect with IDesign, Microsoft Regional Director for San Diego, Microsoft MVP for Web Services and a BEA Technical Director. In addition, Michele is a member of the board of directors for the International Association of Software Architects (IASA). At IDesign Michele provides high-end architecture consulting services, training and mentoring. Her specialties include architecture design for robust, scalable and secure .NET architecture; localization; Web applications and services; and interoperability between .NET and Java platforms. Michele is a member of the INETA; a frequent conference presenter at major technology conferences such as Tech Ed, PDC, SD and Dev Connections; conference chair for SD's Web Services track; and a regularly published author. Michele's next book is Windows Communication Framework Jumpstart for O'Reilly, due out in early 2006. Reach her at www.idesign.net or visit her blog at www.dasblonde.net.


This was first published in December 2007

There are Comments. Add yours.

 
TIP: Want to include a code block in your comment? Use <pre> or <code> tags around the desired text. Ex: <code>insert code</code>

REGISTER or login:

Forgot Password?
By submitting you agree to receive email from TechTarget and its partners. If you reside outside of the United States, you consent to having your personal data transferred to and processed in the United States. Privacy
Sort by: OldestNewest

Forgot Password?

No problem! Submit your e-mail address below. We'll send you an email containing your password.

Your password has been sent to:

Disclaimer: Our Tips Exchange is a forum for you to share technical advice and expertise with your peers and to learn from other enterprise IT professionals. TechTarget provides the infrastructure to facilitate this sharing of information. However, we cannot guarantee the accuracy or validity of the material submitted. You agree that your use of the Ask The Expert services and your reliance on any questions, answers, information or other materials received through this Web site is at your own risk.