 | 级别: 初级 Shu Fang Rui (shufangrui@gmail.com), 软件工程师 Hu Wei Hong (nobleoliver@hotmail.com), 软件工程师
2006 年 3 月 13 日 本文章将介绍嵌入式设备的全球化解决方案的两个方面 —— 国际化和本地化。对于国际化来说,要学习如何设计代码的结构,以支持多种语言。对于本地化来说,要学习如何定制数字、日期、时间以及货币格式。
Internet 的迅速成长为企业实现跨国界开拓了不可预测的机会,多语言和多平台的软件产品开始在全球性企业的成功中扮演重要角色。虽然最初的 Web 开发主要基于英语,但可以理解的是,不说英语的人仍然喜欢看到使用其本国语言的软件界面,他们对自己的文化和国家习惯很敏感。结果就是,不断增长的全球化需求不可避免地落在了电子商务应用程序上。
在嵌入设备上实现全球化支持时,开发人员面对的挑战是如何利用运行库和资源的有限支持来满足不同的全球化需求。在这里,我们将在 Java™ 2 平台微型版(J2ME)平台上解决这些挑战。
一般来说,要开发全球化的应用程序,需要采取几个步骤,我们将这些步骤分成两组:国际化和本地化。国际化意味着应用程序的设计和开发应该足够灵活,可以满足多种语言和文化差异的需求。为了使该需求可行,应该将文本数据与代码分离,并把与文化习惯有关的代码和处理业务逻辑的基本应用程序分离。这个基础的全球化应用程序叫做单一可执行程序 (single executable)。它可以在所有语言版本中运行,可以调用对应的文本数据以及与文化相关的代码,生成满足当前用户地区需要的用户界面。
本地化代表通过转换文本数据、设计与文化相关的配置(例如名称和地址格式),生成支持全球化特性的代码的过程。可以根据不同地区将转换和配置文件保存在不同的目录或文件夹中,这样全球化应用程序就可以随时调用它们。如果使用统一的格式提供文本数据,例如 OASIS XML 本地化交换文件格式 (XLIFF),那么这会极大地提高全球化的效率。
国际化:语言支持
全球化的一项关键特性就是处理多种语言的能力。语言支持通常与用户界面的设计密切相关。下面是给用户界面添加多语言支持的方式。
在支持全球化的应用程序中,通常有一个功能叫做选择语言,它将根据用户的选择在用户界面上显示不同的语言。可以在 Java.util.ResourceBundle 的帮助下实际对多语言特性的支持。通过使用 ResourceBundle 实现的 Messages 类,可以很容易地从相应的属性文件中获得多语言文本。清单 1 提供了 Messages 的详细信息。在 Messages 类中有三个方法:setBundle()、getString() 和 getLocale()。使用 setBundle(String propertyFileName) 类可以把 ResourceBundle 与新的属性文件和对应的地区关联起来。在使用 getString(String key) 时,该类将返回与相应地区有关的文本。
清单 1. Messages 类的源代码
public class Messages {
/**
* Property file name
*/
private static String propertyFileName;
/**
* ResourceBundle associated with the property file name
*/
private static ResourceBundle resourceBundle;
/**
* Locale related with the selected language
*/
private static Locale locale;
static {
propertyFileName = UIConstant.EN_PROPERTY_NAME;
resourceBundle = ResourceBundle.getBundle(propertyFileName);
locale = Locale.US;
}
/**
* The default constructor, preventing others to generate
* instance of this class.
*/
private Messages() {
}
/**
* Set a new property file associated with the newName.
* @param newName Name of the new property file
*/
public static void setBundle(String newName) {
propertyFileName = newName;
resourceBundle = ResourceBundle.getBundle(propertyFileName);
if (UIConstant.EN_PROPERTY_NAME.equals(newName)) {
locale = Locale.US;
} else if (UIConstant.ZH_PROPERTY_NAME.equals(newName)) {
locale = Locale.SIMPLIFIED_CHINESE;
} else if (UIConstant.FR_PROPERTY_NAME.equals(newName)) {
locale = Locale.FRANCE;
}
}
/**
*
* @param key Key in the property file for fetching value.
* @return String value associated with the key value
*/
public static String getString(String key) {
String result = "";
try {
result = resourceBundle.getString(key);
} catch (MissingResourceException e) {
e.printStackTrace();
}
return result;
}
/**
*
* @return the Locale associated with the property file.
*/
public static Locale getLocale() {
return locale;
}
}
|
清单 2 设置标签上的文本,显示字符串 “user ID”。
清单 2. 设置标签上的文本
String userIDStr = Messages.getString("label_userID"); //$NON-NLS-1$
userIDLabel.setText(userIDStr)
|
至于如何在用户界面类上使用 Messages 类的详细信息,我们在 图 1 中演示了用户界面,并在 清单 3 中演示了类框架。要获得细节信息,请参阅本文末尾的 下载。
图 1. 带有语言选择的用户界面
清单 3. 用户界面类的框架
public class LogonUI {
/**
* The only instance of LogonUI class
*/
private static final LogonUI instance;
// The definition of all UI components comes following
á?ˉ
static {
instance = new LogonUI();
}
/**
* Private constructor, make sure the only instance
* of the LogonUI class.
* Create the components on LogonUI
*/
private LogonUI() {
//Create the shell object in this UI
createShell();
//Create the logo image
createImage();
//Create the userID and password-related components
createInputArea();
//Create the select language combo
createCombo();
//Create the login and exit button
createButton();
//Set the text on this UI
setUIText();
}
//The method definition createShell(), createImage(),
createInputArea(),createButton() come following
"//
/**
* private method, used to create the language combo
*
*/
private void createCombo() {
//create select language label and arrange it properly
lanLabel = new Label(shell, SWT.LEFT);
"/
//create the combo box and arrange it properly
lanCombo = new Combo(shell, SWT.CENTER | SWT.READ_ONLY);
" lanCombo.addSelectionListener(new SelectionListener() {
public void widgetSelected(SelectionEvent event) {
Combo cb = (Combo) event.getSource();
selectedLan = cb.getText();
if (UIConstant.ENGLISH.equals(selectedLan)) {
Messages.setBundle(UIConstant.EN_PROPERTY_NAME);
} else if (UIConstant.CHINESE.equals(selectedLan)) {
Messages.setBundle(UIConstant.ZH_PROPERTY_NAME);
} else {
Messages.setBundle(UIConstant.FR_PROPERTY_NAME);
}
}
public void widgetDefaultSelected(SelectionEvent event) {
}
});
data = new FormData();
data.top = new FormAttachment(pwdText, 10);
data.left = new FormAttachment(35);
data.right = new FormAttachment(95);
lanCombo.setLayoutData(data);
}
/**
* Used for setting the text displayed on the UI according
* to the selected language.
*/
private void setUIText() {
welcome = Messages.getString("TITLE_WELCOME"); //$NON-NLS-1$
shell.setText(welcome);
userIDStr = Messages.getString("TABLE_USERID"); //$NON-NLS-1$
userIDLabel.setText(userIDStr);
pwdStr = Messages.getString("TABLE_PASSWORD"); //$NON-NLS-1$
pwdLabel.setText(pwdStr);
lanStr = Messages.getString("TABLE_SELECTLANGUAGE"); //$NON-NLS-1$
lanLabel.setText(lanStr);
loginStr = Messages.getString("BUTTON_LOGIN"); //$NON-NLS-1$
loginBtn.setText(loginStr);
exitStr = Messages.getString("BUTTON_EXIT"); //$NON-NLS-1$
exitBtn.setText(exitStr);
}
// The method definition for open(), which is used to show the
LogonUI on the screen
"/
|
请注意,我们把构造函数 LogonUI 声明为 private。在这里,我们试图让 LogonUI 类成为单体模式的实现,因为在应用程序中只存在 LogonUI 类的一个实例。
可以用私有方法 setUIText() 设置 LogonUI 上的所有文本。该方法将从属性文件提取所有要显示的文本。在 SelectionListener 中,还有对 setUIText() 的另一次调用。每次完成语言修改之后,LogonUI 上显示的文本都会发生相应的变化。与 Messages 关联的 ResourceBundle 实例和 Locale 也会变化,因此其他用户界面上显示的所有文本也会相应地变化。
我们还把 setUIText 作为一个方法提取出来,而不是在每个组件自己的创建方法中设置文本;这是从重用的角度进行考虑。而且,在每次语言选择之后,都需要相应地修改文本。通过编写一个方法来修改显示的文本,可以实现这一要求。
本地化
WebSphere® Studio Device Developer 5.7.1 自带了三个库:JCL MIDP 2.0、JCL Foundation 1.0 和 JCL Personal Profile 1.0,每个库都有不同级别的本地化支持。表 1 比较了每个库对本地化的支持。
表 1. WebSphere Studio Device Developer 5.7.1 的三个库对本地化的支持
| JCL MIDP 2.0 | JCL Foundation 1.0 | JCL Personal Profile 1.0 |
|---|
| Java.util.TimeZone | Java.util.Locale | Java.util.Locale | | Java.util.TimeZones | Java.util.ResouceBundle | Java.util.ResouceBundle | | | Java.util.TimeZone | Java.util.TimeZone | | | Java.util.TimeZones | Java.util.TimeZones | | | Java.text.DateFormat | Java.text.DateFormat | | | Java.text.DateFormatSymbols | Java.text.DateFormatSymbols | | | Java.text.NumberFormat | Java.text.NumberFormat | | | Java.text.DecimalFormat | Java.text.DecimalFormat | | | Java.text.DecimatformatSymbols | Java.text.DecimatformatSymbols |
请注意,在 MIDP 2.0 中,对本地化的支持很有限,只有 Foundation 1.0 提供了与 Personal Profile 1.0 相同的本地化支持。
基于 JCL Foundation 1.0,我们将解释如何支持格式化和验证中使用的数字格式、基于不同时区和语言的日期和时间格式,以及基于不同地区的货币格式。
数字格式
几乎世界上的每个国家都使用的是十进制系统(基于 10)。但是,数字的格式各有不同。表 2 显示了不同地区的不同数字格式。
表 2. 不同地区的不同数字格式
| 地区 | 正数格式 | 负数格式 |
|---|
| ar_EG | 12,345.67 | 12,345.67- | | de_GE | 12.345,67 | -12.345,67 | | en_US | 12,345.67 | -12,345.67 | | zh_CN | 12.345,67 | -12.345,67 | | fr_FR | 12 345,67 | -12 345,67 | | fr_CH | 12'345.67 | -12'345.67 | | de_CH | 12'345.67 | -12'345.67 |
数字格式相当琐碎。清单 4 显示了如何根据不同地区对数字进行格式化。
清单 4. 根据地区对数字进行格式化
double width = 12345.67;
NumberFormat nf = NumberFormat.getInstance(Locale.FRANCE);
nf.setMinimumFractionDigits(3);
nf.setMaximumFractionDigits(3);
String result = nf.format(width);
|
请注意,地区是 fr_FR,double 的值大于 1,000,使用 NumberFormat.format() 的格式化字符串会包含空格;这是因为地区 fr_FR 中的数字分组标记是 " "。但是这个空格的 ASCII 值是 160,而通常使用的空格的 ASCII 值是 32。如果正在处理已格式化的字符串,则需要特别注意空格问题。例如,只使用 清单 5 不会得到想要的结果;必须删除已格式化的输出中的空格。
清单 5. 删除字符串中的空格
StringBuffer sb = new StringBuffer(result);
for (int i = 0; i < sb.length(); i++) {
if (' ' == sb.charAt(i)) {
sb.deleteCharAt(i);
}
}
|
必须把 (' ' == sb.charAt(i)) 这行修改成 if(160 == sb.charAt(i)),删除地区 fr_FR 中的空格。
数字验证
对输入数据进行验证在保护软件强壮方面扮演着重要的角色。验证允许应用程序确保用户正确地输入数据。这样,就可以防止错误数据进入业务逻辑。如果验证失败,则应用程序将重新显示用户界面,或显示另一个界面,请求用户正确地输入数据。如果没错,则开始处理数据。
如果应用程序需要一个数字,就必须确保数据符合正确的数字格式。一般来说,对地区敏感的 Java API 可以帮助您完成这项工作,省去了许多工作。但是,在全球化应用程序中,它有点粗糙。如 表 2 所示,不同地区的数字格式看起来各不相同。在需要处理输入的数字时,首先需要验证它。在地区 Locale.US 和 Locale.SIMPLIFIED_CHINESE 中,千位分隔符是 "," (逗号),小数点是 "." (句点)。而对地区 Locale.FRANCE,千位分隔符就变成了 " " (空格),而小数点则变成 "," (逗号)。虽然基本的 Java API 提供了一些类(比如 Java.text.NumberFormat 和 Java.text.DecimalFormat)来做这项工作,但这些类不能确保输入字符串的数字格式有效。(清单 6 通过解析一个字符串显示了 Java.text.NumberFormat 的基本用法。)
清单 6. NumberFormat 类中的解析函数
Number number =NumberFormat.getNumberInstance(locale).parse(str);
if (number instanceof Long){
//Long value
}else{
//Double value
}
|
如果使用地区 US 输入字符串 “0.34.56”,那么解析的结果仍然是 “0.34”;如果输入是 “,455,0.0”,输出会是 “4550”,如果输入是 “234.23ab”,输入会是有效的数字 “234.23”。解析函数在遇到非法字符时会停止工作。所以,需要先验证数字的格式。
验证数字格式
作为示例,清单 7 显示了如何处理地区 fr_FR。因为 fr_FR 允许空格,所以我们将提供两个函数来验证输入。
首先,需要删除输入字符串中的空格。
清单 7. 检查并删除地区 fr_FR 中的空格
/**
* If locale is fr_FR, check the format and delete spaces
* @param number The number getting from input
* @param locale The locale related with the selected language
* @return If the format is valid, return the modified string,
* @else return null
*/
public static String deleteSpaceIfFR(String number, Locale locale) {
if (Locale.FRANCE.equals(locale)) {
// " " should be before ","
//Note: ' 'here is the ASCII value 0X20
if (number.indexOf(',') > 0
&& number.indexOf(' ') > 0
&& number.lastIndexOf(' ') > number.indexOf(',')) {
return null;
}
// " " should be before ","
//Note: ' 'here is the ASCII value 0XA0
if (number.indexOf(',') > 0
&& number.indexOf(160) > 0
&& number.lastIndexOf(160) > number.indexOf(',')) {
return null;
}
//No continuous " " is allowed
if (number.indexOf(" ") > 0) {
System.out.println("No continuous is allowed");
return null;
}
//delete all spaces in the number string
StringBuffer sb = new StringBuffer(number);
for (int i = 0; i < sb.length(); i++) {
if (32 == sb.charAt(i) || (160 == sb.charAt(i))) {
sb.deleteCharAt(i);
}
}
return sb.toString();
} else {
return number;
}
} |
接下来,做具体验证工作,如 清单 8 中所示。
清单 8. 做具体验证
/**
* To check whether a string is a valid number string
*
* @param number the number string
* @param locale the locale selected
* @return whether the number is in valid format
*/
public static boolean validateNumber(String number, Locale locale) {
//empty string
if (null == number || "".equals(number)) {
return false;
} else {
// ensure no invalid character is contained in the number string
for (int i = 0; i < number.length(); i++) {
char c = number.charAt(i);
boolean isallowed =
(c >= '0' && c <= '9') || (c == ',') || (c == '.');
if (!isallowed) {
return false;
}
}
}
//for fr_FR locale
if (Locale.FRANCE.equals(locale)) {
//should not contain "."
if (number.indexOf('.') >= 0) {
return false;
}
//should contain "," once
if (number.indexOf(',') > 0
&& (number.indexOf(',') != number.lastIndexOf(','))) {
return false;
}
//"," can not at the last position of number
if (number.indexOf(',') == (number.length() - 1)) {
return false;
}
//"0" cannot at the first position of number else it appends with
// ","
if (0 == number.indexOf('0') && 1 != number.indexOf(',')) {
return false;
}
}
return true;
} |

 |

|
日期和时间格式
非常常见的还有将在应用程序中遇到 Date 或 Time。Date 和 Time 在全球化应用程序中也扮演着重要角色。如果为全球性公司开发应用程序,那么纽约的用户会看到他所在时区的日期或时间,而巴黎的用户则会看到巴黎时区的日期和时间。用户也可以选择不同的语言来显示日期和时间信息。
以全球化应用程序的时间表示为例。假设某一用户住在纽约。如果他选择简体中文作为显示语言,那么他们看到以中文习惯显示的本地时间。如果他选择英文,时间则会按美国习惯显示。根据不同地区和选中的语言来显示日期和时间相当简单。
现在假设用户是位于纽约的中国人。他想让日期按中文习惯显示;清单 10 显示了该如何做。
清单 10. 用中文显示纽约时区的日期
1 Date date = new Date();
2 //Get the date formatter according to the selected language
3 DateFormat df = DateFormat.getDateInstance(
DateFormat.LONG,Locale.SIMPLIFIED_CHINESE);
4 TimeZone zone = TimeZone.getTimeZone("America/New_York");
5 df.setTimeZone(zone);
6 String dateValue = df.format(date);
|
图 2 显示了与中文相关的用户界面。
图 2. 中文用户界面

如果用户是法国人,想看到以法文显示的日期信息,那么只需把 清单 10 中的第 3 行修改成 DateFormat df = DateFormat.getDateInstance(DateFormat.LONG, Locale.FRANCE); 即可,如 图 3 中所示。
图 3. 法文用户界面

如果想得到 DateTime 格式,只需使用 DateFormat df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT,Locale.FRANCE); 替代上面那行代码行。表 3 显示了不同语言的 DateTime 格式。
表 3. 同一时区中选择的不同语言的 DateTime 格式
| en_US | zh_CN | fr_FR |
|---|
| Dec 19, 2005 2:04 AM | 2005-12-19 ??2:04 | 19 dè?? 2005 02:04 |
在上面,我们介绍了与同一时区使用不同语言相关的 DateTime 格式。接下来,我们将使用同一语言,显示不同时区的 DateTime 格式,如 清单 11 所示。
清单 11. 显示同一语言的不同时区的 DateTime
Date date = new Date();
TimeZone zones[] =
{
TimeZone.getTimeZone("America/New_York"),
TimeZone.getTimeZone("Asia/Shanghai"),
TimeZone.getTimeZone("Europe/Paris")};
DateFormat df =
DateFormat.getDateTimeInstance(
DateFormat.MEDIUM,
DateFormat.SHORT,
Locale.US);
for (int i = 0; i < 3; i++) {
df.setTimeZone(zones[i]);
System.out.println("time:" + df.format(date));
} |
表 4 显示了以上代码的结果。
表 4. 同一语言的不同时区的 DateTime 格式
| America/New_York | Asia/Shanghai | Europe/Paris |
|---|
| Dec 15, 2005 10:09 PM | Dec 16, 2005 11:09 AM | Dec 16, 2005 4:09 AM |
货币格式
货币格式通常由地区、货币名称、货币子单位、货币标记、正数格式、负数格式、货币代码和货币分隔符构成。货币分隔符包括千位分隔符、小数点、小数位置、字段长度和填充字符。
Java.text.NumberFormat 类也支持货币格式。清单 12 显示了如何支持不同地区的货币格式。
清单 12. 货币格式
Locale locale = Locale.FRANCE;
double price = 12345.078;
NumberFormat formatter = NumberFormat.getCurrencyInstance(locale);
System.out.println(formatter.format(price));
|
表 5 显示了不同地区的输出结果。
表 5. 不同地区的不同货币格式
| en_US | zh_CN | fr_FR |
|---|
| $12,345.08 | ?12,345.08 | 12 345,08 ?? |
货币解析
清单 13 显示了如何解析货币。
清单 13. 货币解析
try {
Number number =
NumberFormat.getCurrencyInstance(Locale.FRANCE).parse(
"12 345, 08 ??");
System.out.println(number);
} catch (ParseException e) {
e.printStackTrace();
} |
清单 13 的输出应该是 12345.08,与 price 变量相同。
结束语
虽然嵌入式设备受到内存和屏幕尺寸的限制,但是 J2ME API 提供了一些全球化特性的支持,例如多语言支持、数字格式、日期和时间格式和货币格式。通过添加这些基本特性,嵌入式应用程序会得到在不断成长的全球市场上获得成功的竞争优势。
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| Code sample | wi-globalappssource.zip | 104KB | HTTP |
|---|
参考资料 学习
获得产品和技术
讨论
作者简介  | 
|  | Shu Fang Rui 是上海交大(中国)的研究生。她对无线技术和 Web 服务感兴趣。除了旅行之外,她还喜欢其他各种运动。 |
 | 
|  | Hu Wei Hong 是浙江大学(中国)的研究生。他的兴趣包括 Java 技术和跳舞。 |
对本文的评价
|  |