如何在支持多国语言的程序中正确使用格式化字符串

让程序支持多国语言也叫做国际化( internationalization / i18n )。几乎所有的现代软件开发框架都会提供至少一种国际化解决方案,开发人员也可以使用第三方的 i18n 库,比如 GNU gettext 等。其实现原理也很简单:开发者在编写软件时,使用某个 Native 语言书写代码内部的文本字符串,一般选择使用英语;然后将每个文本字符串都翻译成软件需要支持的其他语言;在软件运行时,每次输出文本字符串都会调用 i18n 模块的接口,模块会根据当前系统的 locale 环境将文本字符串转换成对应的语言版本,这一过程也被称为本地化( localization / l10n )。

下面我们以 gettext 为例,演示如何让一个国际化的命令行程序实现中文本地化输出。

C// myapp.c

#include <stdio.h>
#include <stdlib.h>
#include <libintl.h>
#include <locale.h>

#define _(MSGID) gettext(MSGID)

int main()
{
    // 可以从当前环境变量 LC_ALL 中读取,也可以在代码中显式指定
    setlocale(LC_ALL, "zh_CN");
    bindtextdomain("myapp", "./locale");
    textdomain("myapp");

    // 我们希望此处输出 "你好,世界!\n"
    printf(_("Hello, world!\n"));

    return EXIT_SUCCESS;
}
在 C 代码中使用 gettext 库

使用 xgettextmsginit 命令生成 PO 文件:

SHELLxgettext --keyword=_ --language=C -o myapp.pot myapp.c
msginit --input=myapp.pot --locale=zh_CN --output=myapp.po

编辑生成的 myapp.po 文件,将里面的 msgid 翻译成中文:

msgid ""
msgstr ""
"Language-Team: Chinese (simplified)\n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=GBK\n"
"Content-Transfer-Encoding: 8bit\n"

#: myapp.c:16
#, c-format
msgid "Hello, world!\n"
msgstr "你好,世界!\n"

使用 msgfmt 命令生成 MO 文件:

SHELLmsgfmt --output-file=./locale/zh_CN/LC_MESSAGES/myapp.mo myapp.po

以上操作也可以通过软件 Poedit 完成。

我们在开发过程中往往输出的文本是通过格式化字符串生成的,因此 msgid 中通常会包含格式化占位符。此时就要注意了,在不同语言中,语序往往并不一致,特别是中文介词两侧的语素往往和英文是相反的。比如:

Cprintf("completed %d of %d", numerator, denominator);

而用中文表达则是:

Cprintf("已完成 %d 分之 %d", denominator, numerator);

这时可以用 %n$ 作为前缀来表示该占位符引用的是第几个参数,这里 n 是起始值为 1 的整数。那么上例中的格式化字符串可以写成 "completed %1$d of %2$d" ,翻译成中文就是 "已完成 %2$d 分之 %1$d" 。不过要注意的是,这个特性是 POSIX 扩展,并未包含在 ISO C 标准中。因此 Microsoft Visual C++ 编译器的 C 运行时并不支持该特性,不过微软提供了一组额外的 printf_p 函数族来支持这一特性。

对于大多数现代高级语言,都提供了支持带编号或有命名的格式化占位符。

C# 的 String.Format() 方法支持带编号的格式化占位符:

C#String.Format("completed {0} of {1}", denominator, numerator);

Python 字符串的 format() 方法同时支持带编号和有命名的格式化占位符:

PYTHON"completed {0} of {1}".format(denominator, numerator)
"completed {denominator} of {numerator}".format(
    denominator=denominator, 
    numerator=numerator)

PHP 的字符串格式化函数 sprintf() 支持 POSIX 扩展风格的格式化占位符:

PHPsprintf('completed %1$d of %2$d', $denominator, $numerator);

特别要注意的是,不要在代码中使用部分高级语言提供的字符串插值( String interpolation )特性。因为这种字符串会动态格式化,运行时不会保留字面上的静态字符串,导致不能被正确翻译。所谓「字符串插值」就是在字符串内部可以直接使用变量名来拼接字符串。支持该特性的高级语言包括但不限于:

以 PHP 为例,它的字符串插值如下:

PHP$denominator = 1;
$numerator = 2;
echo "completed $denominator of $numerator";