分类
代码

Java 使用 GNU 的 Gettext 工具实现国际化

在 Java 中最常见的实现国际化(i18n)的方式应该是使用 .properties 资源文件。本文将使用另一种方式实现 Java 代码的国际化与本地化(l10n)。
Gettext 是一个可用于多种语言的国际化工具。相比于传统的 key – value 资源文件方式,其不同点有:

使用 properties 的传统方式 GNU gettext
key 是短文本 key 是纯文本形式的未翻译内容
翻译文件通常命名为 resources_locale.properties, 文件内容只能包含 ASCII 文本,非 ASCII 文字会被用 Unicode 编码代替 翻译文件通常命名为resources.local.po,文件内容可以使用任意编码可以包含任意字符
ResourceBundle.getString会在找不到翻译内容时抛出异常 gettext当找不到翻译内容时会返回 key.
不支持单复数及上下文翻译 支持单复数和上下文相关的翻译

下面,将通过一个示例来演示如何在 Java 中使用 Gettext 实现国际化。

一般步骤:

  1. 相关工具下载。
  2. 引入依赖,编写工具类。
  3. 在需要翻译的地方调用相关翻译方法。
  4. 抽取待翻译内容。
  5. 翻译。
  6. 生成翻译后资源文件。
  7. 运行。

相关工具下载。

在 Gettext 主页上可以找到其源代码,*nix 通常自带了相关工具,不需另行下载。当然,也可以在主页上找到用于 Windows 的工具包:https://mlocati.github.io/articles/gettext-iconv-windows.html
我下载的是 64 位绿色版: gettext0.19.8.1-iconv1.14-static-64.zip 解压后建议添加其下 bin 目录到 Path 中,方便后续输入命令。

引入依赖,编写工具类。

首先随便新建一个工程,这里使用的是 Maven 空项目。添加 libintl 的依赖。
如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.youthlin.demo</groupId>
    <artifactId>I18N</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.gnu.gettext/libintl -->
        <dependency>
            <groupId>org.gnu.gettext</groupId>
            <artifactId>libintl</artifactId>
            <version>0.18.3</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.3</version>
                <!-- 指定源码级别,防止每次编辑pom文件后IDEA都会认为源码级别为1.5 -->
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    
</project>

如果你不是使用 Maven, 那么可以搜索下载这个 jar 包。

编写翻译工具类:

package com.youthlin.demo.i18n.util;

import gnu.gettext.GettextResource;

import java.util.ResourceBundle;

/**
 * Created by lin on 2016-09-20-020.
 * Translation
 * 抽象类可以保证不会生成该类的实例,因为我们只需使用静态方法,不需生成对象。
 * 参考:http://www.gted.org/
 */
public abstract class Translation {
    private static ResourceBundle catalog = ResourceBundle.getBundle("Messages");

    public static String __(String msgid) {
        return GettextResource.gettext(catalog, msgid);
    }

    public static String _x(String msgid, String msgctxt) {
        return GettextResource.pgettext(catalog, msgctxt, msgid);
    }

    public static String _n(String msgid, String msgid_plural, long n) {
        return GettextResource.ngettext(catalog, msgid, msgid_plural, n);
    }

    public static String _nx(String msgid, String plural, long n, String context) {
        return GettextResource.npgettext(catalog, context, msgid, plural, n);
    }

    public static ResourceBundle getCatalog() {
        return catalog;
    }

    public static void setCatalog(ResourceBundle catalog) {
        Translation.catalog = catalog;
    }
}

这里,我使用的是 WordPress 中的国际化函数名称,实际上 gettext 习惯上也是使用下划线作为其别名的,因为文字输出相对频繁,因此使用简短的名称。

  • __(msgid)用于普通翻译。
  • _x(msgid,context)用于上下文翻译。如,post 一词,即可表示“文章”,也可表示“发表”,要区分则使用一个上下文字符串标识到底是哪一个意思。
  • _n(msgid,msg_plural,n)用于单复数翻译。当 n 为单数时返回 msgid 的翻译,当 n 为复数时,返回 msg_plural 的翻译。
  • _nx(msgid,msg_plural,n,context)就是上下文相关单复数翻译啦。

在需要翻译的地方调用相关翻译方法。

现在随便试一下输出:

package com.youthlin.demo.i18n;

import com.youthlin.demo.i18n.util.Translation;

import java.text.MessageFormat;
import java.util.Locale;
import java.util.ResourceBundle;

import static com.youthlin.demo.i18n.util.Translation.__;
import static com.youthlin.demo.i18n.util.Translation._n;
import static com.youthlin.demo.i18n.util.Translation._x;
import static com.youthlin.demo.i18n.util.Translation._nx;

/**
 * Created by lin on 2016-09-20-020.
 * <p>
 * 主页:https://www.gnu.org/software/gettext/<br>
 * Windows工具包:https://mlocati.github.io/articles/gettext-iconv-windows.html<br>
 * 用法:https://www.gnu.org/software/gettext/manual/html_node/Program-Index.html<br>
 * Java示例:https://www.gnu.org/software/gettext/manual/html_node/Java.html#Java<br>
 * 一般步骤:<ol>
 * <li>编写程序,需要翻译的地方使用工具类的方法</li>
 * <li>
 * 抽取 .pot 翻译模板文件<pre>xgettext -k__ -k_x:2c,1 -k_n:1,2 -k_nx:4c,1,2 -o resources/lang/messages.pot java/com/youthlin/demo/i18n/Main.java --from-code UTF-8</pre>
 * xgettext 用法:<a href="https://www.gnu.org/software/gettext/manual/html_node/xgettext-Invocation.html#Language-specific-options">xgettext-Invocation.html#Language-specific-options</a>
 * </li>
 * <li>复制模板文件为 .po 文件进行翻译,记得修改 .po 中元数据<pre>"Content-Type: text/plain; charset=<strong>UTF-8</strong>\n"</pre></li>
 * <li>
 * 生成资源文件<pre>msgfmt --java2 -d resources -r Messages -l zh_CN resources/lang/messages_zh_CN.po</pre>
 * msgfmt 用法:<a href="https://www.gnu.org/software/gettext/manual/html_node/msgfmt-Invocation.html">msgfmt-Invocation.html</a>
 * </li>
 * </ol>
 */
public class Main {
    public static void main(String[] args) {
        print();
        Translation.setCatalog(ResourceBundle.getBundle("Messages", Locale.ENGLISH));
        print();
    }

    private static void print() {
        System.out.println(__("Hello,World"));
        System.out.println(_x("post", "a post"));
        System.out.println(_x("post", "to post"));
        System.out.printf(_n("%d comment\n", "%d comments\n", 1), 1);
        System.out.printf(_n("%d comment\n", "%d comments\n", 2), 2);
        System.out.printf(_nx("%d comment\n", "%d comments\n", 1, "another context"), 1);
        System.out.printf(_nx("%d comment\n", "%d comments\n", 2, "another context"), 2);
        //System.out.println(MessageFormat.format(_n("one apple", "{0} apples", 2), 2));
        //System.out.println(__("未翻译内容"));
    }
}

抽取待翻译内容。

使用下载的工具提取翻译字符串。命令用法

xgettext [option] [inputfile]...

网上的示例都是只有最简单的提取普通翻译字符串的方法,没有写如何提取单复数翻译字符串。看了一圈文档终于找到了。使用 -kxxx即可,但具体 xxx 该怎么写呢?
文档中有示例:

For Java: GettextResource.gettext:2, GettextResource.ngettext:2,3, GettextResource.pgettext:2c,3, GettextResource.npgettext:2c,3,4, gettext, ngettext:1,2, pgettext:1c,2, npgettext:1c,2,3, getString.

因此,对应过来我们的工具类,完整的命令就是:(在src/main/目录下运行的示例)

xgettext -k__ -k_x:2c,1 -k_n:1,2 -k_nx:4c,1,2 -o resources/lang/messages.pot java/com/youthlin/demo/i18n/Main.java --from-code UTF-8
  • -kxxx 用于指明抽取哪些函数同时指明第几个参数是什么含义。
  • -o 用于指明输出文件
  • 后面跟着要扫描的 java 源文件
  • 最后使用 –from-code UTF-8 指明编码。

该命令将生成一个翻译模板文件(.pot),内容如下:

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <[email protected]>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-09-21 14:44+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <[email protected]>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"

#: java/com/youthlin/demo/i18n/Main.java:42
msgid "Hello,World"
msgstr ""

#: java/com/youthlin/demo/i18n/Main.java:43
msgctxt "a post"
msgid "post"
msgstr ""

#: java/com/youthlin/demo/i18n/Main.java:44
msgctxt "to post"
msgid "post"
msgstr ""

#: java/com/youthlin/demo/i18n/Main.java:45
#: java/com/youthlin/demo/i18n/Main.java:46
msgid "%d comment\n"
msgid_plural "%d comments\n"
msgstr[0] ""
msgstr[1] ""

#: java/com/youthlin/demo/i18n/Main.java:47
#: java/com/youthlin/demo/i18n/Main.java:48
msgctxt "another context"
msgid "%d comment\n"
msgid_plural "%d comments\n"
msgstr[0] ""
msgstr[1] ""

翻译。

复制模板文件为待翻译文件: messages_zh_CN.po 修改需要的翻译的地方

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR , YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-09-21 11:36+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: YouthLin Chen \n"
"Language-Team: LANGUAGE \n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"

#: java/com/youthlin/demo/i18n/Main.java:23
msgid "Hello,World"
msgstr "你好,世界"

#: java/com/youthlin/demo/i18n/Main.java:24
msgctxt "a post"
msgid "post"
msgstr "文章"

#: java/com/youthlin/demo/i18n/Main.java:25
msgctxt "to post"
msgid "post"
msgstr "发表"

#: java/com/youthlin/demo/i18n/Main.java:26
#: java/com/youthlin/demo/i18n/Main.java:27
msgid "%d comment\n"
msgid_plural "%d comments\n"
msgstr[0] "一则评论\n"
msgstr[1] "%d 条评论\n"

#: java/com/youthlin/demo/i18n/Main.java:28
#: java/com/youthlin/demo/i18n/Main.java:29
msgctxt "another context"
msgid "%d comment\n"
msgid_plural "%d comments\n"
msgstr[0] "1条评论\n"
msgstr[1] "%d 则评论\n"

特别注意需要修改第 17 行文件编码,否则下一步报错。

生成翻译后资源文件。

使用 msgfmt 命令。(在src/main/目录下运行的示例)

msgfmt --java2 -d resources -r Messages -l zh_CN resources/lang/messages_zh_CN.po
  • –java2 表明生成 JDK1.2+ 的class 文件(内部使用的是 Object 数组,如果用 –java 则内部使用的是 Hashtable )。
  • -d resources 保存在 resources 目录
  • -r Messages 生成的类名的 BaseName.
  • -l zh_CN 该翻译文件的语言编码
  • 最后是翻译完的文件。
gettext命令
gettext命令

可能 gettext 使用的编译命令是 1.3 版本的, path 中 javac 是 1.8 的则会报警告。觉得烦的话,可以使用 –source 选项生成 .java 文件再自行编译。(不过这样直接编译还是报不安全,因为public java.util.Enumeration getKeys ()方法没有用泛型)

运行。

运行效果:

运行结果
运行结果

修改合并。

当之后源文件有修改时,可以使用 msgmerge 合并更改,而不需删除原翻译。
比如上述 java 文件最后一行语句去掉注释,直接运行的话是没有翻译的,会显示原文,因此我们打算把新的待翻译内容也抽取出来。
仍用 xgettext 命令抽取,不过你可以保存为另一个 .pot 文件名,当然覆盖也是可以的。
然后合并到已翻译文件:
用法:

msgmerge [options] existfile.po newpot.pot
msgmerge -U resources/lang/messages_zh_CN.po resources/lang/messages_new.pot
  • -U 表示更新已有翻译文件。

然后再进行翻译、生成的步骤即可。

参考连接:


“Java 使用 GNU 的 Gettext 工具实现国际化”上的2条回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注

[/鼓掌] [/难过] [/调皮] [/白眼] [/疑问] [/流泪] [/流汗] [/撇嘴] [/抠鼻] [/惊讶] [/微笑] [/得意] [/大兵] [/坏笑] [/呲牙] [/吓到] [/可爱] [/发怒] [/发呆] [/偷笑] [/亲亲]

Loading提交中

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据