The Road to an International Application

Building an international application, although not that difficult, does take some forethought and planning, but the payoff can be enormous. Following are some of the reasons why you would want to go to the extra trouble it takes to build an international application:

  • For many companies, more than half of current profits are generated in international markets.

  • Even the users of the English version of the software might have second thoughts about using the software if it has not embraced other languages and cultures. Internationalizing an application has a way of giving a certain amount of credibility to the software. Code is often better designed and better made if it adheres to a framework that demands localizability.

  • Because of fierce competition, more international users have a choice. All other things being equal, users will choose software that respects their local customs and language.

  • The European Common Market will have regulatory practices in place to make sure that software adapts to a country's culture and language.

  • After the initial framework is in place, the actual cost of internationalizing an application is minor when considered over the life of the software.

  • Communicating in a native language and adopting local cultural conventions is fundamental to building respect and trust that is key to success in today's market.

Now that you know the reasons for building an international application, how should you go about doing so? Remember that internationalization is the process of extracting all cultural/regional conventions and language dependencies from the software. An application can become internationalized to a certain degree:

  • The absolute bare minimum requirement for an application to be considered internationalized is the removal of any character-set and cultural dependencies (such as date and number format). Because the default character set of a string within the .NET Framework is 16-bit Unicode, most applications meet this requirement out of the box. It is possible to add code as was done in Listing 18.1 (the date format in this sample is hard-coded for a U.S. style date), but it does take a certain amount of effort to break the rules.

  • At the next level, all user-visible strings are moved out of the software and into a language and country-dependent resource file. Java calls these ResourceBundles. The Visual Studio 6 environment built binary resource files that ended in a .res suffix from .rc source files. The .NET Framework binary resource is named with a .resources suffix, and it is created either from an XML .resx file or a .txt file that contains key/value pairs to be entered into a string table.

  • At the third level, an application can decide to support non-Western languages such as Chinese, Japanese, and Korean. This requires a commitment up front to either Unicode or multibyte character encoding. This level of internationalization increases the storage cost of string-related data because now all character data requires more than one character to represent and store the data. Additional time is required to develop such an application because further consideration must be given to sorting, indexing, and manipulating the string data. Some languages such as Thai require special algorithms to even break a sentence into words.

    In addition, some non-Western languages, such as Arabic, have an added complexity in that they read from right-to-left. This requirement can be difficult for a programmer who is not used to this convention.

  • The highest degree at which an application can be internationalized is for the application to support many different character encodings at once. This would mean that an application could display Chinese and English at the same time. Internationalization would mean that provision should be provided so that an application can support multiple encodings at a time.

As will be seen further in this chapter, the .NET Framework and the CLR supply support for all of these degrees of internationalization. After the application is internationalized, it needs to be localized to be a true international application.

When localizing an application, the following must be taken into account:

  • Time can be represented as a 24-hour clock or a 12-hour clock. Separators can be either the common colon or a single period. (Italy uses the period convention).

  • A date can be represented in many different ways. You can use different separators, different orders, different abbreviations for months, and even different calendars. For instance, in the Thai Buddhist calendar, the year 2001 is the year 2544. The Hijri calendar, used in many Arabic-speaking countries, has 12 months in a year, but each month is not the same length as in the Gregorian calendar. In addition, the Hijri calendar recognizes one era A.H. (referencing Mohammed's migration from Mecca), so 2001 becomes 1422.

  • Number formatting also has several different conventions. Some cultures have a convention that uses a period instead of a comma to separate the thousands (1000000 becomes 1.000.000,00). Negative numbers can also be represented in many different ways.

  • Every country has a different currency symbol.

  • Word order varies between countries.

  • Some languages do not have a word separator character.

  • Collation sequences differ from country to country.

  • Oriental languages do not have capitalization. Rules for capitalization differ from country to country.

  • The rules of hyphenations (including what to hyphenate and where) can differ from country to country.

  • Spelling, grammar, and punctuation can vary from language to language.

  • Particular care must be taken when phrases are combined to form messages. Make sure that the combination is grammatically and culturally correct. One area where this would occur would be in the building of an error message.

  • Translation of phrases can rarely be done literally.

In all but the simplest of localization projects, it is recommended that a known, trusted professional translator be used to assist in the localization. This is especially true if you are not comfortable with the language and the culture/region that you are trying to address.

To extract the cultural dependencies, you need an object or class to encapsulate those cultural dependencies. The CLR and the .NET Framework use the CultureInfo class for this purpose.

Using the CultureInfo Class

As the name implies, the CultureInfo class holds culture-specific information such as language, regional dialect, country/region, calendar, and cultural conventions. CultureInfo encapsulates information that is required by culture-specific operations such as sorting, string comparison, character type information, and date and number formatting. This class extends and encapsulates a “locale” that was used to describe languages and cultures with VBScript, C++, and VB.

A specific CultureInfo object is completely described by a string that is of the format <languagecode>-<country>. The details of the format of this string are based on RFC 1766.

Note

RFC 1766 specifies the culture string to be of the following form:

<languagecode>-<country/regioncode>

<languagecode> is a lowercase two-letter code derived from ISO 639-1, and <country/regioncode> is an uppercase two-letter code derived from ISO 3166. For example, English is en, Spanish is es, German is de, and Chinese is zh for the <languagecode>.

One source for RFC 1766 is http://www.faqs.org/rfcs/rfc1766.html.

RFC 1766 has been copied and made available many other places besides the preceding URL. This document references ISO 639 and ISO 3166. The essence for ISO 639 can be obtained at http://www.oasis-open.org/cover/iso639a.html.

ISO 639 lists the language codes for each of the recognized languages. The official copy of the document is at http://www.iso.ch/iso/en/CatalogueDetailPage.CatalogueDetail?CSNUMBER=4766, but it must be purchased. The ISO 3166 lists country codes. One source for these country codes is at http://www.unicode.org/unicode/onlinedat/countries.html.

A neutral culture is associated with a language but not a specific country. Therefore, en would specify English, but not a specific country. A complete specification en-US would specify English in the United States, en-GB would be English in Great Britain, and en-CA would be English in Canada.

An invariant culture is a culture that is associated with a language but not with any particular country. Anywhere a culture is required, the invariant culture can be used except for sorting. An invariant culture should be used only for processes that produce or consume culture-independent results, such as system services.


The CLR relies on operating system support for various cultures. The support for the culture and language must be installed for support to be available. This support is not intrinsic to the CLR or the .NET Framework, and it is not installed by default when your operating system is installed. Under Windows 2000, Control Panel → Regional Options → General tab, you will see language settings, as shown in Figure 18.3.

Figure 18.3. Installed language support.


If you have checked support for all languages and then run the code in Listing 18.3, you can see the languages and cultures for which you have support. The complete source for this application is in the CultureList directory.

Listing 18.3. Programmatically Listing Supported Cultures
using System;
using System.Globalization;

namespace CLRUnleashed
{
    class CultureList
    {
        public static void Main(String[] args)
        {
            CultureInfo[] cia = CultureInfo.GetCultures(CultureTypes.SpecificCultures);
            foreach(CultureInfo ci in cia)
            {
                Console.WriteLine(ci);
            }
        }
    }
}

From Listing 18.3, you can see which languages and cultures are currently supported. From this list, some notable missing components include the following:

  • en-UK seems to have been superseded by en-GB.

  • ko-KP (North Korea) is not supported.

  • bo-cn (Tibetan) is not supported.

Given the great amount of information contained in a CultureInfo object, you can understand how a line had to be drawn somewhere. Table 18.2 shows the supported cultures. Because support for additional languages and cultures will undoubtedly be added, you should check to see which cultures are supported with the code that is provided in Listing 18.3. Table 18.2 should be taken as an example of some of the specific cultures that are currently supported.

Table 18.2. Installed/Supported Cultures
Culture Code Language – Country
af-ZA Afrikaans – South Africa
ar-AE Arabic – United Arab Emirates
ar-BH Arabic – Bahrain
ar-DZ Arabic – Algeria
ar-EG Arabic – Egypt
ar-IQ Arabic – Iraq
ar-JO Arabic – Jordan
ar-KW Arabic – Kuwait
ar-LB Arabic – Lebanon
ar-LY Arabic – Lybia
ar-MA Arabic – Morocco
ar-OM Arabic – Oman
ar-QA Arabic – Qatar
ar-SA Arabic – Saudi Arabia
ar-SY Arabic – Syria
ar-TN Arabic – Tunisia
ar-YE Arabic – Yemen
be-BY Byelorussian – Belarus
bg-BG Bulgarian – Bulgaria
ca-ES Catalan – Spain
cs-CZ Czech – Czech Republic
cy-az-AZ Azeri (Latin) – Azerbaijan
cy-sr-SP Serbian (Cyrillic) – Serbia
cy-uz-UZ Uzbek (Latin) – Uzbekistan
da-DK Danish – Denmark
de-AT German – Austria
de-CH German – Switzerland
de-DE German – Germany
de-LI German – Liechtenstein
de-LU German – Luxembourg
es-HN Spanish – Honduras
el-GR Greek – Greece
en-AU English – Australia
en-BZ English – Belize
en-CA English – Canada
en-CB English – Caribbean
en-GB English – Great Britain
en-IE English – Ireland
en-JM English – Jamaica
en-NZ English – New Zealand
en-PH English – Philippines
en-TT English – Trinidad Tobago
en-US English – US
en-ZA English – South Africa
en-ZW English – Zimbabwe
es-AR Spanish – Argentina
es-BO Spanish – Bolivia
es-CL Spanish – Chile
es-CO Spanish – Columbia
es-CR Spanish – Costa Rica
es-DO Spanish – Dominican Republic
es-EC Spanish – Ecuador
es-ES Spanish – Spain
es-GT Spanish – Guatemala
es-MX Spanish – Mexico
es-NI Spanish – Nicaragua
es-PA Spanish – Panama
es-PE Spanish – Peru
es-PR Spanish – Puerto Rico
es-PY Spanish – Paraguay
es-SV Spanish – El Salvador
es-UY Spanish – Uruguay
es-VE Spanish – Venezuela
et-EE Estonian – Estonia
eu-ES Basque – Spain
fa-IR Persian – Iran
fi-FI Finnish – Finland
fo-FO Faeroese – Faeroe Islands
fr-BE French – Belgium
fr-CA French – Canada
fr-CH French – Switzerland
fr-FR French – France
fr-LU French – Luxembourg
fr-MC French – Monaco
gl-ES Galician – Spain
gu-IN Quechua – India
he-IL Hebrew – Israel
hi-IN Hindi – India
hr-HR Croatian – Croatia
hu-HU Hungarian – Hungary
hy-AM Armenian – Armenia
id-ID Indonesian – Indonesia
is-IS Icelandic – Iceland
it-CH Italian – Switzerland
it-IT Italian – Italy
ja-JP Japanese – Japan
ka-GE Georgian – Georgia
kk-KZ Kazakh – Kazachstan
kn-IN Kannada – India
kok-IN Konkani – India
ko-KR Korea – South Korea
ky-KZ Kirghiz – Kazakhstan
lt-az-AZ Azeri (Cyrillic) – Azerbaijan
lt-LT Lithuanian – Lithuania
lt-sr-SP Serbian (Latin) – Serbia
lt-uz-UZ Uzbek (Cyrillic) – Uzbekistan
lv-LV Latvian – Latvia
mk-MK Macedonian – Macedonia
mn-MN Mongolian – Mongolia
mr-IN Marathi – India
ms-BN Malay – Brunei Darussalam
ms-MY Malay – Malaysia
nb-NO Norwegian (Bokmål) – Norway
nl-BE Dutch – Belgium
nl-NL Dutch – Netherlands
nn-NO Norwegian (Nynorsk) – Norway
pa-IN Punjabi – India
pl-PL Polish – Poland
pt-BR Portuguese – Brazil
pt-PT Portuguese – Portugal
ro-RO Romanian – Romania
ru-RU Russian – Russian Federation
sa-IN Sanskrit – India
sk-SK Slovak – Slovakia
sl-SI Slovenian – Slovenia
sq-AL Albanian – Albania
sv-FI Swedish – Finland
sv-SE Swedish – Sweden
sw-KE Swahili – Kenya
syr-SY Syriac – Syria
ta-IN Tamil – India
te-IN Telugu – India
th-TH Thai – Thailand
tr-TR Turkish – Turkey
tt-TA Tatar – Tatarstan
uk-UA Ukrainian – Ukraine
ur-PK Urdu – Pakistan
vi-VN Vietnamese – Vietnam
zh-CN Chinese – PRC
zh-HK Chinese – Hong Kong
zh-MO Chinese – Macau
zh-SG Chinese – Singapore
zh-TW Chinese – Taiwan
div-MV Dhivehi – Maldives

If you're curious, that totals 136 cultures that are installed and supported. When an application starts up, it acquires a culture from the default culture that is part of the user preferences. What if you were at a company that wanted to support 50 of these cultures in their application? You would want to test each of the cultures to make sure that they work, which would require 50 PCs each with a specific language and locale associated with it. It turns out that a better way is available.

It is not completely accurate to say that the application acquires a culture. Every Thread has a culture associated to it—specifically, a CultureInfo object. Therefore, it is possible for an application to have one AppDomain and several Threads, each with different CultureInfo objects associated with them. To get/set the CultureInfo object associated with a Thread, you use the CurrentCulture property. To get a property use the following:

CultureInfo ci = Thread.CurrentThread.CurrentCulture;

To associate a CultureInfo object with a Thread, use this:

Thread.CurrentThread.CurrentCulture = new CultureInfo(c);

Here, c is a string variable that has one of the values taken from Table 18.2.

A neutral culture is a super-set to a specific culture. A neutral culture is associated with a language and no-specific country. This is only possible if all of the countries associated with the language do not require specific modifications to the language as to render it non-neutral. For example, notice that a neutral Chinese culture does not exist. That is because some of the countries listed as speaking Chinese use a modified written language, creating a simplified version and a traditional version. Table 18.3 lists the current neutral cultures.

Table 18.3. Neutral Cultures
Neutral Culture Language
af Afrikaans
ar Arabic
az Azeri
be Byelorussian
bg Bulgarian
ca Catalan
cs Czech
da Danish
de German
div Dhivehi
el Greek
en English
es Spanish
et Estonian
eu Basque
fa Persian
fi Finnish
fo Faeroese
fr French
gl Galician
gu Quechua
he Hebrew
hi Hindi
hr Croatian
hu Hungarian
hy Armenian
id Indonesian
is Icelandic
it Italian
ja Japanese
ka Georgian
kk Kazakh
kn Kannada
ko Korean
kok Konkani
ky Kirghiz
lt Lithuanian
lv Latvian
mk Macedonian
mn Mongolian
mr Marathi
ms Malay
nl Dutch
no Norwegian
pa Punjabi
pl Polish
pt Portuguese
ro Romanian
ru Russian
sa Sanskrit
sk Slovak
sl Slovenian
sq Albanian
sv Swedish
sw Swahili
syr Syriac
ta Tamil
te Telugu
th Thai
tr Turkish
tt Tatar
uk Ukrainian
ur Urdu
uz Uzbek
vi Vietnamese
zh-CHS Chinese – Simplified
zh-CHT Chinese – Traditional

A Thread cannot be assigned a neutral culture. Trying to do so results in an exception being thrown:

System.NotSupportedException: Culture "fr" is a neutral culture. It can not be used in
 formatting and parsing and therefore cannot be set as the thread's current culture.
   at System.Globalization.CultureInfo.CheckNeutral(CultureInfo culture)
   at System.Threading.Thread.set_CurrentCulture(CultureInfo value)
   at LocalizedApplication.LocalizedApplicationForm..ctor() in localizedapplication.cs:line 39

If a Thread cannot be assigned a neutral culture, what good is a neutral culture? As it turns out, a neutral culture is important for an application when loading a language- specific resource, as you will see next. If you really do not care to which country a specific culture is assigned, then use the static CultureInfo.CreateSpecificCulture method like this:

Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr");

As the preceding exception indicates, the CurrentCulture is associated with formatting and parsing string data. If you need to format a date properly, it will use the culture information contained in the CurrentCulture property of the thread that is performing the format. Formatting numbers, currency, time, and sorting all use the CurrentCulture property to determine how the operation should be performed.

After the CultureInfo object is associated with the current thread, you use the CultureInfo object indirectly via calls to date formatting, number formatting, and so on. Specific calls retrieve date formatting information, number formatting information, calendar information, and so on. However, typically, you would just call the formatting or parsing function, and that function would look up the culture and do the right thing.

Using the RegionInfo Class

In direct contrast to the CultureInfo class, the RegionInfo class holds data that is not dependent on user preferences or settings. This class contains information such as whether the region uses metric units, what the name of the region is, what the English name of the region is, what the name of the currency symbol is, and so on. If you have constructed a RegionInfo class based on Saudi Arabia, and your operating system has set the English language as its default, then the DisplayName of the region will always be Saudi Arabia.

Using Resources in Your Application

Resources are repositories for data that your application requires. In the end, all resources end up as binary .resources files, which are then embedded into the running executable. You can see the resources that your application has available to it by looking at the compiled application or library with the tool ildasm. If resources are embedded into your application, then these resources would show up as a .mresource in the manifest of the Assembly. Figure 18.4 shows how the output might look.

Figure 18.4. Looking for resources in an application.


Notice that the .mresource is declared as being public. This means that these resources are visible to other assemblies. Resources can also be private, as shown in Figure 18.5.

Figure 18.5. Private resources.


A private resource is a resource that is not visible to other assemblies. You can create a private resource by directly using the assembly linker al. The option to use when embedding assemblies is /embed. The /embed option takes a comma-delimited string as an argument of the following form:

/embed:<filename>[,<name>[,Private]

The last argument is optional and it makes a resource private. Finally, resources can be linked in rather than embedded into an assembly. A linked resource means that rather than have the entire resource embedded in the assembly, a reference to the .resources file is available. Figure 18.6 shows how a linked resource might look.

Figure 18.6. Linked resources.


This would result in a significantly smaller assembly, but now the .resources file(s) must be included as part of the distribution. The .resources file(s) becomes a dependency that must be resolved at runtime.

To add embedded or linked resources to an assembly, you use the al tool supplied as part of the .NET SDK. This tool is called the Assembly Linker, and it is installed by default in the runtime installation directory (for example, WindowsMicrosoft.NETFrameworkv1.x.x.xxxx). If you want to embed a binary .resources file into your assembly, the command line would look something like this:

al /out:GlobalApp.resources.dll /c:en-US /v:1.0.0.0 /embed:Flags.en-US.resources,Flags
.en-US.resources,Private /embed:Translation.en-US.resources,Translation.en-US.resources,Private

To link a resource, you modify the command line only slightly:

al /out:GlobalApp.resources.dll /c:en-US /v:1.0.0.0 /embed:Flags.en-US.resources,Flags
.en-US.resources,Private /embed:Translation.en-US.resources,Translation.en-US.resources,Private

Notice that each of these two options can occur multiple times on a command line, thus creating multiple resources in an assembly.

Now you can put a binary .resources file into the assembly. How is the .resources file generated? A .resources file is compiled using the resgen utility. This utility takes in either a plain text .txt file or an XML .resx file.

The text file contains simple name/value pairs like this:

hello=Hello
goodbye=Goodbye
greeting=How are you?

Compiling this source with resgen would result in three resource strings. (For example, the application could query for the resource string greeting and have “How are you?” returned.) Only strings can be included in this source form of a resource.

A .resx resource file is more complicated because it includes a schema or a definition of the type and format of the tags that follow first. If you were to include the same resource information as described in the preceding paragraph, the .resx file would look like Listing 18.4.

Listing 18.4. A Sample .resx File
<?xml version="1.0" encoding="utf-8" ?>
<root>
    <xsd:schema id="root" targetNamespace="" xmlns="" xmlns:xsd=http://www.w3.org/2001
/XMLSchema xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
        <xsd:element name="root" msdata:IsDataSet="true">
            <xsd:complexType>
                <xsd:choice maxOccurs="unbounded">
                    <xsd:element name="data">
                        <xsd:complexType>
                            <xsd:sequence>
                                <xsd:element name="value" type="xsd:string" minOccurs="0"
 msdata:Ordinal="1" />
                                <xsd:element name="comment" type="xsd:string"
 minOccurs="0" msdata:Ordinal="2" />
                            </xsd:sequence>
                            <xsd:attribute name="name" type="xsd:string" />
                            <xsd:attribute name="type" type="xsd:string" />
                            <xsd:attribute name="mimetype" type="xsd:string" />
                        </xsd:complexType>
                    </xsd:element>
                    <xsd:element name="resheader">
                        <xsd:complexType>
                            <xsd:sequence>
                                <xsd:element name="value" type="xsd:string" minOccurs="0"
 msdata:Ordinal="1" />
                            </xsd:sequence>
                            <xsd:attribute name="name" type="xsd:string" use="required" />
                        </xsd:complexType>
                    </xsd:element>
                </xsd:choice>
            </xsd:complexType>
        </xsd:element>
    </xsd:schema>
    <resheader name="ResMimeType">
        <value>text/microsoft-resx</value>
    </resheader>
    <resheader name="Version">
        <value>1.0.0.0</value>
    </resheader>
    <resheader name="Reader">
        <value>System.Resources.ResXResourceReader</value>
    </resheader>
    <resheader name="Writer">
        <value>System.Resources.ResXResourceWriter</value>
    </resheader>
    <data name="hello">
        <value>Hello</value>
        <comment>Hello</comment>
    </data>
    <data name="goodbye">
        <value>Goodbye</value>
        <comment>Goodbye</comment>
    </data>
    <data name="greeting">
        <value>How are you?</value>
        <comment>greeting</comment>
    </data>
</root>

This is a lot of overhead just to encapsulate three strings! The key portion of the file is in the last few lines, where you see the following lines:

<data name="hello">
    <value>Hello</value>
    <comment>Hello</comment>
</data>
<data name="goodbye">
    <value>Goodbye</value>
    <comment>Goodbye</comment>
</data>
<data name="greeting">
    <value>How are you?</value>
    <comment>greeting</comment>
</data>

However, this format has structure, and the .resx file can contain more than just strings. To get over the overhead, Visual Studio .Net automates the production of .resx files. In addition, two classes, ResXResourceReader and ResXResourceWriter, allow a programmer to build .resx files. In fact, a tool called resxgen is shipped with the .NET SDK that takes in an image file (.bmp, .jpg, .ico, and so on) and creates a .resx file that contains the image. This tool uses the ResXResourceWriter class to do the bulk of its work.

Accessing .NET Resources

After resources are embedded or linked into an assembly, they can be accessed with the ResourceSet, ResourceReader, or ResourceManager classes.

This section will first look at ResourceSet so you can get some appreciation for what ResourceManager adds in functionality.

Assuming the same resource strings as described in Listings 18.3 and 18.4, the following code will retrieve the named resources from the resource file.

ResourceSet rs = new ResourceSet(a.GetManifestResourceStream(s));
Debug.WriteLine(rs.GetString("hello"));
Debug.WriteLine(rs.GetString("goodbye"));
Debug.WriteLine(rs.GetString("greeting"));
rs.Close();

The argument to the ResourceSet constructor is a stream returned by the Assembly a. A ResourceSet can take a filename or an IResourceReader interface. The next three lines retrieve the named resource strings from the set. The problem with ResourceSet is that a specific resource file must be specified to read in a resource. In a running environment, using ResourceSet does not offer much more than reflection in getting the resources. ResourceSet is meant as a tool for reading .resources files; it is not a general solution that meets the requirements for an international application. ResourceSet is the base class for the ResXResourceReader class; therefore, it is important for reading resources, but it is not culturally aware. What about ResourceReader?

You can look at the resources in an Assembly. Listing 18.5 illustrates how this can be done.

Listing 18.5. Listing Resources in an Assembly
Assembly a = Assembly.LoadFrom(@"jaLocalizedApplication.resources.dll");
foreach(string s in a.GetManifestResourceNames())
{
    Debug.WriteLine(s);
    ManifestResourceInfo ri = a.GetManifestResourceInfo(s);
    if(ri.FileName != null)
        Debug.WriteLine("File name: " + ri.FileName);
    if(ri.ReferencedAssembly != null)
        Debug.WriteLine("Assembly: " + ri.ReferencedAssembly.GetName());
    Debug.WriteLine("Location: " + ri.ResourceLocation);
    Stream rs = a.GetManifestResourceStream(s);
    ResourceReader rr = new ResourceReader(rs);
    IDictionaryEnumerator ren = rr.GetEnumerator();
    //Go through the enumerator, printing out the key and value pairs.
    while (ren.MoveNext())
    {
        Debug.WriteLine(string.Format("Name: {0} ", Convert.ToString(ren.Key)));
        Debug.WriteLine(string.Format("Value: {0} ", Convert.ToString(ren.Value)));
    }
}

The added benefit of ResourceReader is that you can enumerate through the resources. With ResourceSet, you had to know ahead of time what the names of the resources were.

The original goal was to build a localized application. How does all of this apply? The ResourceManager class relies heavily on the notion of satellite assemblies. Before you learn about the features of the ResourceManager class, you need to understand satellite assemblies.

Putting It All Together with Satellite Assemblies

Satellite assemblies are assemblies that have no code—just data. They are roughly equivalent to resource DLLs in the unmanaged world. A satellite assembly can be created on the command line with the al tool. You have already seen the creation of a satellite assembly in the discussion of the differences between embed and link. The command line is repeated here for clarity:

al /out:GlobalApp.resources.dll /c:en-US /v:1.0.0.0 /embed:Flags.en-US.resources,Flags
.en-US.resources,Private /embed:Translation.en-US.resources,Translation.en-US.resources,Private

The key point to notice from this command line is that no code or files are referencing code—just resources and the output. Also notice that a version and a culture are specified (“1.0.0.0” and “en-US” respectively).

The ResourceManager is a class that provides convenient access to culture-specific resource information at runtime. For most cases, this class queries the Thread.CurrentUICulture for the culture that is to be used for the search. Thread.CurrentUICulture, like Thread.CurrentCulture, is a property that can be assigned to or retrieved. Typically in an international application, it is advisable to set both of these properties to the same CultureInfo object. Just remember that CurrentUICulture is used almost exclusively by the ResourceManager class to look for appropriate culture-specific resources, whereas CurrentCulture is used by the runtime in formatting, parsing, and sorting.

It is possible to retrieve resources without setting the culture for the thread. You can retrieve a ResourceSet that is particular to a specific culture. For example:

ResourceSet rmrs = rm.GetResourceSet(new CultureInfo("ja-JP"), true, true);
Debug.WriteLine(rmrs.GetString("hello"));
Debug.WriteLine(rmrs.GetString("goodbye"));
Debug.WriteLine(rmrs.GetString("greeting"));

This would retrieve the named resources from the resource set built for the Japanese culture regardless of what CurrentUICulture is set to. This method provides no “fallback” if the specified resource does not exist.

To effectively use this class, some structure must be in place. For each culture that is to be supported, a subdirectory (relative to the current working directory of the application) must exist that has the same name as the culture, and the satellite assembly should be placed in that directory. For example, if you are expecting to support Japanese, then you need to have a directory called ja-JP in which you include a satellite assembly that has a name of the form <application name>.resources.dll, where <application name> is the main assembly name of the application. In addition, you need to make sure that the version as recorded in the main assembly is the same as the version in the satellite assembly.

Note

The version restriction can be modified with SatelliteContractVersionAttribute or publisher policy configuration. If you want to change the main assembly version number, then place a line like this:

[SatelliteContractVersion(“<satellite-version>”)]

where <satellite-version> is a string specifying the version used to load satellite assemblies. Without this attribute, the version of the main assembly is used. Notice that you cannot specify a single assembly. All assemblies must be versioned in unison. Refer to Chapter 6, “Publishing Applications,” to see how to construct a publisher policy.


A ResourceManager object is constructed with a line like the following:

ResourceManager rm = new ResourceManager("LocalizedApplication.Strings", a);

The string that is passed as the first argument is the “base-name” of the resources. If you are unsure what to put there, look at the satellite assemblies with ildasm. You should see something like Figure 18.7.

Figure 18.7. Resource base name.


Notice on the .mresource line the name of the file preceding the culture (“ja”) and the suffix (“.resources”). This is the base name of the resources, and it is what is used as an argument to the constructor of ResourceManager. The last argument is the Assembly to which the ResourceManager is tied. For most cases, you can get the Assembly with the following line:

Assembly a = Assembly.GetExecutingAssembly();

After a ResourceManager is constructed, calls can be made to the GetObject or GetString methods. This is where the culture-aware features of ResourceManager start to kick in. Calling GetObject or GetString results in a search for an assembly that matches the CurrentUICulture of the Thread calling these methods. If the matching satellite assembly has not been loaded, then the subdirectory that matches the culture string is searched for the satellite DLL. If the directory does not exist or the satellite assembly cannot be loaded, then a neutral culture is tested.

For example, if the specific culture was “ja-JP,” then the neutral culture is “jp.” What if you wanted to support all English speaking countries and cultures with a single resource? This can be done by building a single satellite DLL and placing it in an “en” subdirectory with a culture assigned to it as “en.” Now when a thread set up with “en-GB” or “en-AU” cultures (Great Britain and Australia respectively) the thread retrieves resources from the “en” subdirectory, a resource for a specific culture does not exist.

If neither a neutral culture nor a specific culture matches the culture in CurrentUICulture, then the satellite loading process is said to “fallback.” An application should place resources of the same name as the satellite assemblies in the main assembly. This allows for a default resource if no resource seems to be available.

A Sample Using the Hard Way (Manual)

The manual process of building an internationalized application has been described. It is important that you understand the process involved in building such an application before you go to the “wizard-generated code” of the IDE. Visual Studio .NET allows the automation of many of the tasks outlined so far, but underneath it is doing the same work. When something goes wrong, you will need to understand why it went wrong and how to fix it. This understanding will not come from automatically generated code.

The following sample simply showcases some of the features that have been described. Ideally, the strings in the application should be translated and placed in a satellite assembly. As it is, this application shows a flag for each of the selected languages and displays some of the culture-specific information contained in the CultureInfo class. Only one string has been translated. (The translations are from http://www.trigeminal.com/samples/provincial.html, put together by Michael Kaplan). If you want to add more translations, additional strings can be added easily. The application looks like Figure 18.8.

Figure 18.8. Localization showcase.


The complete source for Listings 18.618.8 is located in the Localization directory. The first listing shows a portion of the commands used to build the satellite assemblies.

Listing 18.6. Building Resource Assemblies
. . .
resxgen /i:united-nations.bmp /o:Flags.resx /n:flag
resgen flags.resx flags.resources
resgen globalapp.resx globalapp.resources
resgen strings.resx strings.resources
resgen translation.txt translation.resources

cd bo-CN
del /s *.resx
..
esxgen /i:tibet.bmp /o:Flags.bo-CN.resx /n:flag
resgen flags.bo-CN.resx flags.bo-CN.resources
resgen translation.bo-CN.txt translation.bo-CN.resources
al /out:GlobalApp.resources.dll /c:bo-CN /v:1.0.0.0 /embed:Flags.bo-CN.resources,Flags
.bo-CN.resources,Private /embed:Translation.bo-CN.resources,Translation.bo-CN.resources,Private

cd ..de-AT
del /s *.resx
..
esxgen /i:austria.bmp /o:Flags.de-AT.resx /n:flag
resgen flags.de-AT.resx flags.de-AT.resources
resgen translation.de-AT.txt translation.de-AT.resources
al /out:GlobalApp.resources.dll /c:de-AT /v:1.0.0.0 /embed:Flags.de-AT.resources,Flags
.de-AT.resources,Private /embed:Translation.de-AT.resources,Translation.de-AT.resources,Private

cd ..de-LU
del /s *.resx
..
esxgen /i:luxembourg.bmp /o:Flags.de-LU.resx /n:flag
resgen flags.de-LU.resx flags.de-LU.resources
resgen translation.de-LU.txt translation.de-LU.resources
al /out:GlobalApp.resources.dll /c:de-LU /v:1.0.0.0 /embed:Flags.de-LU.resources,Flags
.de-LU.resources,Private /embed:Translation.de-LU.resources,Translation.de-LU.resources,Private
. . .

For each supported culture (language and country), a .resx is generated that represents the flag. The generated flag .resx file is compiled with resgen into a .resources file, and the translated text in the .txt file is compiled into a .resources file. Finally, the satellite assembly is created, embedding the .resources files that were just created. For many cultures (like with this application), this is quite a process.

After a language/culture has been selected in the application, the code in Listing 18.7 starts:

Listing 18.7. Retrieving Information from the Satellite Assemblies
LanguageIdentifier li = (LanguageIdentifier)languageSelection.SelectedItem;
Text = "GlobalApp " + li.Message;
Thread.CurrentThread.CurrentUICulture = new CultureInfo(li.CultureString);
Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture;
ResourceManager rm = new ResourceManager("Flags", this.GetType().Assembly);
flagImage.Image = (System.Drawing.Image)rm.GetObject("flag");
rm = new ResourceManager("Translation", this.GetType().Assembly);
translatedText.Text = (string)rm.GetObject("question");

The first line extracts the LanguageIdentifier object. This is just a simple object that encapsulates the string that is to be displayed on the ComboBox and the culture string associated with it. Next, you set the CurrentUICulture and the CurrentCulture properties of the current thread based on the selected language. Then, a ResourceManager object is constructed and the “flag” and the translated “question” resources are retrieved. Other than the name of an audio file (used to say the translated phrase in the selected language if available), that is about all that is contained in the satellite assembly. Listing 18.8 shows how different information is formatted specific to the culture selected.

Listing 18.8. Using CultureInfo for Formatting
DateTime now = DateTime.Now;
time.Text = now.ToLongTimeString();
date.Text = now.ToLongDateString();
double d = 1000000.0;
floatNumber.Text = String.Format("{0:N} ", d);
currencyNumber.Text = String.Format("{0:C} ", d);
// Region Info
RegionInfo ri = new RegionInfo(ci.LCID);
metric.Text = Convert.ToString(ri.IsMetric);
countryName.Text = ri.DisplayName;

These simple lines of code perform culture-specific formatting of the data. Notice in the output that even in languages that are similar, the region has different conventions on formatting date, time, and currency. In addition, notice that the text is displayed differently for right-to-left languages. Besides the display, try editing the translated line for any of the Arabic languages. Pressing the right arrow key causes the caret to move left; the left arrow key causes the caret to move right. If you insert some English into the phrase, then the cursor will behave like expected for a Western language, only while in the inserted text.

The last few lines of Listing 18.8 show some of the information contained in the RegionInfo class.

A Sample Using Visual Studio .NET

One of the problems with trying to do without Visual Studio .NET is that building the UI becomes very hard. Simply resizing the dialog box or moving one of the controls becomes a trial-and-error process. It is possible to build all of the satellite assemblies by hand and still use Visual Studio to build the UI. This is a little better, but it means you are constantly switching between Visual Studio and the command line to build the application. With Visual Studio .NET, building a localized application consists of the following steps:

  • Start with a fresh solution and build the UI as you would normally.

  • After the UI has been built, change the Localizable property of the Form to True, shown in Figure 18.9. It is important that the UI essentially be “complete” when starting the localization process. As you select each language, specific files will be generated based on the default. If this default changes, then corresponding changes will need to be made to each of the generated files. If you have generated files for many languages, this process could be tedious and error prone. The default is the resource that will be used in the fallback mode.

    Figure 18.9. Selecting localizable in Visual Studio .NET.


  • Select a language/culture to which you want to localize. The Language/Culture property can be seen just above the Localizable property in Figure 18.9. If this property is selected, a list of possible cultures to select will be presented. You just need to pick one.

  • Assume that you chose “Spanish (Mexico)” in the previous step. This will cause a .resx file to be generated with the same name as your application with the culture name es-MX embedded in the name. As you add bitmaps, strings, and so on, they will be added to the generated .resx file. Figure 18.10 shows what your directory might look like after selecting a few languages.

    Figure 18.10. Generated files after selecting many languages.


  • When you are satisfied, build the project. You will see as part of the build process a Building satellite assemblies... status line appear during the build. Visual Studio is taking each of the .resx files, creating the appropriate directory, and placing the satellite assembly in it.

  • Repeat the previous two steps for each culture that you want to support.

Input Method Editor(s)

Although not directly related to the CLR, it should be noted that standard methods are available for inputting non-Western characters into an application. To do this, you need to install an Input Method Editor for the locale in which you are interested. Figure 18.11 shows what the installation dialog box looks like. You just select the locale in the IME that you want to install. You do not need to reboot. Now the EN in the bottom-right corner of your screen has meaning. Selecting it shows the input methods that are available.

Figure 18.11. Installing input method editors.


After these editors are installed, you can use them to write messages in another language like that shown in Figure 18.12.

Figure 18.12. Using an IME.


This figure shows a Chinese IME that allows you to input the Romanized sound of the character. Numerous other IMEs do handwriting analysis to detect the character, map the keyboard to specific characters, and so on. Almost all of the supported languages have an IME that allows the input of non-Western characters from a standard keyboard and mouse.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.70.203