In addition to simplified access to typed resources, the ResourceManager class provides one other important feature: the ability to localize resources for your components without recompiling them. The act of localization (l10n [6] ) is a process of providing culture-specific information to display to a user in that culture. For example, a form has been localized when it shows "OK" in English but "Le OK" in French (or whatever the French actually say when they mean OK). The act of internationalization (i18n), on the other hand, is taking advantage of localized information. This could mean using localized resources in the UI or using code that formats currency or dates according to the current locale, as shown in Figure 10.13.
Figure 10.13. Localized Currencies and Dates
Culture InformationI generated the currencies and dates in Figure 10.13 by enumerating all the cultures that .NET knows about (centralized in the System.Globalization namespace) and using the information about each culture to provide formatting information: Imports System.Globalization ... Sub Form1_Load(sender As Object, e As EventArgs) Dim amount As Double = 4.52 Dim date As DateTime = DateTime.Now Dim info As CultureInfo For Each info In CultureInfo.GetCultures(CultureTypes.AllCultures) Dim item As ListViewItem = _ listView1.Items.Add(info.EnglishName) item.SubItems.Add(info.Name) If Not(info.IsNeutralCulture) Then item.SubItems.Add( _ amount.ToString("C", info.NumberFormat)) item.SubItems.Add( _ date.ToString("d", info.DateTimeFormat)) End If Next End Sub This code enumerates all known cultures, pulling out the name, the number formatting information, and the date formatting information; the latter two are passed to the ToString function to govern formatting. The intrinsic ToString implementations format strings by using the culture stored in the CurrentCulture property of the current thread (available via System.Threading.Thread.CurrentThread). The CurrentCulture property on the System.Windows.Forms.Application class is just a wrapper around the CurrentCulture property of the current thread, so either can be used to test your programs in alternative cultures: Shared Sub Main() Dim amount As Double = 4.52 ' Show currency using default culture MessageBox.Show(amount.ToString("C"), _ Application.CurrentCulture.EnglishName) ' Change current culture (one way) Application.CurrentCulture = New CultureInfo("fr-CA") ' Change current culture (another way) System.Threading.Thread.CurrentThread.CurrentCulture = _ New CultureInfo("fr-CA") ' Show currency in current culture (Canadian French) MessageBox.Show(amount.ToString("C"), _ Application.CurrentCulture.EnglishName) End Sub By default, the current culture is whatever users have set in their machines. Changing it requires an instance of the CultureInfo object, which is most easily constructed with a culture name. A culture name is composed of unique identifiers of a language and a country and is formatted this way: <twoLetterLanguageId>-<twoLetterCountryId> For example, U.S. English is "en-US," and Australian English is "en-AU." Resource ProbingThe ResourceManager class was written with internationalization and localization in mind. Each new ResourceManager uses the CurrentUICulture property [7] of the current thread to determine which culture's resources to load. When it's created, the resource manager probes the file system for an assembly that contains the appropriate culture-specific resources. Based on the namespace of the type it's loaded with, the resource manager looks in 16 places for an assembly, either DLL or EXE. First it looks for country- and language-specific resources, and then it falls back on country-neutral, language-specific resources. Assuming a namespace of LocalizedApp, Table 10.1 shows the relative paths that the resource manager probes looking for localized resources.
The assemblies that the resource manager is looking for are known as satellite assemblies in that they're separate assemblies that can be found near the location of the main assembly , which is the assembly containing the code for the localized form(s). The resources embedded in the main assembly get loaded only if no culture-specific resources are found. By default, these resources are culture-neutral in that they aren't specialized for any culture. To mark resources embedded with code as culture-specific , you can apply the NeutralResourcesLanguageAttribute attribute (from the System.Resources namespace) to the assembly as a whole. [8] The following is an example of marking an assembly's resources as country- and language-specific:
Table 10.1. Resource Manager Probing for Localized Resources
Imports System.Resources ' Mark all resources in this assembly as U.S. English. ' No probing will be done in the en-US culture. <Assembly: NeutralResourcesLanguageAttribute("en-US")> The following is an example of marking an assembly's resources as country-neutral and language-specific: Imports System.Resources ' Mark all resources in this assembly as country-neutral English. ' Probing will be done for country-specific resources but ' will stop when country-neutral resources are needed. <Assembly: NeutralResourcesLanguageAttribute("en")> The reason to mark an assembly's resources as culture-specific is to avoid the resource probing process for satellite assemblies when the main assembly code also contains the culture-specific resources. Whereas culture-specific resource assemblies are loaded at the namespace level, resources themselves are localized at the form level. A form is localized if the Localizable property is set in the Property Browser to true (the default is false). When the Localizable property is false, a new form has no entries in the .resx file. However, when the Localizable property is set to true, the .resx file expands to hold 26 entries, each corresponding to a property to be read from a localized resource during execution of the InitializeComponent method: Sub InitializeComponent() Dim resources As Resource Manager = _ New ResourceManager(GetType(Form1)) ' Form1 Me.AccessibleDescription = _ (CStr(resources.GetObject("$Me.AccessibleDescription"))) Me.AccessibleName = _ (CStr(resources.GetObject("$Me.AccessibleName"))) Me.Anchor = _ (CType(resources.GetObject("$Me.Anchor"), AnchorStyles)) Me.AutoScaleBaseSize = _ (CType(resources.GetObject("$Me.AutoScaleBaseSize"), Size)) Me.AutoScroll = _ (CBool(resources.GetObject("$Me.AutoScroll"))) Me.AutoScrollMargin = _ (CType(resources.GetObject("$Me.AutoScrollMargin"), Size)) Me.AutoScrollMinSize = _ (CType(resources.GetObject("$Me.AutoScrollMinSize"), Size)) Me.BackgroundImage = _ (CType(resources.GetObject("$Me.BackgroundImage"), Image)) Me.ClientSize = _ (CType(resources.GetObject("$Me.ClientSize"), Size)) Me.Dock = _ (CType(resources.GetObject("$Me.Dock"), DockStyle)) Me.Enabled = _ (CBool(resources.GetObject("$Me.Enabled"))) Me.Font = _ (CType(resources.GetObject("$Me.Font"), Font)) Me.Icon = _ (CType(resources.GetObject("$Me.Icon"), Icon)) Me.ImeMode = _ (CType(resources.GetObject("$Me.ImeMode"), ImeMode)) Me.Location = _ (CType(resources.GetObject("$Me.Location"), Point)) Me.MaximumSize = _ (CType(resources.GetObject("$Me.MaximumSize"), Size)) Me.MinimumSize = _ (CType(resources.GetObject("$Me.MinimumSize"), Size)) Me.Name = "Form1" Me.RightToLeft = _ (CType(resources.GetObject("$Me.RightToLeft"), RightToLeft)) Me.StartPosition = _ (CType(resources.GetObject("$Me.StartPosition"), _ FormStartPosition)) Me.Text = _ resources.GetString("$Me.Text") Me.Visible = _ (CBool(resources.GetObject("$Me.Visible"))) End Sub For a localized form, the InitializeComponent method checks satellite resources for any property that could be culture-specific. When a form has been set to localizable, you can choose a culture from the Language property in the Property Browser, as shown in Figure 10.14. Figure 10.14. Choosing a Culture in the Property Browser
For each culture you choose, a corresponding .resx file containing culture-specific data will be associated with the form. Figure 10.15 shows a form in the Property Browser after the developer has chosen to support several languages ”some country-specific and others country-neutral. Figure 10.15. One Form with Localization Information for Several Cultures
When the project is built, all of the form's culture-specific resources are bundled together into a satellite assembly, one per culture, and placed into the appropriately named folder. The folders and satellite assemblies are named so that the resource manager, looking for the culture-specific resources, can find the ones it's looking for: LocalizedApp.exe en\ LocalizedApp.resources.dll en-CA\ LocalizedApp.resources.dll en-US\ LocalizedApp.resources.dll fr\ LocalizedApp.resources.dll fr-CA\ LocalizedApp.resources.dll Notice that the main application is at the top level, containing the culture-neutral resources, and the culture-specific resource assemblies are in subfolders named after the culture. Notice also that VS.NET has chosen the names of the subfolders and satellite assemblies that the resource manager will look for first (as shown in Table 10.1), saving probing time. The presence of a new satellite assembly in the file system in a place that the resource manager can find it is all that's required to localize an assembly's form for a new culture. When a localized form is loaded, the resource manager will find the new satellite assembly and will load the resources from it as appropriate, without the need to recompile the main assembly itself. This provides no-compile deployment for localized resources. Resource Localization for NondevelopersVS.NET is a handy tool for resource localization, but it's not something you want to force nondevelopers to use. Luckily, after you set the Localizable property to true for each localizable form and rebuild your component, your user can localize a set of forms in an assembly without further use of VS.NET. To allow nondevelopers to localize resources, the .NET Framework SDK ships with a tool called Windows Resource Localization Editor ( winres .exe). To use it, you open a culture-neutral .resx file for a localizable form ”that is, a form with the Language property set to (Default). [9] After you've loaded the .resx file, you're presented with a miniature version of the VS.NET forms Designer, which you can use to set culture-specific resource information as shown in Figure 10.16.
Figure 10.16. Localizing a Form Using winres.exe
Before you make any changes, we recommend choosing File Save As to choose a culture. The culture will be used to format a culture-specific name for the .resx file. For example, LocalizedForm1.resx will be saved as LocalizedForm1.en-US.resx for the U.S. English culture, just as VS.NET does it. After you save the culture-specific .resx file, make the culture-specific changes and save again. Next, you create the set of culture-specific .resx files for an assembly, one per form, to use in creating a satellite assembly. To do that, you start by bundling them into a set of .resources files. You can do that using the resgen.exe tool shown earlier. To execute resgen .exe on more than one .resx file at a time, use the /compile switch: C:/> resgen.exe /compile Form1.en-US.resx Form2.en-US.resx ... Running resgen.exe in this manner will produce multiple .resources files, one per .resx file. After you've got the .resources files for all the localized forms for a particular culture, you can bundle them into a single resource assembly by using the assembly linker command line tool, al.exe: [View full width]
The assembly linker is a tool with all kinds of uses in .NET. In this case, we're using it to bundle a number of .resources files into a single satellite assembly. The /out argument determines the name of the produced assembly. Make sure to pick one of the file names that the resource manager will probe for (as shown in Table 10.1). The /culture argument determines the culture of the resource assembly and must match the culture name for the resources you're building. The /embedresource arguments provide the .resources files along with the alternative names to match the names that the resource manager will look for. By default, al.exe bundles each resource into a named container based on the file name. However, to match what the resource manager is looking for, you must use the alternative name syntax to prepend the resource namespace. Again, ildasm is a useful tool to make sure that you have things right when it comes to building satellite resources. Figure 10.17 shows the result of running ildasm on the App.resources.dll produced by the earlier call to al.exe. Figure 10.17. ildasm Showing a Culture-Specific Resource Satellite Assembly
Figure 10.17 shows two localized forms, one for each of the .resources files passed to the al.exe file. In addition, notice that the locale has been set to en-US in the .assembly block. This locale setting is reserved for resource-only satellite assemblies and is used by the resource manager to confirm that the loaded resources match the folder and assembly name used to find the satellite assembly. Resource ResolutionWhen there are multiple resources that match the current culture, the resource manager must choose among them. For example, if an application is running under the en-US culture, a resource with the same name can be present in an en-US satellite assembly, in an en satellite assembly, and in the main assembly itself. When multiple assemblies can contain a resource, the resource manager looks first in the most specific assembly, that is, the culture-specific assembly. If that's not present, the language-specific assembly is checked, and finally the culture-neutral resources. For example, imagine a form that has three resource-specific Text properties: one for a Label control, one for a Button control, and one for the Form itself. Imagine further that there are two satellite assemblies ”one for en-US and one for en ”along with the neutral resources bundled into the form's assembly itself. Figure 10.18 shows how the resource manager resolves the resources while running in the en-US culture. Figure 10.18. The Resource Manager's Resource Resolution Algorithm
Remember that the resource manager always looks for the most specific resource it can find. So even though there are three instances of the button's Text property, the most culture-specific resource in the en-US assembly " overrides " the other two. Similarly, the language-specific resource for the label is pulled from the en assembly only because it's not present in the en-US assembly. Finally, the culture-neutral resource is pulled from the main assembly for the form's Text property when it's not found in the satellite assemblies. This resolution algorithm enables resources that are shared between all cultures to be set in the culture-neutral resources, leaving the culture-specific resources for overriding only the things that are culture-specific. However, resolving resources in less culture-specific assemblies works only when a resource is missing from the more culture-specific assembly. VS.NET is smart about putting only properties that have changed into a more culture-specific assembly, but that is not the case with WinRes. Because of the way it works, WinRes duplicates all the culture-neutral resource information to the culture-specific resource files. This means that when using WinRes, all the resources will need to be localized to a more specific culture, even if they aren't changed from a less specific culture. Testing Resource ResolutionTo test that resource resolution is working the way you think it should, you can manually set the CurrentUICulture property of the current thread: Shared Sub Main() ' Test localized resources under fr-CA culture System.Threading.Thread.CurrentThread.CurrentUICulture = _ New CultureInfo("fr-CA") Application.Run(New MainForm()) End Sub Although the CurrentUICulture property defaults to the current culture setting of Windows itself, it can be changed. Whatever the value is when a resource manager is created will be the culture that the resource manager uses to resolve resources. Input LanguageClosely related to a thread's current culture is the input language to which the keyboard is currently mapped, which determines which keys map to which characters . As a further means of testing your application in alternative cultures, the WinForms Application object supports switchable input languages. The list of installed layouts is available from the InputLanguage class's InstalledInputLanguages property: Dim l As InputLanguage For Each l In InputLanguage.InstalledInputLanguages MessageBox.Show(l.LayoutName) Next You can change the current input language by setting one of the installed input languages to the InputLanguage class's property (which is also wrapped by the Application.CurrentInputLanguage property): Dim lang As InputLanguage = ... ' Select an input language Application.CurrentInputLanguage = lang ' one way InputLanguage.CurrentInputLanuage = lang ' another way The default system input language is available via the DefaultInputLanguage property of the InputLanguage class, should you need to reinstate it. |