Welcome!

PowerBuilder Authors: Chris Pollach, Yeshim Deniz, Jayaram Krishnaswamy, Kevin Benedict, Avi Rosenthal

Related Topics: PowerBuilder

PowerBuilder: Article

Internationalizing PowerBuilder Applications with Resource DLLs Part 2

Internationalizing PowerBuilder Applications with Resource DLLs Part 2

In Part 1 of this article (PBDJ, Vol. 9, issue 4) we discussed creating resource DLLs using the open-source LCC-WIN32 C compiler, maintaining string phrases in its IDE, and utilizing the compiled resource DLLs in our PowerBuilder application. Although that provides the basic technical foundation for internationalizing an application, it still doesn't cover many of the real-life issues that come up during the internationalization process, which is what we'll cover in Part 2.

Clarity of Code
One thing that became obvious early on is that using only resource IDs without indicating to the programmer what sort of text they refer to quickly makes it very unreadable. Imagine opening a window in PowerBuilder and instead of seeing labels on the command buttons, text boxes, etc., all you see are numeric IDs. No one would know which is the OK, Cancel, or Print button without cross-referencing the numeric ID back to the master list of corresponding strings.

We hit on a simple solution in which each resource ID is followed by the corresponding English description (separated by a comma). This description is there purely for documentation and clarity (it gets discarded at runtime), but it makes it easy to know which resource ID will be used to internationalize a particular control, as well which string phrase it actually refers to. Figure 1 is an example of this approach. This philosophy permeates all the APIs of our internationalization engine that, in most cases, require not only a resource ID but the corresponding English description for documentation.

For example, instead of something cryptic:

This.Text = g_cc_language.of_get_desc(1)

we prefer to use an API that looks like:

This.Text = g_cc_language.of_get_desc(1,"OK")

This tells you right away that at runtime, resource ID #1 will be fetched and that it corresponds to the English word "OK" (although at runtime, of course, it will be fetched from an internationalized resource DLL).

It's a little bit of extra work for the programmer, but once again, it creates much more readable and understandable code and pays off handsomely in the long run.

Productivity
PowerBuilder's biggest strength is its legendary productivity, and we didn't want to impede that by hard coding all the internationalization logic in the actual code. Therefore, we decided to embed the required resource ID for each on-screen item directly in its text label, which then gets parsed at runtime and replaced with the string fetched from the resource DLL corresponding to the aforementioned ID. This simple approach allowed us to internationalize our app using the regular PowerBuilder GUI builder, which makes it as fast as maintaining a unilingual application.

Any programmer who opens a window (like the one in Figure 1) knows instantly what each object is supposed to display at runtime; the embedded resource ID at the beginning of each description allows each object to extract it at runtime from within its constructor and request the corresponding string from the currently loaded resource DLL. It's the best of both worlds: no hard coding of string descriptions in the PowerBuilder code (everything is entered graphically) and automatic translation at runtime from within each object's constructor.

We use this "resource_id,description" approach everywhere, including objects embedded in DataWindows (column headings, columns with list box/combo box/radio button edit masks, etc.). Our internationalization classes are responsible for extracting the resource ID at runtime and replacing the text with the internationalized phrase from the resource DLL.

Passing Parameters to Strings
Prior to internationalization we had messages such as "There are %s %s waiting to be processed" and we would pass it an array of arguments (e.g., {"34","stores"}). This sort of approach has to be discarded right away when dealing with internationalization. First, make it a point to pass only numbers and dates as parameters but never words (especially nouns). The reason is that in many languages the spelling of other words (verbs and adjectives, in particular) changes depending on the gender of the noun (as well as whether it's plural or singular).

We don't have this issue in English, but those familiar with Romance and Slavic languages will notice it immediately. Therefore, such phrases have to be replaced with their multiple equivalents where all the nouns are hard-coded and only the numeric arguments are left, e.g., "There are %s stores waiting to be processed." From a grammatical point of view, this is the only way to reliably and properly translate strings into other languages.

How to Work Around the Lack of Layout Managers in PB
One of the most powerful features of cross-platform toolkits (e.g., Java/Swing, Java/SWT, or Qt) is the ability to automatically resize and reposition controls and labels based on the length of the string phrase they're displaying. Since those toolkits were designed with internationalization in mind, they estimate the minimum required width necessary to fully display internationalized text. Based on programmer specifications, they then reposition all the controls dynamically to account for those different widths, which makes internationalizing an application that much easier.

Unfortunately, in PowerBuilder we don't have layout managers yet (although nothing stops Sybase from adding them in the future, maybe as a PFC enhancement) nor do we have any built-in functionality for estimating label widths depending on the text they're displaying. However, both of these shortcomings can be corrected by using Windows APIs and properly spacing out your onscreen controls to allow for more room between them (which is nowhere near as flexible as a layout manager, but it's the best we can do).

To get the label width (in pixels) required to display a piece of text, the following Windows APIs are required:

Function ulong GetDC(ulong hWnd) Library
"USER32.DLL"
Function ulong SelectObject(ulong hdc, ulong
Wnd) Library "GDI32.DLL"
Function boolean GetTextExtentPoint32A
(ulong hdcr, string lpString,
long nCount, ref s_os_size
size) Library "GDI32.DLL"
Function long ReleaseDC(ulong hWnd, ulong
hdcr) Library "USER32.DLL"
Listing 1 provides an example of how to use these APIs.

Also, please note that these APIs return only the size of the text. If controls also have another graphic object embedded in them besides a label (check box, radio button), add a hard-coded constant to the width to display it properly.

Spacing of Controls
As a rule of thumb we decided to leave around 50-100% more horizontal space between controls than in the original English version. In most cases, that's enough for all the labels, check boxes, radio buttons, etc., to resize themselves at runtime and not overlap. You should also increase the base width of your ancestor command button by at least 50% (usually we don't resize the width of a command button at runtime, so it's better to make it larger by default to begin with).

Occasionally, there are some places where the translated description is just too long and then we have to go back to the translator to shorten it, but about 95% of the time this simple approach is good enough. I'd like to stress once again that layout managers are a much better solution and I look forward to seeing them introduced in a later version of PowerBuilder. Until that day arrives, adding more space between controls is the only alternative.

Also, we banned placing labels next to fields:

Label: Field

and instead always resort to placing labels above the fields they describe:

Label:
Field

This allows the labels more room to resize themselves.

The Trouble with DataWindows
The Windows APIs described previously require a handle to a "real" Windows object in order to properly estimate the width of the control. Based on its font family, font size, font weight, and additional parameters (i.e., whether the text is italic or not), it returns the minimum width required to fully display the text. As you know, a runtime DataWindow is basically rendered as a bitmap with an edit control on top. In design mode we place check boxes, radio buttons, etc., on a DataWindow, but at runtime they're rendered as a bitmap; there's no "real" Windows object to pass a handle to.

We worked around this problem in our framework by using a hidden popup window with a statictext and passing a handle to it whenever we needed to translate an item embedded in a DataWindow. The hidden statictext would be set with the same font, font size, and font weight as the DataWindow item it represented in order to get a proper calculation from the Windows APIs. It was somewhat of a hack but worked very well.

Of course, dealing with DataWindows means you'll need to parse all the objects in it at runtime, including text, columns (you'll need to write different code to account for the different edit styles on a DataWindow column), groupboxes, command buttons, etc. Just as with regular controls, the "resource ID, description" description should be entered on each of them during design mode. It can then be passed to the internationalization class to extract it and replace it with translated text.

Handling Text Justification
The Windows APIs can help you estimate the width required to fully display a control's text. However, you'll need to perform extra work to also adjust the X position of the control depending on whether it's right- or left-justified or centered. If it's left-justified, leave the X position unchanged and just change the width of the control. If it's right-justified, change its width first, then change the left-hand X position so the right-hand edge of the control is still in the same X position as before. In the case of centered controls, change the width first and then modify the X position so the center of the newly resized control is still in the same spot it was in prior to internationalization.

The same principles apply to controls embedded in a DataWindow data object; you'll need to modify them via the appropriate PowerBuilder Modify() commands.

Accept the Things You Cannot Change
Many of the built-in PowerBuilder functions [e.g., GetFileOpenName()] are basically wrappers around existing Windows dialogs. Since these are external to PowerBuilder and your application, you have no control over which language they will appear in, as that depends on which language-specific version of Windows a user has installed on his or her PC. Make sure your trainers and users understand this limitation and make it part of both the documentation and training, otherwise you'll be receiving many defect reports about certain dialogs not getting internationalized by your application. The same issue applies to any Windows APIs you might be using (e.g., popping up the Windows dialog to select a folder), as they'll obviously appear in the base Windows language as well.

Avoid Hard Coding Region-Specific Edit Masks
As a matter of principle you should always use "[date]", "[shortdate]", or "[time]" edit masks when displaying dates or times. This will tell PowerBuilder to automatically display them using the user's Regional Settings from Windows. The same thing applies to any currency amounts: avoid hard coding a dollar sign "$" in front of amounts; use the "[currency]" edit mask instead.

Handling Hard-Coded PowerBuilder VM Messages
Theoretically, Sybase provides a wide range of localized deployment DLLs. However, these are often not available for particular EBF (emergency bug fix) builds, and also force your application to display only one language instead of being able to dynamically select the required language at startup (as when you're deploying one common set of EXEs and PBDs and loading different resource DLLs at runtime for the different supported languages). Therefore, as a workaround, some of the most common messages displayed by the PowerBuilder VM can be either overridden or intercepted.

The two most important ones are:

1.   The "Ready" message in the status bar: You can modify it at runtime with the internationalized version of that word by modifying the application's MicroHelpDefault property

2.   The "Item failed validation" message displayed by PowerBuilder within the DataWindow's ItemError event: To internationalize it, modify this event in your ancestor DataWindow to display your own internationalized version of this error message and avoid displaying the default PowerBuilder one by using RETURN 1 (or any value greater than 0) in that event.

Seeing It All In Action
I've updated the LANGUAGE.PBL example from Part 1 of this article to include samples of using the Windows APIs to estimate widths of text labels. That should provide a good starting point for building your own customized internationalization framework that can be integrated either into the standard PFC libraries or into any in-house custom framework that many PowerBuilder shops have developed over the years. The updated code can be downloaded from www.sys-con.com/pbdj/source/index.html.

Please check in particular the n_cst_language.of_get_object_size_specs() function that covers many of the topics discussed in this article, as well as the u_cbx_language constructor code (see Listing 2) that explains how to translate and resize an internationalized control at runtime.

Summary
In Part 2, we covered some of the most common issues that need to be addressed before internationalizing an application, i.e., the passing of runtime parameters to strings and avoiding invalid ones (especially nouns) due to the complications they create for translators.

We also covered some of the changes that will need to be made to the application's user interface to make it more accommodating when faced with different widths of controls (depending on which language the user is running in) and different text justification methods. As mentioned before, these are basically workarounds to compensate for the lack of layout managers in PowerBuilder; as soon as Sybase includes them in a future update, any internationalized application should use them instead. If you need more information about layout managers, I recommend the following links:

  • http://eclipse.org/articles/Article-Understanding%20Layouts/Understandin...
  • http://java.sun.com/docs/books/tutorial/uiswing/overview/layout.html

    These two links should give you a good idea why layout managers are more powerful and flexible than the standard PFC resize service (in particular, when dealing with internationalized applications). I've submitted a product enhancement request for this to Sybase so let's hope this makes it into PowerBuilder 10 (or maybe even 9.5 if we're lucky).

    Last, but not least, we've covered some of the more obscure issues such as handling hard-coded PowerBuilder VM messages. Depending on how your application is structured, you might also need to develop your own framework to internationalize database error messages from stored procedures (if you use them). In our case we just mapped each database message ID to a corresponding resource ID and used that internationalized text instead.

    Happy internationalization!

    Acknowledgment
    Thanks to Roy Kiesler and the rest of TeamSybase on the PowerBuilder newsgroups for tracking down the Windows APIs.

  • More Stories By Jacek Furmankiewicz

    Jacek Furmankiewicz is a PowerBuilder R&D supervisor at the Montreal office of STS Systems, an NSB company. His team is responsible for developing applications for some of the largest retailers in North America and Europe. Jacek has been using PowerBuilder since version 4.0 in Sybase, Oracle, and Microsoft SQL Server environments.

    Comments (0)

    Share your thoughts on this story.

    Add your comment
    You must be signed in to add a comment. Sign-in | Register

    In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.