第 4 章. 本地化和国际化 - L10N 和 I18N

4.1. 编程 I18N 兼容应用程序

为了使您的应用程序对其他语言的使用者更有用,我们希望您能编写 I18N 兼容的程序。GNU gcc 编译器和 QT、GTK 等 GUI 库通过对字符串的特殊处理来支持 I18N。使程序与 I18N 兼容非常容易。它允许贡献者快速将您的应用程序移植到其他语言。有关更多详细信息,请参阅库特定的 I18N 文档。

与普遍看法相反,I18N 兼容的代码很容易编写。通常,它只需要用库特定的函数包装您的字符串。此外,请务必允许使用宽字符或多字节字符支持。

4.1.1. 统一 I18N 工作的号召

我们注意到,每个国家/地区的单独 I18N/L10N 工作一直在重复彼此的工作。我们许多人一直在重复且低效地重复造轮子。我们希望 I18N 中的各个主要群体能够像核心团队的职责一样,聚集到一个共同的努力中。

目前,我们希望在您编写或移植 I18N 程序时,将其发送到每个国家/地区的相关 FreeBSD 邮件列表进行测试。将来,我们希望创建无需脏乱的 hack 就能在所有语言中开箱即用的应用程序。

已建立 FreeBSD 国际化邮件列表。如果您是 I18N/L10N 开发人员,请发送您的评论、想法、问题以及您认为相关的任何内容。

4.1.2. Perl 和 Python

Perl 和 Python 具有 I18N 和宽字符处理库。请使用它们来实现 I18N 兼容性。

4.2. 使用 POSIX.1 本地语言支持 (NLS) 进行本地化消息

除了基本的 I18N 功能(如支持各种输入编码或支持国家/地区的约定,例如不同的十进制分隔符)之外,在更高的 I18N 级别,还可以本地化各种程序写入输出的消息。一种常见的方法是使用 POSIX.1 NLS 函数,这些函数作为 FreeBSD 基本系统的一部分提供。

4.2.1. 将本地化消息组织到目录文件中

POSIX.1 NLS 基于目录文件,这些文件包含所需编码中的本地化消息。消息被组织成集合,每个消息由包含集合中的整数标识。目录文件通常以其包含本地化消息的语言环境命名,后跟 .msg 扩展名。例如,ISO8859-2 编码的匈牙利消息应存储在名为 hu_HU.ISO8859-2 的文件中。

这些目录文件是包含编号消息的普通文本文件。可以通过以 $ 符号开头来编写注释。集合边界也由特殊注释分隔,其中关键字 set 必须直接位于 $ 符号之后。然后 set 关键字后跟集合编号。例如

$set 1

实际的消息条目以消息编号开头,后跟本地化消息。printf(3) 中众所周知的修饰符被接受

15 "File not found: %s\n"

语言目录文件必须编译成二进制形式才能从程序中打开。此转换使用 gencat(1) 实用程序完成。它的第一个参数是已编译目录的文件名,其后续参数是输入目录。本地化消息也可以组织到更多目录文件中,然后所有这些文件都可以使用 gencat(1) 进行处理。

4.2.2. 从源代码中使用目录文件

使用目录文件很简单。要使用相关函数,必须包含 nl_types.h。在使用目录之前,必须使用 catopen(3) 打开它。该函数接受两个参数。第一个参数是已安装和已编译目录的名称。通常,使用程序的名称,例如 grep。此名称将在查找已编译目录文件时使用。catopen(3) 调用在 /usr/share/nls/locale/catname/usr/local/share/nls/locale/catname 中查找此文件,其中 locale 是设置的语言环境,catname 是正在讨论的目录名称。第二个参数是一个常量,可以有两个值

  • NL_CAT_LOCALE,这意味着使用的目录文件将基于 LC_MESSAGES

  • 0,这意味着必须使用 LANG 来打开正确的目录。

catopen(3) 调用返回类型为 nl_catd 的目录标识符。请参阅手册页以获取可能的返回错误代码列表。

打开目录后,可以使用 catgets(3) 检索消息。第一个参数是 catopen(3) 返回的目录标识符,第二个参数是集合编号,第三个参数是消息编号,第四个参数是备用消息,如果无法从目录文件中检索请求的消息,则将返回此消息。

使用目录文件后,必须通过调用 catclose(3) 关闭它,该函数带有一个参数,即目录 ID。

4.2.3. 一个实际的例子

以下示例将演示一种关于如何以灵活的方式使用 NLS 目录的简单解决方案。

以下几行需要放入程序的公共头文件中,该文件包含在所有需要本地化消息的源文件中

#ifdef WITHOUT_NLS
#define getstr(n)	 nlsstr[n]
#else
#include nl_types.h

extern nl_catd		 catalog;
#define getstr(n)	 catgets(catalog, 1, n, nlsstr[n])
#endif

extern char		*nlsstr[];

接下来,将这些行放入主源文件的全局声明部分

#ifndef WITHOUT_NLS
#include nl_types.h
nl_catd	 catalog;
#endif

/*
 * Default messages to use when NLS is disabled or no catalog
 * is found.
 */
char    *nlsstr[] = {
        "",
/* 1*/  "some random message",
/* 2*/  "some other message"
};

接下来是打开、读取和关闭目录的实际代码片段

#ifndef WITHOUT_NLS
	catalog = catopen("myapp", NL_CAT_LOCALE);
#endif

...

printf(getstr(1));

...

#ifndef WITHOUT_NLS
	catclose(catalog);
#endif

4.2.3.1. 减少需要本地化的字符串

有一种很好的方法可以减少需要本地化的字符串,即使用 libc 错误消息。这对于避免重复并为许多程序可能遇到的常见错误提供一致的错误消息也很有用。

首先,这里有一个不使用 libc 错误消息的示例。

#include err.h
...
if (!S_ISDIR(st.st_mode))
	errx(1, "argument is not a directory");

可以通过读取errno并相应地打印错误消息来将其转换为打印错误消息。

#include err.h
#include errno.h
...
if (!S_ISDIR(st.st_mode)) {
	errno = ENOTDIR;
	err(1, NULL);
}

在这个例子中,自定义字符串被消除了,因此翻译人员在本地化程序时工作量会减少,并且用户在遇到此错误时会看到通常的“不是目录”错误消息。此消息可能对他们来说更熟悉。请注意,为了直接访问errno,必须包含errno.h

值得注意的是,在某些情况下,errno会由前面的调用自动设置,因此无需显式设置它。

#include err.h
...
if ((p = malloc(size)) == NULL)
	err(1, NULL);

4.2.4. 使用bsd.nls.mk

使用目录文件需要一些可重复的步骤,例如编译目录文件并将它们安装到正确的位置。为了进一步简化此过程,bsd.nls.mk引入了一些宏。无需显式包含bsd.nls.mk,它从通用的 Makefile 中提取,例如bsd.prog.mkbsd.lib.mk

通常,只需定义NLSNAME就足够了,它应该包含作为catopen(3)的第一个参数提到的目录名称,并在NLS中列出目录文件(不带.msg扩展名)。这是一个示例,它使得在与之前的代码示例一起使用时可以禁用 NLS。必须定义WITHOUT_NLS make(1)变量才能在没有 NLS 支持的情况下构建程序。

.if !defined(WITHOUT_NLS)
NLS=	es_ES.ISO8859-1
NLS+=	hu_HU.ISO8859-2
NLS+=	pt_BR.ISO8859-1
.else
CFLAGS+=	-DWITHOUT_NLS
.endif

按照惯例,目录文件放置在nls子目录下,这是bsd.nls.mk的默认行为。不过,可以使用NLSSRCDIR make(1)变量覆盖目录文件的位置。预编译目录文件的默认名称也遵循之前提到的命名约定。可以通过设置NLSNAME变量来覆盖它。还有其他选项可以微调目录文件的处理,但通常不需要,因此这里没有描述。有关bsd.nls.mk的更多信息,请参阅文件本身,它简短易懂。


上次修改时间:2024年3月9日,作者 Danilo G. Baio