在 Java 中使用 GNU gettext 实现本地化(L10N)

Java 本身已经可以很好地国际化(I18N),SUN 网站上也有关于国际化的[教程][文档],其中也包含了如何进行本地化(L10N)的过程。这里所谈的本地化指的是显示出来的字符串的本地化,不涉及数字、货币、日期时间等。Java 本身是如何进行本地化的呢?看一个简单的例子:

  1. I18NExample.java:
  2.  
  3. package org.supermmx.example.i18n;
  4.  
  5. import java.util.Locale;
  6. import java.util.ResourceBundle;
  7. import java.awt.BorderLayout;
  8. import javax.swing.JFrame;
  9. import javax.swing.JButton;
  10.  
  11. public class I18NExample extends JFrame {
  12.  
  13.     public I18NExample(String[] args) {
  14.        
  15.         String language;
  16.         String country;
  17.  
  18.         if (args.length != 2) {
  19.             language = new String("en");
  20.             country = new String("US");
  21.         } else {
  22.             language = new String(args[0]);
  23.             country = new String(args[1]);
  24.         }
  25.  
  26.         Locale currentLocale = new Locale(language, country);
  27.  
  28.         ResourceBundle bundle = ResourceBundle.getBundle("i18n",
  29.                                            currentLocale);
  30.  
  31.         setDefaultCloseOperation(EXIT_ON_CLOSE);
  32.  
  33.         setTitle(bundle.getString("title"));
  34.         JButton helloButton = new JButton(bundle.getString("greetings"));
  35.         JButton byeButton = new JButton(bundle.getString("farewell"));
  36.  
  37.         getContentPane().add(helloButton, BorderLayout.CENTER);
  38.         getContentPane().add(byeButton, BorderLayout.SOUTH);
  39.  
  40.         setSize(200, 150);
  41.         setVisible(true);
  42.     }
  43.  
  44.     public static void main(String[] args) {
  45.         new I18NExample(args);
  46.     }
  47. }

  1. native2ascii.exe -encoding UTF-8 i18n_zh_CN.properties.orig > i18n_zh_CN.properties

这里用 UTF-8 编码,你可以选择其他编码,比如 GB2312 或者 GBK。

然后编译运行:

  1. javac -d . I18NExample.java
  2. java -classpath . org.supermmx.example.i18n.I18NExample zh CN

结果如图所示:

我能看到的唯一好处就是不用修改代码就可以修改显示出来的信息,但是在软件开发过程中,只修改显示信息而再次发布的次数会有多少呢?但带来的问题可能更多:

  • 开发者不能直接在代码中看到应该出现的字符串是什么样,不管是英文的还是其他语言的,得去英文的 properties 文件去看看才知道。
  • 开发者需要手动作很多事情,每一个字符串都需要定义一个键(Key),这样在程序中需要引用的键也就很多,很容易拼写错误、不容易改名等等不方便的地方。
  • 翻译者没有很好的工具跟踪信息修改,比如两次发布之间有哪些信息增加、改动或者删除了,只能依靠英文 properties 文件的 diff 来做,很难保持同步。似乎没有看见这样的工具。
  • properties 文件必须使用 ISO-8859-1 编码,所以对于大多数语言来说,必须要 native2ascii 将翻译语言本身的编码转换成 Unicode 转义代码才能以 ISO-8859-1 编码存储。这对翻译者要求有些高。

对于第 4 条来说,问题不大,只要维护一份源编码的,在编译项目的过程中加一步进行自动进行转换,再把转换出来的资源文件打包。翻译者只需要提交原始的翻译即可。

Linux 上基本上都是使用 [GNU gettext] 来进行本地化。它的基本思想就是在源代码中用某种方式标记出要本地化的字符串,当然一般情况下都是英文的字符串,然后根据这些标记把字符串提取出来,以某种方式(.POT)存下来,然后本地化人员可以对照这些字符串直接进行翻译(.PO),再转换二进制形式(.MO)。运行时根据运行的区域(locale)信息读取合适语言的相关字符串然后进行处理或者显示出来。这一套完整的流程是针对 C 程序的,这里不做具体解释。它也可以支持其他很多种语言,也包括 Java,但并不完整,需要再额外作点工作,和语言本身的国际化支持要结合起来使用。下面我们把上面的例子改写一下:

  1. GettextI18NExample:
  2.  
  3. package org.supermmx.example.i18n;
  4.  
  5. import static org.supermmx.example.i18n.GettextI18NUtils._;
  6.  
  7. import java.util.Locale;
  8. import java.util.ResourceBundle;
  9. import java.awt.BorderLayout;
  10. import javax.swing.JFrame;
  11. import javax.swing.JButton;
  12.  
  13. /**
  14.  * Gettext I18N Example
  15.  *
  16.  * @author SuperMMX
  17.  */
  18. public class GettextI18NExample extends JFrame {
  19.  
  20.     public GettextI18NExample(String[] args) {
  21.        
  22.         String language;
  23.         String country;
  24.  
  25.         if (args.length != 2) {
  26.             language = new String("en");
  27.             country = new String("US");
  28.         } else {
  29.             language = new String(args[0]);
  30.             country = new String(args[1]);
  31.         }
  32.  
  33.         Locale currentLocale = new Locale(language, country);
  34.  
  35.         Locale.setDefault(currentLocale);
  36.  
  37.         setDefaultCloseOperation(EXIT_ON_CLOSE);
  38.  
  39.         setTitle(_("I18N Example"));
  40.         JButton helloButton = new JButton(_("Hello"));
  41.         JButton byeButton = new JButton(_("Goodbye"));
  42.  
  43.         getContentPane().add(helloButton, BorderLayout.CENTER);
  44.         getContentPane().add(byeButton, BorderLayout.SOUTH);
  45.  
  46.         setSize(200, 150);
  47.         setVisible(true);
  48.     }
  49.  
  50.     public static void main(String[] args) {
  51.         new GettextI18NExample(args);
  52.     }
  53. }
  54.  
  55. GettextI18NUtils.java:
  56.  
  57. package org.supermmx.example.i18n;
  58.  
  59. import java.util.Locale;
  60. import java.util.ResourceBundle;
  61.  
  62. /**
  63.  * Gettext I18N Example
  64.  *
  65.  * @author SuperMMX
  66.  */
  67. class GettextI18NUtils {
  68.     private static ResourceBundle bundle =
  69.         ResourceBundle.getBundle("gettext_i18n");
  70.  
  71.     public static String _(String s) {
  72.         return bundle.containsKey(s) ? bundle.getString(s) : s;
  73.     }
  74. }

  1. # 提取需要翻译的字符串
  2. xgettext -k_ -o gettext_i18n.pot -L java GettextI18NExample.java
  3.  
  4. # 为某种语言产生翻译文件
  5. # 第一次使用 msginit
  6. msginit -i gettext_i18n.pot -l zh_CN -o gettext_i18n_zh_CN.po
  7. # 以后使用 msgmerge
  8. msgmerge -U gettext_i18n_zh_CN.po gettext_i18n.pot
  9.  
  10. # 产生 Java 资源文件
  11. msgcat -p -o gettext_i18n_zh_CN.properties gettext_i18n_zh_CN.po

所产生的 .pot.po 文件的具体格式可以查看 GNU Gettext 的手册。最后生成的 .properties 资源文件以原本的字符串作为键,值是 \uXXXX 形式的语言内容。

  1. javac -d . GettextI18NExample.java
  2. java -classpath . org.supermmx.example.i18n.GettextI18NExample zh CN

结果如图所示:

那使用 GNU gettext 有什么好处呢?

  • 开发者可以在源码中直接写出需要的字符串,不需要在 properties 文件中写一次,再用一个 KEY 去引用。
  • 开发者很容易修改字符串,也不会因为 KEY 拼写错误而在运行时找不到资源,避免很多不必要的错误。
  • 每次发布之前,开发者或者翻译者自己可以运行 xgettextmsgmerge 将新的字符串合并到各语言的 PO 文件中,翻译者就可以直接对原有的翻译进行更新。
  • 已经有很多 PO 的编辑工具或者在线翻译工具,大大减轻了翻译者的工作,增强社区翻译的能力。

上面使用的都是 properties 文件作为资源,GNU gettext 也可以支持扩展 java.util.ResourceBundle 的类资源,如下所示:

  1. msgfmt --java2 -r gettext_i18n -d . -l zh_CN gettext_i18n_zh_CN.po

这样它会生成 gettext_i18n_zh_CN.class,具有同样的效果。

[Gettext Commons] 跟上面用到的方法类似,但提供了更好的[复数]支持,不过这就必须使用 msgfmt 生成类形式的 ResourceBundle。我们把程序再改写一下:

  1. package org.supermmx.example.i18n;
  2.  
  3. import org.xnap.commons.i18n.I18nFactory;
  4. import org.xnap.commons.i18n.I18n;
  5.  
  6. import java.util.Locale;
  7. import java.util.ResourceBundle;
  8. import java.awt.BorderLayout;
  9. import javax.swing.JFrame;
  10. import javax.swing.JButton;
  11.  
  12. /**
  13.  * Gettext I18N Example
  14.  *
  15.  * @author SuperMMX
  16.  */
  17. public class GettextCommonsI18NExample extends JFrame {
  18.  
  19.     private I18n i18n;
  20.  
  21.     public GettextCommonsI18NExample(String[] args) {
  22.        
  23.         String language;
  24.         String country;
  25.  
  26.         if (args.length != 2) {
  27.             language = new String("en");
  28.             country = new String("US");
  29.         } else {
  30.             language = new String(args[0]);
  31.             country = new String(args[1]);
  32.         }
  33.  
  34.         Locale currentLocale = new Locale(language, country);
  35.  
  36.         i18n = I18nFactory.getI18n(this.getClass(), "gettext_commons_i18n", currentLocale);
  37.  
  38.         setDefaultCloseOperation(EXIT_ON_CLOSE);
  39.  
  40.         setTitle(i18n.tr("I18N Example - Gettext Commons"));
  41.  
  42.         JButton helloButton = new JButton(i18n.tr("Hello"));
  43.         JButton byeButton = new JButton(i18n.tr("Goodbye"));
  44.  
  45.         getContentPane().add(helloButton, BorderLayout.CENTER);
  46.         getContentPane().add(byeButton, BorderLayout.SOUTH);
  47.  
  48.         setSize(300, 150);
  49.         setVisible(true);
  50.     }
  51.  
  52.     public static void main(String[] args) {
  53.         new GettextCommonsI18NExample(args);
  54.     }
  55. }

  1. # 提取
  2. xgettext -ktrc -ktr -kmarktr -ktrn:1,2 -L java -o gettext_commons_i18n.pot GettextCommonsI18NExample.java
  3.  
  4. # 翻译文件
  5. msginit -i gettext_commons_i18n.pot -l zh_CN -o gettext_commons_i18n_zh_CN.po
  6. msgmerge -U gettext_commons_i18n_zh_CN.po gettext_commons_i18n.pot
  7.  
  8. # 产生 .properties
  9. msgcat -p -o gettext_commons_i18n_zh_CN.properties gettext_commons_i18n_zh_CN.po

  1. javac -classpath .:gettext-commons-0.9.1.jar -d . GettextCommonsI18NExample.java
  2. java -classpath .:gettext-commons-0.9.1.jar org.supermmx.example.i18n.GettextCommonsI18NExample zh CN

结果如图所示:

Gettext Commons 还带了 [Maven] plugin 和 [Ant] task,可以在 Maven 和 Ant 中直接使用,而不用象上面一样用命令行。

本文所附带的例子经整理以后,可以直接下载:[i18n.tar.gz]

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <blockquote>
  • You can use BBCode tags in the text.
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>. The supported tag styles are: <foo>, [foo].
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
             __     __             ____                  __        __        
_ __ ___ \ \ / / __ __ | _ \ _ _ ___ \ \ / / __ _
| '_ ` _ \ \ \ / / \ \ /\ / / | |_) | | | | | / _ \ \ \ /\ / / / _` |
| | | | | | \ V / \ V V / | __/ | |_| | | __/ \ V V / | (_| |
|_| |_| |_| \_/ \_/\_/ |_| \__,_| \___| \_/\_/ \__, |
|_|
Enter the code depicted in ASCII art style.