Windows Forms 2.0 Programming (Microsoft .NET Development Series)
Throughout this chapter, we've come back repeatedly to the workhorse of the resources world, the ResourceManager class. This situation is no different when you talk about providing support for application internationalization (i18n).[9] Globalization is the act of creating an application that will execute across multiple cultures without the need for recompilation. Localization is the act of providing one or more sets of culture- and location-specific data over which a globalized application operates. Together, these two concepts are often joined under the umbrella term: internationalization.[10] Figure 13.21 illustrates. [9] The i18n abbreviation came from the need to spell out internationalization so often that the middle 18 letters were replaced with the number 18. Similarly, globalization and localization become g11n and l10n, respectively. In this same spirit, I plan to switch from abbreviation to "a10n" any day now. [10] Further information on internationalization can be found at http://msdn.microsoft.com/library/ default.asp?url=/library/en-us/vbcon/html/vboriInternationalization.asp (http://tinysells.com/24). Figure 13.21. Localized Data Sets
A globalized application might be one that uses code to format currency or dates according to the current locale, as shown in Figure 13.22. Figure 13.22. Localized Currencies and Dates
This code requires special knowledge of available cultures in .NET. Culture Information
I generated the currencies and dates in Figure 13.22 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: // MainForm.cs using System.Globalization; ... partial class MainForm : Form { ... void MainForm_Load(object sender, EventArgs e) { // Get example values double amount = 4.52; DateTime date = DateTime.Now; // Show localized versions of the example values foreach( CultureInfo info in CultureInfo.GetCultures(CultureTypes.AllCultures) ) { ListViewItem item = listView.Items.Add(info.EnglishName); item.SubItems.Add(info.Name); if( !info.IsNeutralCulture ) { item.SubItems.Add(amount.ToString("C", info.NumberFormat)); item.SubItems.Add(date.ToString("d", info.DateTimeFormat)); } } } }
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 a wrapper around the CurrentCulture property of the current thread, so either can be used to test your programs in alternative cultures: void testCulturesButton_Click(object sender, EventArgs e) { double amount = 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); }
By default, the current culture is whatever the user has set on their machine. 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."[11] [11] The language/country naming convention is dictated by two ISO standards: ISO 639, http://en.wikipedia.org/wiki/ISO_639 (http://tinysells.com/25), and ISO 3166, http://en.wikipedia.org/wiki/ISO_3166 (http://tinysells.com/26). Resource Localization
Thread.CurrentCulture exposes a CultureInfo object that provides access to localization dataincluding date, time, and formattingfor the current region.[12] But what about localization data that is application specific, such as control text? .NET supports application-specific localization via culture-specific resource assemblies deployed in satellite assemblies. Satellite assemblies are separate assemblies that can be found near the location of the main assembly, which is the assembly containing the code for the localized forms. [12] In Windows XP, the current region and date, time, and currency formatting are set from the Regional and Language Options control panel. The resources embedded in the main assembly are considered culture neutral in that they aren't specialized for any culture. Culture-specific resources, in contrast, are embedded into a project on a per-form basis, with each form being responsible for one or more sets of culture- and language-specific localized data sets, as well as a culture-neutral data set. To support form localization, each form has a Localizable property that can be changed from the default value of false to true. When the Localizable property is false, a form doesn't have any entries in its .resx file. When the Localizable property is set to true, a form's .resx file expands to hold the three entries shown in Figure 13.23. Figure 13.23. Default Resource Entries for an Empty Form
Resource entries that are localizable are given resource names in the following format: $this.FormProperty As you can see in Figure 13.23, then, only Form.Text is localized by default. The remaining entries, prefixed by ">>," are form properties that aren't localizable. As you saw earlier, the localizable form properties are set from the .resx file during form initialization: // MainForm.Designer.cs using System.ComponentModel; ... partial class MainForm { ... void InitializeComponent() { ... ComponentResourceManager resources = new ComponentResourceManager(typeof(LocalizedFormSampleForm)); ... // MainForm resources.ApplyResources(this, "$this"); ... } }
InitializeComponent uses ComponentResourceManager (from the System.ComponentModel namespace) and its ApplyResources method to enumerate the underlying form resources and set their equivalent properties on the form. Although resource data entries are automatically created for three localizable form properties, additional form properties also become localized when you edit them in the Windows Forms Designer. But, not all form properties can be localized, and unfortunately neither IntelliSense nor the SDK documentation has an immediately obvious technique for determining property localizability. However, the implementations of localizable form properties are adorned with the Localizable from attribute, with its IsLocalizable property set to true. This information can be discovered using reflection, eliciting the following localizable form properties:[13] [13] Christophe Nasarre very kindly provided the code sample to find localizable form properties, a version of which (LocalizableFormPropertyDiscoverer) is part of the samples for this chapter.
Additionally, you can use the Windows Resource Localization Editor tool, covered shortly, to quickly identify localizable properties. It's the act of localizing a form that results in the InitializeComponent method probing for satellite resources, specifically for any property that could be culture specific. You create a culture-specific satellite resource when you choose a culture from a form's Language property in the Properties window, as shown in Figure 13.24. Figure 13.24. Choosing a Culture in the Properties Window
When a culture is chosen, you add culture-specific values by setting the desired form properties. For each culture you choose, a corresponding .resx file containing culture-specific data is created and associated with the form (when the culture-specific data is provided and differs from the default values). Figure 13.25 shows a form in Solution Explorer after the developer has chosen to support several languagessome country specific and others country neutral. Figure 13.25. One Form with Localization Information for Several Cultures
When the project is built, all of the form's culture-specific resources are bundled into a satellite assembly, one per culture, and placed into the appropriately named folder, as shown in Figure 13.26. Figure 13.26. Use of Appropriately Named Folders to Store Satellite Assemblies
The folders and satellite assemblies are named so that the resource manager can find the culture-specific resources it's probing for: LocalizedFormSample.exe en\LocalizedFormSample.resources.dll en-US\LocalizedFormSample.resources.dll fr\LocalizedFormSample.resources.dll fr-CA\LocalizedFormSample.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 VS05 has chosen the names of the subfolders and satellite assemblies that the resource manager looks for first (as shown in Table 13.1 later in this chapter), 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 the only thing required to localize an assembly's form for a new culture. When a localized form is loaded, the resource manager finds the new satellite assembly and loads 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 Nondevelopers
VS05 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 VS05. To allow nondevelopers to localize resources, the .NET Framework SDK ships with a tool called Windows Resource Localization Editor (winres.exe).[14] One way to use it is to open a culture-neutral .resx file for a localizable formthat is, a form with the Language property set to (Default). After you've loaded the .resx file, you're presented with a miniature version of the VS05 Forms Designer, which you can use to set culture-specific resource information, as shown in Figure 13.27. [14] Another advantage of using winres.exe is that the Properties window for each edited form contains only properties that can be localized. Figure 13.27. Localizing a Form Using winres.exe
Before you make any changes, I recommend choosing File | Save As, which opens the Select Culture dialog, where you can choose a culture and a file mode, both shown in Figure 13.28. Figure 13.28. Saving a Localized Form
The culture is used to format a culture-specific name for the .resx file. For example, MainForm.resx is saved as MainForm.en-US.resx for the U.S. English culture, just as VS05 does it. The file mode determines what is persisted to localized .resx files; Visual Studio file mode (VSFM) ensures that only resource deltas (differences) are persisted, something that can save quite a bit of space. After you save the culture-specific .resx file, make the culture-specific changes and save again. Because both culture-neutral and culture-specific .resx files can be edited equally in VS05 and the Windows Resource Localization Editor, you can also create culture-specific .resx files in the former and edit them in the latter, or vice versa as you've seen. Thus, you can choose a model that works best for your nondevelopers. Next, you create the set of culture-specific .resx files for an assembly, one per form, to use in creating a satellite assembly. You start by bundling them into a set of .resources files by 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 MainForm.en-US.resx OtherForm.en-US.resx ...
Running resgen.exe in this manner produces multiple .resources files, one per .resx file. After you have the .resources files for all the localized forms for a particular culture, you can bundle them into a single resource assembly by using al.exe, the assembly linker command line tool: C:/> al.exe /out:en-US\WinResLocalizedFormSample.resources.dll /culture:en-US /embedresource:LocalizedForm1.en- US.resources,WinResLocalizedFormSample.LocalizedForm1.en-US.resources /embedresource:LocalizedForm2.en- US.resources,WinResLocalizedFormSample.LocalizedForm2.en-US.resources ... The assembly linker tool has 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 file path and the name of the produced assembly. Make sure that the file path exists, and pick one of the file names that the resource manager will probe for (as shown later in Table 13.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 for making sure that you have things right when it comes to building satellite resources. Figure 13.29 shows the result of running ildasm on WinResLocalizedFormSample.resources.dll, which was produced by the earlier call to al.exe. Figure 13.29. ildasm Showing a Culture-Specific Resource Satellite Assembly
Figure 13.29 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 Probing
After you create localized resources and store them in either the main assembly (culture-neutral resources) or satellite assemblies (culture-specific resources), an application needs a way to find the appropriate localization data. As you saw earlier, Thread.CurrentCulture provides access to localization data that's stored on a per-system basis. For per-form localization, the resource manager uses the CurrentUICulture property of the current thread to determine which culture's resources to load. When the resource manager needs its first resource, it probes the file system for a satellite assembly that contains the appropriate culture-specific resource. Based on the assembly name of the type it's loaded with, the ResourceManager component looks in 16 places for the assembly, specifically targeting executables (.exes) and libraries (.dlls). It probes satellite assemblies first for country- and language-specific resources and then for country-neutral and language-specific resources, before falling back on the culture-neutral resources bundled with the calling assembly. Assuming an assembly name of LocalizedDataSample, Table 13.1 shows the relative paths that the resource manager probes looking for localized resources. When the main assembly code also contains the culture-specific resources, you can avoid unnecessary resource probing by marking the main assembly as culture-specific; to do this, you apply the NeutralResourcesLanguage attribute (from the System.Resources namespace) to the assembly as a whole.[15] The following is an example of marking an assembly's resources as country- and language-specific: [15] The VS05-generated AssemblyInfo.cs file is a handy place to put assembly-level attributes. using System.Resources; ... // Mark all resources in this assembly as U.S. English. // No probing will be done in the en-US culture. [assembly: NeutralResourcesLanguage("en-US")]
The following is an example of marking an assembly's resources as country-neutral and language-specific: using 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: NeutralResourcesLanguage("en")]
You can circumvent the probing process using the NeutralResourcesLanguage attribute, but you need to rely on the resource manager to decide which set of resource data to rely on when resources exist for multiple languages for a specific culture. Resource Resolution
When multiple resources match the current culture, the resource manager must choose among them. For example, if an application is running under the fr-CA culture, a resource with the same name can be present in an fr-CA satellite assembly, in an fr 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 assembliesone for fr-CA and one for fralong with the neutral resources bundled into the form's assembly. Figure 13.30 shows how the resource manager resolves the resources while running in the en-US culture. Figure 13.30. 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 fr-CA assembly "overrides" the other two. Similarly, the language-specific resource for the label is pulled from the fr assembly only because it's not present in the fr-CA 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. Both VS05 and WinRes are smart about putting only those properties that have changed into a more-culture-specific assembly. Testing Resource Resolution
To test that resource resolution is working the way you think it should, you can manually set the CurrentUICulture property of the current thread: // Program.cs using System.Threading; ... [STAThread] static void Main() { ... // Test localized resources under fr-CA culture Thread.CurrentThread.CurrentUICulture = new CultureInfo("fr-CA"); Application.Run(new MainForm()); }
CurrentUICulture defaults to the current culture setting of Windows itself, and this is why you don't need to set it unless you want a culture other than the current one. Whether you use the current Windows culture or some alternative culture, the culture used by the resource manager is the value of CurrentUICulture at the time the ApplyResources method is invoked for a form or control. As a rule of thumb for ensuring that the desired culture takes effect, set CurrentUICulture before the call to InitializeComponent in places like the application's entry point. Input Language
Closely related to a thread's current culture is the input language to which the keyboard is currently mapped. The input language determines which keys map to which characters. Input language support is exposed by the .NET Framework as the InputLanguage type, which offers a variety of support for inspecting and changing culture and language information: namespace System.Windows.Forms { sealed class InputLanguage { // Properties public CultureInfo Culture { get; } public static InputLanguage CurrentInputLanguage { get; set; } public static InputLanguage DefaultInputLanguage { get; } public static InputLanguageCollection InstalledInputLanguages { get; } public string LayoutName { get; } // Methods public static InputLanguage FromCulture(CultureInfo culture); } }
For example, to enumerate the list of installed layouts, you use the InputLanguage type's InstalledInputLanguages property: void listInputLanguagesButton_Click( object sender, EventArgs e) { foreach( InputLanguage lng in InputLanguage.InstalledInputLanguages ) { string language = lng .LayoutName + " [" + lng.Culture + "]"; this.inputLanguagesList.Items.Add(language); } }
Figure 13.31 shows the result of executing this code on a computer with three input languages, including U.S. English, Australian English, and Canadian French. Figure 13.31. Discovering a Computer's Input Languages
As a further means of testing your application in alternative cultures, the Windows Forms Application class supports switchable input languages. You can change the current input language by setting one of the installed input languages to either of two properties of the InputLanguage class (which is also wrapped by the Application.CurrentInputLanguage property): // Change input language to Australian English InputLanguage lng = InputLanguage.FromCulture(new CultureInfo("en-AU")); Application.CurrentInputLanguage = lng; // one way InputLanguage.CurrentInputLanguage = lng; // another way
The default system input language is available via the DefaultInputLanguage property of the InputLanguage class, should you need to reinstate it: // Reinstate default Application.CurrentInputLanguage = InputLanguage.DefaultInputLanguage; .NET offers various input language support features that you should become familiar with when globalizing your applications. Reading Direction
One feature that internationalized applications may need is the ability to support both right-to-left and left-to-right reading order. You can toggle all the text on a form between left and right alignments, including the form's caption and text on child controls, by toggling the RightToLeft property: partial class MainForm : Form { void ltrRadioButton_CheckedChanged(object sender, EventArgs e) { // For text this.RightToLeft = RightToLeft.No; } void rtlRadioButton_CheckedChanged(object sender, EventArgs e) { // For text this.RightToLeft = RightToLeft.Yes; } }
Figure 13.32 illustrates the effect of toggling the RightToLeft property. Figure 13.32. Toggling the Form's RightToLeft Property
Unfortunately, even though the text is swapped between left- and right-aligned, you can see that the controls hosted by the form aren't themselves changing alignment, just as the form's adornments remain where they are.[16] What we really need to do is swap the whole UI between left-to-right and right-to-left layout. We can do that by toggling a form's RightToLeftLayout property: [16] The adornments are the system menu and the minimize, maximize, and close buttons. partial class MainForm : Form { void ltrRadioButton_CheckedChanged(object sender, EventArgs e) { // For text this.RightToLeft = RightToLeft.No; } void rtlRadioButton_CheckedChanged(object sender, EventArgs e) { // For text this.RightToLeft = RightToLeft.Yes; } void rtlLayoutCheckBox_CheckedChanged(object sender, EventArgs e) { this.RightToLeftLayout = this.rtlLayoutCheckBox.Checked; } }
Toggling RightToLeftLayout yields the behavior illustrated in Figure 13.33. Figure 13.33. Toggling the Form's RightToLeftLayout Property
The best thing about both properties is that they are localizable, so, for example, you could ensure that all English-derived languages are laid out left-to-right and that all Arabic-derived languages are laid out right-to-left, changing the values of RightToLeft and RightToLeftLayout as necessary between languages.[17] [17] As you saw in Chapter 6: Drawing Text, painting complex scripts can be problematic with GDI+. See http://www.microsoft.com/middleeast/msdn/visualstudio2005.aspx (http://tinysells.com/27) for more information on the finer points of right-to-left text rendering. |
Категории