Resources and Localization
Code download available at:BasicInstincts05.exe(146 KB)
Embedding a Resource
Working with Resource Files
Resource Files in Visual Studio 2005
The Project-Level Resource File
Culture Settings and Localization
Loading Localized Resources
Localizing Form and Control Settings
There are two ways you can utilize resources such as strings, images, and text-based files from your Microsoft® .NET Framework-based application. You can embed them directly in the app or you can load them from an external file. When you choose to load from an external source rather than from an embedding resource, you must distribute the files along with the assembly. You must also make sure that code inside the application can determine the correct path and load the resource files at run time. This approach causes problems if the .exe ever becomes separated from the files it depends upon.
Taking the embedded option and compiling the resources you need directly into the assemblies that use them makes distribution more reliable and less error prone. This month I will discuss the use of resources, how and why you might embed a resource, and the role of resource files in .NET.
Embedding a Resource
Let's start with a simple example to see how embedding is accomplished. Assume you would like to embed a graphic image named LitwareLogo.png into your Windows® Forms-based application. You begin by adding the file to the Visual Studio® project. Then, from within the property sheet for the file, you set the Build Action to Embedded Resource, as shown in Figure 1. By doing this, you have instructed Visual Studio to embed the file into the physical image of the output assembly .exe file.
Figure 1** Set Build Action **
Once you have embedded a file as a resource, you must learn how to access it at run time. Examine the following code fragment, which obtains a reference to the current assembly object and then calls the GetManifestResourceStream method to acquire stream-based access to the embedded resource file. This code assumes that you have imported the System.Reflection namespace and the System.IO namespace:
'*** get current Assembly object. Dim asm As Assembly = Assembly.GetExecutingAssembly() '*** load embedded resource into stream Dim ResourceName As String = "LitwareSmartClient.LitwareLogo.png" Dim str As Stream = asm.GetManifestResourceStream(ResourceName) '*** convert stream into image and load in '*** picture box Dim img As Image = Image.FromStream(str) PictureBox1.Image = img
As you can see, an Assembly object exposes the GetManifestResourceStream method, which allows you to pass a string name that identifies the embedded resource. Note that the resource name is case-sensitive even when you are using a case-insensitive language like Visual Basic®. In the example, the code calls the Image.FromStream method to convert the stream containing the image file into an Image object that can be loaded into a PictureBox control.
Figure 2 Load Resources
'*** get current Assembly object. Dim asm As Assembly = Assembly.GetExecutingAssembly() '*** load embedded SQL resources file Dim SqlResourceName As String = "LitwareSmartClient.GetProducts.sql" Dim strSQL As Stream = asm.GetManifestResourceStream(SqlResourceName) Dim reader As New StreamReader(strSQL) Dim sql As String = reader.ReadToEnd reader.Close() '*** load embedded XML resources file Dim XmlResourceName As String = "LitwareSmartClient.Customers.xml" Dim strXML As Stream = asm.GetManifestResourceStream(XmlResourceName) Dim xmlDoc As New XmlDocument() xmlDoc.Load(strXML) strXML.Close()
The techniques you have just seen involve embedding resource files directly into an assembly and loading them using the GetManifestResourceStream method supplied by the Assembly class. But there is another alternative in .NET, resource files, which can make it even easier to handle resources in many scenarios. Plus, as you will see, Visual Studio 2005 provides some conveniences when it comes to working with resources and localizing apps.
Working with Resource Files
In .NET, resource files can be used to embed resources into assemblies. One of the key benefits of using resource files is that all the language and locale-specific elements in an application or class library DLL such as captions and user messages can be factored out of your application code. To do this you need to create a separate resource file for each spoken language that you need to support. The actual resource file is a text-based file containing XML with a .resx extension. Figure 3 shows a skimmed-down example of the XML data found inside a resource file.
Figure 3 Inside an XML Resource File
<root> <data name="MainFormCaption"> <value>Litware Customer Manager</value> </data> <data name="UserWelcome"> <value>Good day</value> </data> <data name="ErrorMessage1"> <value>Oh no, Something went wrong!</value> </data> </root>
While the XML fragment in the figure isn't a complete resource file, it gives you a general idea of what's inside one. You can compile a resource file into a binary image using a .NET-based utility called the resource file generator (Resgen.exe). Compiled resource files typically have a .resources extension. For example, a developer at the company named Litware can create a resource file named LitwareStrings.resx and compile it into a binary image named LitwareStrings.resources by executing the following command within a batch file or from the Visual Studio 2005 Command Prompt:
RESGEN.EXE LitwareStrings.resx LitwareStrings.resources
After you compile a text-based .resx file into a binary .resource file, it is still not yet ready to use. Instead, you must further compile this binary image into a .NET assembly before you can use it from an application. This can be accomplished using another .NET tool called the Assembly Linker (Al.exe). For example, to compile LitwareStrings.resources into its own assembly DLL, you can run the following command-line instruction from a batch file or from the Visual Studio 2005 command prompt:
AL.EXE /t:library /out:LitwareStrings.resources.dll /link:LitwareStrings.resources
Once you have compiled a resource file into a .NET assembly, you can access the resources inside using the ResourceManager class that is defined in the System.Resources namespace. The following shows a simple example of code that accesses a string resource using the ResourceManager:
Dim asm As Assembly = Assembly.Load("LitwareStrings.resources") Dim rm As New System.Resources.ResourceManager("LitwareStrings", asm) Dim caption As String = rm.GetString("MainFormCaption")
Resgen.exe can also be used to generate a strongly typed resource class that exposes properties providing easy access to the resource inside. For example, to a generate a strongly typed resource class in Visual Basic, you can add the /str parameter and a value of "vb" to the command line when calling Resgen.exe:
RESGEN.EXE LitwareStrings.resx LitwareStrings.resources /str:vb
This command-line instruction generates a Visual Basic source file named LitwareStrings.vb. This source file contains a class named LitwareStrings. Inside the class, there is code that uses the ResourceManager to implement strongly typed properties that look like this:
Shared ReadOnly Property MainFormCaption() As String Get Return ResourceManager.GetString("MainFormCaption", resourceCulture) End Get End Property
I have just quickly stepped through a high-level explanation of how resource files are compiled into assemblies and how they can be accessed using the ResourceManager class and strongly typed resource classes.This should give you a better idea of how the individual pieces fit together.
I don't want to spend any more time here on the low-level details of resource files because you will not be required to deal with them when you begin localizing applications and class library DLLs. That's because Visual Studio 2005 and Visual Basic supply many valuable conveniences behind the scenes. However, keep in mind that you might have to work directly with Resgen.exe, Al.exe, and some of the other .NET-based utilities and classes when you are localizing large-scale development projects.
Resource Files in Visual Studio 2005
Now I am going to focus on using resource files in a Windows Forms-based application. Most of the concepts I explain will apply to using resource files in a class library DLL as well. Visual Studio 2005 makes it easy to work with resource files by supplying a visual editor. You can create a new resource file using the Add New Item command and choosing Resources file, as shown in Figure 4.
Figure 4** New Item Template for Adding Resource Files **
Once you have added a resource file to a project, there's no need to work directly with the XML format required inside a .resx file. Instead, Visual Studio supplies the friendly visual resource designer that is shown in Figure 5. This resource file designer makes it easy to add and maintain strings as well as other types of file-based resources such as graphics and XML documents.
Figure 5** Visual Studio Resource Editor **
When you compile a project that contains a resource file, Visual Studio compiles this .resx file into a .resources file and then links it inside the physical image of the resulting output assembly. That means that all the details involved in compiling the resource file and embedding it into the image of the target assembly are handled for you by Visual Studio.
Visual Studio also builds in a strongly typed resource class and exposes it in Visual Basic projects using the My namespace features introduced in Visual Basic 2005. That means you get the benefit of having the .NET ResourceManager class loading your resources, yet you never have to program directly against this class. For example, if you want to access resource strings you have added to LitwareStrings.resx, you can simply write the following code:
Sub Main_Load(sender As Object, e As EventArgs) Handles MyBase.Load Me.Text = _ My.Resources.LitwareStrings.MainFormCaption Me.lblUserWelcome.Text = _ My.Resources.LitwareStrings.UserWelcome End Sub
The Project-Level Resource File
While you can explicitly add one or more resource files to a Visual Studio project, that's often unnecessary because Visual Studio automatically includes a project-level resource file each time you create a new project. The project-level resource file can be accessed through the Project Properties dialog in the Visual Studio editor.
When you want to access resources such as strings from the project-level resource file, you can access them directly from within the My.Resources class:
Private Sub LoadResources() '*** load project-level resources Me.Text = My.Resources.MainFormCaption Me.lblWelcomeMessage.Text = My.Resources.UserWelcome End Sub
Me.picLogo.Image = My.Resources.LitwareLogo Dim xmlDoc As New Xml.XmlDocument xmlDoc.LoadXml(My.Resources.Customers)
You can observe that the strongly typed resource class automatically converts a.png file into an Image object that can be directly loaded into a PictureBox. The strongly typed resource class also converts the embedded XML file into a string that can be easily loaded into an XmlDocument object.
Culture Settings and Localization
It's a common requirement for software projects to be localized so they can be used by people who speak different languages. For example, imagine you are developing the Litware Customer Manager application with Visual Basic and Visual Studio 2005 and you are required to write localized versions of the application for users who speak English as well as users who speak French. Fortunately, the Microsoft .NET Framework and Visual Studio have many features geared toward localizing applications and class library DLLs.
When you first start designing and writing .NET-based software projects that need to support localization, you must quickly become familiar with the CultureInfo class defined inside the System.Globalization namespace. A CultureInfo object tracks a culture name that identifies a spoken language.
The culture name for English is "en" and the culture name for French is "fr". The culture name of a CultureInfo object can also carry additional information that identifies a particular region in the world such as "en-US" for US English, "en-GB" for British English, and "fr-BE" for Belgian French. Here's an example that creates and initializes CultureInfo objects with valid culture names:
Dim culture1 As CultureInfo = New CultureInfo("en-US") Dim culture2 As CultureInfo = New CultureInfo("en-GB") Dim culture3 As CultureInfo = New CultureInfo("fr") Dim culture4 As CultureInfo = New CultureInfo("fr-BE")
There are actually two CultureInfo objects associated with the current thread that require your attention. The first CultureInfo object listed represents the current culture while the second CultureInfo object represents the current UI culture. You can determine the culture names for each of these two CultureInfo objects with the following code:
'*** determine current culture and current UI culture Dim t As Thread = Thread.CurrentThread Dim currentCulture As CultureInfo = t.CurrentCulture Dim currentUICulture As CultureInfo = t.CurrentUICulture '*** display cultures in console Console.WriteLine("Current Culture: " & currentCulture.Name) Console.WriteLine("Current UI Culture: " & currentUICulture.Name)
The first CultureInfo object known as the CurrentCulture is not used to localize strings in applications. Instead, it affects how the .NET Framework formats dates, numbers, and currency. For example, if you modify the CurrentCulture object back and forth between en-US and en-GB, it can lead to strange side effects such as a single currency value switching back and forth between US dollars ($100.00) and British Pounds ($100.00). As you can see, switching the currency formatting on a currency value can lead to incorrect results. For this reason, it's a common practice to keep the CurrentCulture static even when localizing your application for use with different languages.
If you determine that you do need to programmatically change the CurrentCulture, you should remember that you must use a culture name that includes a regional identifier. You can use a culture name of en-US, en-GB, or fr-BE. However, you will receive run-time errors if you try to change the CurrentCulture so that it has a culture name of en or fr because there is no regional information and formatting requirements become too ambiguous.
The second CultureInfo object known as the CurrentUICulture is far more important to the discussion of .NET-based application localization. Modifications to the CurrentUICulture object associated with the current thread influence how the .NET Framework and the ResourceManager class load assemblies that contain embedded resources. In particular, it can have the effect of loading the set of resources that have been localized to the language preferred by the current user.
To begin localization, you make multiple copies of resource files-one copy of each resource file for each language you want to support. For example, start by taking the project-wide resource file named Resources.resx and making a copy. You can do this with a simple copy-and-paste operation right inside the Visual Studio Solution Explorer.
Once you have copied a resource file such as Resources.resx, rename the copied resource file by adding the culture name just before the .resx extension, as shown in Figure 6. For example, you should have resource files named Resources.fr.resx for generic French localized strings and another resource file named Resources.fr-BE.resx for French strings localized specifically for Belgium.
Figure 6** Separate Resource Files for Each Language **
When you compile the project with localized resource files like the one that is shown in Figure 6, Visual Studio does not compile all the resources into a single output assembly. Instead, it compiles each of these localized resource files into its own separate assembly. Each of these localized assemblies contains only resources and no code. This type of localized, resource-only assembly is known as a satellite assembly.
Each satellite assembly is associated with a master assembly known as the neutral assembly. The neutral assembly contains all the code and it loads whatever satellite assembly is necessary to get the localized resources required by the current user. In my example, LitwareSmartClient.exe is the neutral assembly which contains all the application code. Then there are several satellite assemblies associated with LitwareSmartClient.exe. Each of these has the same file name, LitwareSmartClient.resources.dll.
When satellite assemblies are deployed along with the neutral assembly in the AppBase directory, they must be deployed according to the rules of the .NET assembly loader. In particular,each satellite assembly must be deployed in a directory named after its localized culture name. For example, the AppBase directory that contains LitwareSmartClient.exe should contain a subdirectory named fr-BE which holds the satellite assembly localized for Belgian French named LitwareSmartClient.resources.dll. As long as these rules are followed, the .NET Framework assembly loader along with the assistance of the ResourceManager class will load the correct set of resources when required.
Fortunately, Visual Studio knows how to name the satellite assemblies correctly and how to deploy them in the correct directory structure expected by the .NET assembly loader. To gain a better understanding of how all of the various pieces fit together, you can simply compile your project and then examine the resulting structure of the AppBase directory and the subdirectories inside that hold the satellite assemblies.
Loading Localized Resources
Once you have created and edited all the required localized resource files and compiled your project, it's time to concentrate on how you are going to make your application load the correct set of localized strings that are preferred by the current user. One way you can accomplish this is to obtain a reference to the current thread and assign a new created CultureInfo object to the CurrentUICulture property. If you are writing a Windows Forms-based application in Visual Basic, you can also use the following code:
In the sample app that accompanies this column, I added support for the users to select their preferred language and to have the application track user language preferences when the application is shut down and restarted. While it is possible to maintain these sorts of user preferences using registry keys, Visual Studio 2005 makes it possible to avoid using the registry by adding an application setting that is tracked on a user-by-user basis. The sample app tracks a user-scoped application setting named UserLanguagePreference for this purpose. The application also contains an application startup event (see Figure 7).
Figure 7 Application Startup Event
'*** code in ApplicationEvents.vb Namespace My Partial Friend Class MyApplication Private Sub MyApplication_Startup(ByVal sender As Object, _ ByVal e As StartupEventArgs) Handles Me.Startup '*** initialize application by setting preferred language Dim lang As String = My.Settings.UserLanguagePreference My.Application.ChangeUICulture(lang) End Sub End Class End Namespace
The sample application also provides the user with a group of radio buttons as a means to switch from one language to another. Figure 8 shows a fragment of code from the event handler that responds to a user's request to change languages.
Figure 8 Language Preferences
'*** retrieve user's language preference Dim lang As String = CType(sender, Control).Tag '*** save user setting My.Settings.UserLanguagePreference = lang My.Settings.Save() '*** change application's UI culture My.Application.ChangeUICulture(My.Settings.UserLanguagePreference) '*** call custom method to reload localized strings LoadResources()
You have now seen all the fundamental code that allows the user to switch languages. The .NET Framework responds to the call to My.Application.ChangeUICulture by loading in the correct satellite assembly the next time the ResourceManager retrieves strings from the project-level resources. After the call to ChangeUICulture, the application can then requery the application-level resource strings and load them into controls on the form with the exact same code you saw earlier inside the custom LoadResources method:
Me.Text = My.Resources.MainFormCaption Me.lblWelcomeMessage.Text = My.Resources.UserWelcome
Note that the .NET assembly loader will first attempt to find an exact match with both language and region between the requested culture name and the culture name of an available satellite assembly. If the .NET assembly loader cannot find an exact match, it will then look for an available satellite assembly with a matching language. For example, if the requested language is fr-CA for Canadian French, the .NET Framework would first look for a satellite assembly with that language. If it cannot locate a satellite assembly with fr-CA, it then looks for a satellite assembly with a culture name of fr. If the .NET Framework cannot locate a satellite assembly with a culture name of fr, it then resorts to using the resources found within the neutral assembly, which has a set of default culture resources embedded inside. As you just saw, the .NET Framework can always fall back on the default culture resources from the neutral assembly if it cannot find a more specific satellite assembly.
When you compile the neutral assembly, you can mark it with a special attribute to inform the .NET Framework that the default culture resources are sufficient for those users who require a specific language. For example, you can add an assembly-level attribute to the AssemblyInfo.vb file of a Windows Forms-based application project, as shown in the following line of code:
As it is used here, the NeutralResourcesLanguage attribute informs the .NET assembly loader that it can use the default culture resources whenever the current user has requested that the application be localized for English.
Localizing Form and Control Settings
You have just seen how to localize string resources on a project-wide basis. This technique involves copying and maintaining localized resource files. Visual Studio 2005 provides some extra assistance when you need to localize strings specific to a form and the controls it contains.
Each form has a Localizable property that can be set to either true or false. If you set this property to true, as shown in Figure 9, Visual Studio 2005 will create and maintain a set of localized resource files for you behind the scenes.
When you set the Localizable property to true, the initial Language setting is default. As you add property values into the property sheet for the form and its controls, Visual Studio maintains them in a resource file behind the scenes that is compiled into the neutral assembly. When you change the Language property of the form to a specific language, such as "French (Belgium)," Visual Studio then creates a new localized resource file that will be compiled into a satellite assembly. Behind the scenes, things work exactly as they do with project-wide resources. It's just that Visual Studio eliminates your need to work directly with resource files and allows you to work with a standard property sheet as you add property values for things such as control Text properties.
Figure 9** Localizable Property **
Visual Studio is required to add some extra code into the form behind the scenes to support form localization. In particular, Visual Studio adds code to load the correct localized resources and assign their values to form and control properties. Instead of using the ResourceManager, the code generated by Visual Studio 2005 uses a more specialized class that derives from ResourceManager named ComponentResourceManager that is defined in the System.ComponentModel namespace.
When a localized form loads, everything required to load the localized resources is done for you by the code that is generated by Visual Studio. In particular, Visual Studio supplies the code to create an instance of the ComponentResourceManager, which loads the proper set of resources and assigns all the necessary control values.
However, if a form is already loaded and the user switches languages, you must supply some additional code to refresh the form with the resources of the requested language. The following shows a sample of code that uses the ComponentResourceManager to accomplish this goal:
Dim crm As ComponentResourceManager crm = New ComponentResourceManager(GetType(Main)) crm.ApplyResources(cmdAddCustomer, cmdAddCustomer.Name) crm.ApplyResources(mnuFile, mnuFile.Name) crm.ApplyResources(mnuFileAddCustomer, mnuFileAddCustomer.Name)
As you can see, you can create and initialize an instance of the ComponentResourceManager by passing type information about the form, which in this case is named Main.
The completed sample application, which demonstrates all the localization techniques discussed in this column, is shown in Figure 10. The application now supports US English, British English, and Belgian French. Also note that since there is a satellite assembly localized to fr without any specific region, the application also supports a generic form of French for users around the world who speak French.
Figure 10** Supporting Localized Versions **
If you want to add support for additional languages in the future, it's not very complicated. It's simply a matter of creating and maintaining additional resource files. In fact, you can add support for new languages without ever having to recompile the neutral assembly that contains all the application code. This is one of the most valuable features of the .NET Framework strategy for localizing applications and class library DLLs.
This month's column covered the fundamentals of resources and localization in the .NET Framework by working through how to localize a simple Windows Forms-based application. In my next Basic Instincts column, I will build upon what I covered this month and move on to a discussion of resource and localization in ASP.NET 2.0, which has some valuable and unique features for utilizing resources and localizing applications.
Send you questions and comments for Ted to firstname.lastname@example.org.
Ted Pattison is an author and trainer who delivers hands-on training through his company, the Ted Pattison Group. Ted is currently researching and writing a new book focusing on Windows SharePoint Services"V3" and Office 12 servers technology.