如何在支持多国语言的程序中正确使用格式化字符串
让程序支持多国语言也叫做国际化( 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;
}
使用 xgettext
和 msginit
命令生成 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
- Ruby
- C#
- Python
- Dart
- JavaScript / TypeScript
- Kotlin
- Julia
- Rust
- Swift
- Scala
以 PHP 为例,它的字符串插值如下:
php$denominator = 1;
$numerator = 2;
echo "completed $denominator of $numerator";