文章目录
- 1. 前言
- 2. 消息翻译
-
- 2.1. X/Open 处理消息目录
-
- 2.1.1. catgets 函数族
- 2.1.2. 新闻目录文件的格式
- 2.1.3. 生成消息目录文件
- 2.1.4. 如何使用catgets接口
-
- 2.1.4.1. 不使用符号名
- 2.1.4.2. 使用符号名
- 2.1.4.3. 如何允许开发?
- 2.2. Uniforum 新闻翻译方法
-
- 2.2.1. gettext 系列函数
-
- 2.2.1.1. 翻译新闻需要做什么?
- 2.2.1.2. 如何确定使用哪个目录?
- 2.2.1.3. 更复杂的附加功能
- 2.2.1.4. 如何指定 gettext 使用的输出字符集
- 2.2.1.5. 如何在 GUI 程序中使用 gettext
- 2.2.1.6. 用户对 gettext 的影响
- 2.2.2. 为 gettext 处理消息目录的程序
- 3. 参考
1. 前言
The GNU C Library Reference Manual for version 2.35
2. 消息翻译
Message Translation
程序和用户界面应设计为简化用户任务。简化用户任务的一种方法是使用户喜欢的任何语言信息。
它可以以不同的方式用不同的语言打印信息。您可以在源代码中添加所有不同的语言,并在每次必须打印信息时选择变体。当然,这不是一个好的解决方案,因为扩展语言集很麻烦(代码必须更改),代码本身可能会变得非常大,有几十个信息集。
更好的解决方案是在单独的文件中保存每种语言的信息集,并根据用户的语言选择加载。
GNU C 库提供了两组不同的函数来支持新闻翻译。问题是这两个接口都不是由这两个接口组成的 POSIX 正式定义标准。catgets 系列函数在 X/Open 标准是定义的,但它来自行业决策,所以不一定是基于合理的决策。
如上所述,消息目录处理通过使用包含信息翻译的外部数据文件提供了轻松的可扩展性。也就是说,这些文件包括翻译程序中使用的每个信息的相应语言。因此,信息处理函数的任务是:
- 找到具有适当翻译的外部数据文件
- 加载数据,处理消息
- 将给定的键映射到已翻译的消息中
这两种方法的主要区别在于最后一步的实现。最后一步的决定会影响设计的其他部分。
2.1. X/Open 处理消息目录
X/Open Message Catalog Handling
catgets 基于简单方案的函数:
将源代码中要翻译的每一条信息与唯一的标识符相关联。从目录文件中检索信息,只使用标识符。
这意味着程序作者必须确保程序代码和消息目录中标识符的含义始终相同。
在翻译信息之前,必须先找到目录文件。程序用户必须能够引导负责任的功能找到用户想要的任何目录。这与程序员的想法是分开的。
catgets 函数的所有类型、常量和函数都在 nl_types.h 头文件中定义/声明。
2.1.1. catgets 函数族
The catgets function family
函数:nl_catd catopen (const char *cat_name, int flag)
Preliminary: | MT-Safe env | AS-Unsafe heap | AC-Unsafe mem | See POSIX Safety Concepts.
catopen 试着将函数定位为 cat_name 发现时加载消息数据文件。返回值为不透明类型,可用于调用其他函数引用此加载目录。
如果函数失败且没有加载目录,则返回值为 (nl_catd) -1。全局变量 errno 包含导致失败的错误代码。但即使函数调用成功,也不代表所有新闻都可以翻译。
定位目录文件必须以允许程序用户影响决策的方式进行。有时使用备用目录文件对用户决定使用的语言非常有用。所有这些都可以由用户设置一些环境变量来指定。
第一个问题是找出所有消息目录的存储位置。每个程序都可以有自己的位置来保存所有不同的文件,但目录文件通常按语言分组,所有程序的目录都保存在同一位置。
为了告诉 catopen 在哪里可以找到程序目录,用户可以将环境变量 NLSPATH 设置为描述她/他选择的值。因为这个值必须用于不同的语言和区域设置,所以不能是简单的字符串。相反,它是一个格式字符串(类似于 printf )。一个例子是
/usr/share/locale/%L/%N:/usr/share/locale/%L/LC_MESSAGES/%N
首先可以看到多个目录(常用语法用冒号分隔)。接下来要观察的是格式字符串,在这种情况下是 %L 和 %N。catopen 函数知道几个,所有这些替换当然是不同的。
%N
该格式元素将被目录文件的名称所取代。这是给出的 catgets 的 cat_name 参数的值。
%L
该格式元素将被当前选定的语言环境的名称所取代。如何确定以下解释。
%l
(这是小写的 ell。)该格式元素被语言环境名称所取代。描述所选语言环境的字符串应具有 lang[_terr[.codeset]]
格式使用第一部分 lang。
%t
该格式元素由当前选定语言环境名称的区域组成 terr 更换。请参阅上述格式说明。
%C
该格式元素由当前选定语言环境名称的代码集的部分代码集替换。请参阅上述格式说明。
%%
由于 % 作为元字符,必须有一种方法来表达结果本身 % 字符。使用 %% 就像它适合一样 printf 一样。
使用 NLSPATH 允许在任何目录中搜索消息目录,同时仍允许使用不同的语言。如果没有设置, NLSPATH 环境变量,默认值
prefix/share/locale/%L/%N:prefix/share/locale/%L/LC_MESSAGES/%N
安装前缀 GNU C 库时配置(这个值在很多情况下是 /usr 或空字符串)。
剩下的问题是决定必须使用哪一个。该值决定了上述格式元素的替换。首先,用户可以在消息目录名称中指定路径(即名称包含斜杠字符)。在这种情况下,不使用 NLSPATH 环境变量。目录必须按照程序规定存在,可能与当前工作目录有关。这种情况是不可取的,目录名称永远不应该这样写。此外,这种行为不能移植到所有其他提供 catgets 接口平台。
否则,将检查标准环境中的环境变量值(请参考标准环境变量)。检查哪些变量是由哪些变量引起的 catopen 决定标志参数。如果值为 NL_CAT_LOCALE(在 nl_types.h 中定义),然后 catopen 目前使用函数为 LC_MESSAGES 语言环境的类别名称。
如果标志为零,请检查 LANG 环境变量。这是早期的遗留物,语言环境的概念甚至还没有达到 POSIX 语言环境水平。
如上所述,环境变量和语言环境名称应具有 lang[_terr[.codeset]] 形式值。如果没有环境变量,则使用C语言环境阻止任何翻译。
函数的返回值在任何情况下都是有效的字符串。它要么是来自新闻目录的翻译,要么与字符串参数相同。因此,确定翻译是否实际发生的代码必须如下:
{
char *trans = catgets (desc, set, msg, input_string);
if (trans == input_string)
{
/* Something went wrong. */
}
}
当发生错误时,全局变量 errno 设置为
EBADF
目录不存在。
ENOMSG
集合/消息元组没有命名消息目录中的现有元素。
虽然有时测试错误很有用,但程序通常会避免任何测试。如果翻译不可用,打印原始的、未翻译的消息也没有什么大问题。要么用户也理解这一点,要么他/她将寻找消息未翻译的原因。
请注意,当前选择的语言环境不依赖于对 setlocale 函数的调用。此语言环境的语言环境数据文件不必存在并且调用 setlocale 成功。catopen 函数直接读取环境变量的值。
函数:char * catgets (nl_catd catalog_desc, int set, int message, const char *string)
Preliminary: | MT-Safe | AS-Safe | AC-Safe | See POSIX Safety Concepts.
必须使用函数 catgets 来访问先前使用 catopen 函数打开的消息目录。catalog_desc 参数必须是 catopen 先前返回的值。
接下来的两个参数 set 和 message 反映了消息目录文件的内部组织。这将在下面详细解释。现在有趣的是,一个目录可以由多个集合组成,并且每个线程中的消息都使用数字单独编号。集合号和消息号都不能是连续的。它们可以任意选择。但是每条消息(除非与另一条消息相等)都必须有自己唯一的一对集合和消息号。
由于不能保证用户选择的语言的消息目录存在,最后一个参数字符串有助于优雅地处理这种情况。如果找不到匹配的字符串,则返回字符串。这对程序员来说意味着
- 字符串参数应包含合理的文本(这也有助于理解程序,否则将不会对预期返回的字符串有任何提示。
- 所有字符串参数都应该用相同的语言编写。
如果没有可用的支持功能,使用 catgets 函数编写程序会有些不舒服。由于每个集合/消息编号元组必须是唯一的,因此程序员必须在编写代码的同时保留消息列表。并且在同一个项目上工作的几个人之间的工作必须协调。我们将看到这些问题中的一些如何可以轻松一点(请参阅如何使用 catgets 接口)。
函数:int catclose (nl_catd catalog_desc)
Preliminary: | MT-Safe | AS-Unsafe heap | AC-Unsafe corrupt mem | See POSIX Safety Concepts.
catclose 函数可用于释放与先前通过调用 catopen 打开的消息目录关联的资源。如果可以成功释放资源,则该函数返回 0。否则返回 -1 并设置全局变量 errno。如果目录描述符 catalog_desc 在这种情况下 errno 设置为 EBADF 无效,则可能会发生错误。
2.1.2. 消息目录文件的格式
Format of the message catalog files
翻译函数的所有消息并将结果存储在可由 catopen 函数读取的消息目录文件中的唯一合理方法是将所有消息文本写入翻译器并让她/他将它们全部翻译。即,我们必须有一个文件,其中包含将集合/消息元组与特定翻译相关联的条目。此文件格式在 X/Open 标准中指定,如下所示:
-
仅包含空白字符或空行的行将被忽略。
-
包含作为第一个非空白字符 $ 后跟空白字符的行是注释,也将被忽略。
-
如果一行包含作为第一个非空白字符的序列 $set 后跟一个空白字符,则需要附加一个参数。这个论点可以是:
- 一个数字。在这种情况下,此数字的值确定要添加以下消息的集合。
- 由字母数字字符和下划线字符组成的标识符。在这种情况下,集合会自动分配一个编号。该值是迄今为止出现的最大集合数的加一。
如何使用符号名称在如何使用 catgets 接口一节中进行了说明。
如果符号名称出现多次,则为错误。以下所有消息都放在具有此编号的集合中。
-
如果一行包含作为第一个非空白字符的序列 $delset 后跟一个空白字符,则需要一个附加参数。这个论点可以是:
- 一个数字。在这种情况下,这个数字的值决定了将被删除的集合。
- 由字母数字字符和下划线字符组成的标识符。此符号标识符必须与先前定义的集合的名称匹配。如果名称未知,则为错误。
在这两种情况下,指定集中的所有消息都将被删除。它们不会出现在输出中。但是,如果稍后再次使用 $set 命令再次选择该集合,则可以添加消息并且这些消息将出现在输出中。
-
如果一行在前导空格之后包含序列 $quote,则用于此输入文件的引用字符将更改为 $quote 之后的第一个非空格字符。如果在行结束之前不存在非空白字符,则禁用引号。
默认情况下不使用引号字符。在这种模式下,字符串以第一个未转义的换行符终止。如果存在 $quote 序列,则不需要转义换行符。相反,字符串以引号字符的第一个非转义外观终止。
此功能的常见用法是将引号字符设置为“。然后字符串中出现的任何“”都必须使用反斜杠进行转义(即,必须写入“\”)。
-
任何其他行必须以数字或字母数字标识符(包括下划线字符)开头。下面的字符(从第一个空格字符开始)将形成与当前选择的集合相关联的字符串,以及分别由数字和标识符表示的消息号。
如果该行的开头是一个数字,则消息编号是显而易见的。如果该集合已出现相同的消息编号,则为错误。
如果前导标记是标识符,则自动分配消息编号。该值是该集合的当前最大消息数加一。如果标识符已用于此集中的消息,则为错误。可以在另一个线程中重用消息的标识符。下面将解释如何使用符号标识符(参见如何使用 catgets 接口)。标识符有一个限制:它不能被设置。原因将在下面解释。
消息的文本可以包含转义字符。识别 ISO C 语言中已知的通常一堆字符(\n、\t、\v、\b、\r、\f、\ 和 \nnn,其中 nnn 是字符代码的八进制编码) .
重要提示:对集合和消息的标识符而不是数字的处理是 GNU 扩展。严格遵循 X/Open 规范的系统没有此功能。消息目录文件的示例如下:
$ This is a leading comment.
$quote "
$set SetOne
1 Message with ID 1.
two " Message with ID \"two\", which gets the value 2 assigned"
$set SetTwo
$ Since the last set got the number 1 assigned this set has number 2.
4000 "The numbers can be arbitrary, they need not start at one."
这个小例子展示了各个方面:
- 第 1 行和第 9 行是注释,因为它们以 $ 开头,后跟一个空格。
- 引号字符设置为 "。否则消息定义中的引号必须被省略,在这种情况下,标识符为 2 的消息将丢失其前导空格。
- 将编号消息与具有符号名称的消息混合是没有问题的,并且编号会自动发生。
虽然这种文件格式非常简单,但它并不是在运行程序中使用的最佳选择。catopen 函数必须解析文件并优雅地处理语法错误。这并不容易,整个过程非常缓慢。因此,catgets 函数期望数据采用另一种更紧凑且易于使用的文件格式。有一个特殊的程序 gencat 将在下一节中详细解释。
这种其他格式的文件不是人类可读的。为了便于程序使用,它是一个二进制文件。但是格式是独立于字节顺序的,因此翻译文件可以由任意体系结构的系统共享(只要它们使用 GNU C 库)。
关于二进制文件格式的详细信息并不重要,因为这些文件总是由 gencat 程序创建的。GNU C 库的源代码也提供了 gencat 程序的源代码,因此有兴趣的读者可以查看这些源文件以了解文件格式。
2.1.3. 生成消息目录文件
Generate Message Catalogs files
gencat 程序在 X/Open 标准中指定,GNU 实现遵循此规范,因此处理所有格式正确的输入文件。此外,还实现了一些扩展,有助于以更合理的方式使用 catgets 函数。
可以通过两种方式调用 gencat 程序:
`gencat [Option …] [Output-File [Input-File …]]`
这是 X/Open 标准中定义的接口。如果没有给出 Input-File 参数,输入将从标准输入中读取。将读取多个输入文件,就像它们被连接一样。如果还缺少 Output-File,则输出将被写入标准输出。为了提供一个用于其他程序的接口,提供了第二个接口。
`gencat [Option …] -o Output-File [Input-File …]`
选项“-o”用于指定输出文件,所有文件参数都用作输入文件。
除此之外,可以使用 - 或 /dev/stdin 作为 Input-File 来表示标准输入。对应的 Output-File 可以使用 - 和 /dev/stdout 来表示标准输出。在 X/Open 中允许使用 - 作为文件名,而使用设备名称是 GNU 扩展。
gencat 程序通过连接所有输入文件,然后将生成的消息集集合与可能存在的输出文件合并来工作。这是通过删除所有具有与输出文件中生成的任何消息匹配的集合/消息编号元组的消息,然后添加所有新消息来完成的。因此,要在忽略旧内容的同时重新生成目录文件,需要删除输出文件(如果存在)。如果将输出写入标准输出,则不会发生合并。
下表显示了 gencat 程序可以理解的选项。X/Open 标准没有为程序指定任何选项,因此所有这些都是 GNU 扩展。
'-V'
'--version'
打印版本信息并退出。
'-H'
'--help'
打印列出所有可用选项的使用消息,然后成功退出。
'--new'
不要将输入文件中的新消息与输出文件的旧内容合并。输出文件的旧内容被丢弃。
'-H'
'--header=name'
此选项用于发出在程序中使用的输入文件中的集合和消息的符号名称。下一节将详细介绍如何使用它。此选项的 name 参数指定输出文件的名称。它将包含许多 C 预处理器 #defines 以将名称与数字相关联。
请注意,生成的文件仅包含输入文件中的符号。如果输出与输出文件的先前内容合并,则生成旧输出文件的文件中可能存在的符号不在生成的头文件中。
2.1.4. 如何使用catgets接口
How to use the catgets interface
catgets 函数可以以两种不同的方式使用。通过严格遵循 X/Open 规范而不依赖扩展和使用 GNU 扩展。我们将首先看一下前一种方法,以了解扩展的好处。
2.1.4.1. 不使用符号名
Not using symbolic names
由于消息目录文件的 X/Open 格式不允许符号名称,我们必须一直使用数字。当我们开始编写程序时,我们必须用类似的东西替换所有出现的可翻译字符串
catgets (catdesc, set, msg, "string")
catgets 是从对 catopen 的调用中检索的,该调用通常在程序启动时完成一次。“字符串”是我们要翻译的字符串。问题从集合号和消息号开始。
在一个更大的程序中,几个程序员通常同时在程序上工作,因此协调数字分配是至关重要的。尽管没有两个不同的字符串必须由相同的数字元组索引,但非常希望将数字重用于具有相同翻译的相同字符串(请注意,在一种语言中可能存在相同但由于上下文不同而具有不同翻译的字符串)。
对于程序的不同部分,分配过程可以通过不同的集数来放宽一点。因此可以减少必须协调分配的开发人员的数量。但是列表仍然必须跟踪分配,并且很容易发生错误。编译器或 catgets 函数无法发现这些错误。只有程序的用户可能会看到打印的错误消息。在最坏的情况下,这些信息非常令人恼火,以至于无法识别它们是错误的。考虑交换“真”和“假”的翻译。这可能导致灾难。
2.1.4.2. 使用符号名
Using symbolic names
上一节提到的问题源于以下事实:
- 这些号码被分配一次,由于可能经常使用它们,以后很难更改号码。
- 这些数字不允许对字符串进行任何猜测,因此很容易发生冲突。
通过不断使用符号名称并提供一种将字符串内容映射到符号名称的方法(但是这会发生),可以防止上述两个问题。这样做的代价是程序员在编写程序本身时必须编写完整的消息目录文件。
这是必要的,因为在编译程序源之前必须将符号名称映射到数字。在上一节中,描述了如何生成包含名称映射的标头。例如,对于上一节中给出的示例消息文件,我们可以如下调用 gencat 程序(假设 ex.msg 包含源)。
gencat -H ex.h -o ex.cat ex.msg
这会生成一个包含以下内容的头文件:
#define SetTwoSet 0x2 /* ex.msg:8 */
#define SetOneSet 0x1 /* ex.msg:4 */
#define SetOnetwo 0x2 /* ex.msg:6 */
可以看出,源文件中给出的各种符号被修改以生成唯一标识符,并且这些标识符得到分配的数字。阅读源文件并了解规则将允许预测头文件的内容(它是确定性的),但这不是必需的。gencat 程序可以处理所有事情。程序员所要做的就是将生成的头文件放在她/他的项目的源文件的依赖列表中,并添加一个规则以在任何输入文件发生更改时重新生成头文件。
关于符号修饰的一句话。每个符号由两部分组成:消息集的名称加上消息的名称或特殊字符串 Set。所以 SetOnetwo 意味着这个宏可以用来访问消息集 SetOne 中标识符为 2 的翻译。
其他名称表示消息集的名称。特殊字符串 Set 用于代替消息标识符。
如果在代码中使用 SetOne 的第二个字符串,则 C 代码应如下所示:
catgets (catdesc, SetOneSet, SetOnetwo,
" Message with ID \"two\", which gets the value 2 assigned")
以这种方式编写函数将允许更改消息编号甚至设置编号,而无需对 C 源代码进行任何更改。(字符串的文本通常不相同;这仅适用于本示例。)
2.1.4.3. 如何允许开发
How does to this allow to develop
为了说明使用符号版本号的常用方法,这里举了一个小例子。假设我们要编写非常复杂且著名的问候程序。我们像往常一样开始编写代码:
#include <stdio.h>
int
main (void)
{
printf ("Hello, world!\n");
return 0;
}
现在我们想要国际化消息,因此用用户想要的任何内容替换消息。
#include <nl_types.h>
#include <stdio.h>
#include "msgnrs.h"
int
main (void)
{
nl_catd catdesc = catopen ("hello.cat", NL_CAT_LOCALE);
printf (catgets (catdesc, SetMainSet, SetMainHello,
"Hello, world!\n"));
catclose (catdesc);
return 0;
}
我们看到目录对象是如何打开的,以及在其他函数调用中使用的返回描述符。实际上没有必要检查任何功能的故障,因为即使在这些情况下,功能也会表现得合理。他们只会返回翻译。
此处未指定的是常量 SetMainSet 和 SetMainHello。这些是描述消息的符号名称。要获得与目录文件中的信息匹配的实际定义,我们必须创建消息目录源文件并使用 gencat 程序对其进行处理。
$ Messages for the famous greeting program.
$quote " $set Main Hello "Hallo, Welt!\n"
现在我们可以开始构建程序(假设消息目录源文件名为 hello.msg,程序源文件名为 hello.c):
% gencat -H msgnrs.h -o hello.cat hello.msg
% cat msgnrs.h
#define MainSet 0x1 /* hello.msg:4 */
#define MainHello 0x1 /* hello.msg:5 */
% gcc -o hello hello.c -I.
% cp hello.cat /usr/share/locale/de/LC_MESSAGES
% echo $LC_ALL
de
% ./hello
Hallo, Welt!
%
gencat 程序的调用会创建缺少的头文件 msgnrs.h 以及消息目录二进制文件。前者用于 hello.c 的编译,而后者则放置在 catopen 函数将尝试定位的目录中。请检查上面描述中的 LC_ALL 环境变量和 catopen 的默认路径。
2.2. Uniforum 消息翻译方法
The Uniforum approach to Message Translation
Sun Microsystems 试图在 Uniforum 小组中标准化一种不同的消息翻译方法。从来没有定义过真正的标准,但 Sun 的操作系统仍然使用该接口。由于这种方法更适合自由软件的开发过程,因此它也在整个 GNU 项目中使用,并且 GNU gettext 包在 GNU C 库之外提供了对此的支持。
来自 GNU gettext 的 libintl 的代码与 GNU C 库中的代码相同。因此 GNU gettext 手册中的文档也适用于此处的功能。下面的文字将详细描述库函数。但是本手册中没有描述众多的帮助程序。相反,人们应该阅读 GNU gettext 手册(请参阅本机语言支持库和工具中的 GNU gettext 实用程序)。我们只会做一个简短的概述。
虽然 catgets 函数在更多系统上默认可用,但 gettext 接口至少与前者一样可移植。GNU gettext 包可以在功能不可用的地方使用。
2.2.1. gettext 系列函数
The gettext family of functions
用于消息翻译的 gettext 方法的范式与 catgets 函数的范式不同,基本功能上是等价的。有以下几类的功能:
2.2.1.1. 翻译消息需要做什么?
What has to be done to translate a message?
gettext 函数有一个非常简单的界面。最基本的函数只是将要翻译的字符串作为参数并返回翻译。这与 catgets 方法根本不同,后者需要额外的键并且原始字符串仅用于错误情况。
如果必须翻译的字符串是唯一的参数,这当然意味着字符串本身就是关键。即,将根据原始字符串选择翻译。因此,消息目录必须包含原始字符串以及任何此类字符串的翻译。gettext 函数的任务是将参数字符串与目录中的可用字符串进行比较,并返回适当的翻译。当然,这个过程已经过优化,因此这个过程不会比使用 catgets 中的原子键访问更昂贵。
gettext 方法有一些优点,但也有一些缺点。有关利弊的详细讨论,请参阅 GNU gettext 手册。
gettext 的所有定义和声明都可以在 libintl.h 头文件中找到。在这些函数不是 C 库的一部分的系统上,它们可以在名为 libintl.a 的单独库中找到(或因此对于共享库不同)。
函数:char * gettext (const char *msgid)
Preliminary: | MT-Safe env | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
gettext 函数在当前选定的消息目录中搜索等于 msgid 的字符串。如果有这样的字符串可用,则返回。否则返回参数字符串 msgid。
请注意,虽然返回值为 char *,但不能更改返回的字符串。这种损坏的类型源于函数的历史,并不反映函数的使用方式。
请注意,上面我们写了“消息目录”(复数)。这是这些功能的 GNU 实现的一个特点,当我们讨论消息目录的选择方式时,我们将对此进行更多说明(请参阅如何确定要使用的目录)。
gettext 函数不会修改全局 errno 变量的值。这对于编写类似的东西是必要的
printf (gettext ("Operation failed: %m\n"));
这里在处理 %m 格式元素时在 printf 函数中使用了 errno 值,如果 gettext 函数会更改此值(在调用 printf 之前调用它),我们将收到错误消息。
因此,除了将参数字符串与结果进行比较之外,没有简单的方法来检测丢失的消息目录。但通常用户的任务是对丢失的目录做出反应。程序无法猜测何时真正需要消息目录,因为对于说程序开发语言的用户来说,消息不需要任何翻译。
其余两个访问消息目录的功能添加了一些功能来选择不是默认的消息目录。如果程序的某些部分是独立开发的,这一点很重要。每个部分都可以有自己的消息目录,并且可以同时使用它们。C 库本身就是一个例子:它在内部使用 gettext 函数,但由于它不能依赖于当前选择的默认消息目录,它必须指定所有不明确的信息。
函数:char * dgettext (const char *domainname, const char *msgid)
Preliminary: | MT-Safe env | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
dgettext 函数的作用与 gettext 函数一样。它只需要一个额外的第一个参数 domainname 来指导选择搜索翻译的消息目录。如果 domainname 参数是空指针,则 dgettext 函数与 gettext 完全相同,因为使用的是域名的默认值。
至于 gettext,返回值类型是 char *,这是不合时宜的。不得修改返回的字符串。
函数:char * dcgettext (const char *domainname, const char *msgid, int category)
Preliminary: | MT-Safe env | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
dcgettext 为 dgettext 采用的参数添加了另一个参数。此参数类别指定本地化消息目录所需的最后一条信息。即,域名和语言环境类别准确地指定了必须使用的消息目录(相对于给定目录,见下文)。
dgettext 函数可以使用 dcgettext 表示
dcgettext (domain, string, LC_MESSAGES)
代替
dgettext (domain, string)
这也显示了第三个参数的预期值。必须对 locale.h 中可用的类别使用可用的选择器。通常可用的值为 LC_CTYPE、LC_COLLATE、LC_MESSAGES、LC_MONETARY、LC_NUMERIC 和 LC_TIME。请注意,不得使用 LC_ALL,即使名称可能暗示这一点,也与该名称的环境变量无关。
dcgettext 函数的实现只是为了与其他具有 gettext 函数的系统兼容。实际上没有任何情况需要(或有用)为类别参数使用与 LC_MESSAGES 不同的值。我们在这里处理消息,任何其他选择只会令人恼火。
至于 gettext,返回值类型是 char *,这是不合时宜的。不得修改返回的字符串。
在程序中使用上述三个函数时,经常会出现 msgid 参数是常量字符串的情况。因此,对这种情况进行优化是值得的。稍微考虑一下这一点就会意识到,只要没有加载新的消息目录,消息的翻译就不会改变。这种优化实际上是由 gettext、dgettext 和 dcgettext 函数实现的。
2.2.1.2. 如何确定使用哪个目录
How to determine which catalog to be used
检索给定消息翻译的功能具有非常简单的界面。但是,为了让程序的用户仍然有机会准确地选择他/她想要的翻译,并为程序员提供影响查找目录文件的方式的可能性,有一个相当复杂的底层机制来控制这一切.代码复杂,使用简单。
基本上我们有两个不同的任务要执行,也可以由 catgets 函数执行:
-
找到一组消息目录。有许多不同语言的文件都属于该包。通常它们都存储在某个目录下的文件系统中。
可以安装任意多个软件包,并且它们可以遵循不同的文件放置准则。
-
相对于包指定的位置,必须根据用户的意愿搜索实际的翻译文件。即,对于用户选择的每种语言,程序应该能够找到适当的文件。
这是 gettext 规范要求的功能,这也是 catgets 函数能够做的事情。但是还有一些问题没有解决:
- 可以通过几种不同的方式指定要使用的语言。对此没有普遍接受的标准,用户总是希望程序能够理解他/她的意思。例如,要选择德语翻译,可以写 de、German 或 deutsch,程序应该始终做出相同的反应。
- 有时用户的说明过于详细。例如,如果她/他指定 de_DE.ISO-8859-1 表示德语,在德国说,使用 ISO 8859-1 字符集编码,则可能无法获得与此完全匹配的消息目录。但是可能有一个与 de 匹配的目录,如果机器上使用的字符集始终是 ISO 8859-1,那么没有理由不使用这个后来的消息目录。(我们称之为消息继承。)
- 如果所需语言的目录不可用,则依赖开发人员的语言并且根本不翻译任何消息并不总是第二好的选择。相反,用户可能能够更好地阅读另一种语言的消息,因此程序的用户应该能够定义语言的优先顺序。
我们可以将配置动作分为两部分:一个由程序员执行,另一个由用户执行。我们将从程序员可以使用的功能开始,因为用户配置将基于此。
正如上一节中描述的功能已经提到,可以通过域名选择单独的消息集。这是一个简单的字符串,对于使用单独域的每个程序部分来说应该是唯一的。可以在一个程序中同时使用任意多个域。例如,GNU C 库本身使用名为 libc 的域,而使用 C 库的程序可以使用名为 foo 的域。重要的一点是,任何时候都只有一个域处于活动状态。这由以下功能控制。
函数:char * textdomain (const char *domainname)
Preliminary: | MT-Safe | AS-Unsafe lock heap | AC-Unsafe lock mem | See POSIX Safety Concepts.
textdomain 函数将在所有未来的 gettext 调用中使用的默认域设置为 domainname。请注意,如果这些函数的 domainname 参数不是空指针,则不会影响 dgettext 和 dcgettext 调用。
在第一次调用 textdomain 之前,默认域是消息。这是在 gettext API 规范中指定的名称。这个名字和其他名字一样好。任何程序都不应该真正使用具有此名称的域,因为这只会导致问题。
该函数返回从现在开始作为默认域的值。如果系统内存不足,则返回值为 NULL,并且全局变量 errno 设置为 ENOMEM。尽管返回值类型为 char *,但不得更改返回字符串。它由 textdomain 函数在内部分配。
如果 domainname 参数是空指针,则不会设置新的默认域。而是返回当前选择的默认域。
如果 domainname 参数是空字符串,则默认域将重置为其初始值,即带有消息的域。这种可能性的使用是有问题的,因为域消息真的不应该被使用。
函数:char * bindtextdomain (const char *domainname, const char *dirname)
Preliminary: | MT-Safe | AS-Unsafe heap | AC-Unsafe mem | See POSIX Safety Concepts.
bindtextdomain 函数可用于指定包含不同语言的域 domainname 的消息目录的目录。正确地说,这是预期目录层次结构的目录。详细说明如下。
对于程序员来说,重要的是要注意程序附带的翻译必须放在从 /foo/bar 开始的目录层次结构中。然后程序应该调用 bindtextdomain 来将当前程序的域绑定到这个目录。因此,确保找到目录。正确运行的程序不依赖于用户设置环境变量。
bindtextdomain 函数可以多次使用,如果 domainname 参数不同,则不会覆盖先前绑定的域。
如果希望在某个时间点使用 bindtextdomain 的程序使用 chdir 函数来更改当前工作目录,重要的是 dirname 字符串应该是绝对路径名。否则,寻址目录可能会随时间而变化。
如果 dirname 参数为空指针,则 bindtextdomain 返回名称为 domainname 的域的当前选定目录。
bindtextdomain 函数返回一个指向包含所选目录名称的字符串的指针。该字符串在函数内部分配,用户不得更改。如果系统在执行 bindtextdomain 期间脱离内核,则返回值为 NULL,并相应地设置全局变量 errno。
2.2.1.3. 更复杂情况的附加功能
Additional functions for more complicated situations
到目前为止描述的 gettext 系列的功能(以及所有的 catgets 功能)在现实世界中存在一个问题,在所有现有方法中都被完全忽略了。这里的意思是处理复数形式。
在任何人考虑国际化之前(遗憾的是,甚至在之后)查看 Unix 源代码,通常可以找到类似于以下的代码:
printf ("%d file%s deleted", n, n == 1 ? "" : "s");
在人们对代码国际化的第一次抱怨之后,人们要么完全避免这样的表述,要么使用像“file(s)”这样的字符串。两者看起来都不自然,应该避免。首先尝试正确解决问题如下所示:
if (n == 1)
printf ("%d file deleted", n);
else
printf ("%d files deleted", n);
但这并不能解决问题。它有助于名词的复数形式不是简单地通过添加“s”来构建的语言,仅此而已。人们又一次陷入了相信他们的语言使用的规则是通用的陷阱。但是不同语系对复数形式的处理差异很大。我们可以在两件事之间(甚至在语言家族内部)有所不同;
- 复数形式的构建方式不同。这是具有许多不规则性的语言的问题。例如,德语就是一个极端的例子。尽管英语和德语属于同一个语系(日耳曼语),但在德语中几乎找不到复数名词形式(附加一个“s”)的规则形式。
- 复数形式的数量不同。对于那些只有罗马语和日耳曼语经验的人来说,这有点令人惊讶,因为这里的数字是相同的(有两个)。 但其他语系只有一种或多种形式。有关这方面的更多信息,请参见额外部分。
这样做的结果是应用程序编写者不应该尝试在他们的代码中解决问题。这将是本地化,因为它仅可用于某些硬编码的语言环境。相反,应该使用扩展的 gettext 接口。
这些额外的函数用两个字符串和一个数字参数代替了一个键字符串。这背后的想法是,使用数字参数和第一个字符串作为键,实现可以使用翻译器指定的规则选择正确的复数形式。如果没有找到消息目录(类似于正常的 gettext 行为),则两个字符串参数将用于提供返回值。在这种情况下,使用日耳曼语言的规则,并假设第一个字符串参数是单数形式,第二个是复数形式。
这导致没有语言目录的程序只有在程序本身使用日耳曼语编写时才能显示正确的字符串。这是一个限制,但由于 GNU C 库(以及 GNU gettext 包)是作为 GNU 包的一部分编写的,并且 GNU 项目的编码标准要求程序必须用英文编写,因此该解决方案仍然可以实现其目的。
函数:char * ngettext (const char *msgid1, const char *msgid2, unsigned long int n)
Preliminary: | MT-Safe env | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
ngettext 函数类似于 gettext 函数,因为它以相同的方式查找消息目录。但这需要两个额外的参数。msgid1 参数必须包含要转换的字符串的单数形式。它也用作在目录中搜索的关键字。msgid2 参数是复数形式。参数n用于确定复数形式。如果没有找到消息目录,如果 n == 1,则返回 msgid1,否则返回 msgid2。
使用此功能的一个示例是:
printf (ngettext ("%d file removed", "%d files removed", n), n);
请注意,数值 n 也必须传递给 printf 函数。仅将其传递给 ngettext 是不够的。
函数:char * dngettext (const char *domain, const char *msgid1, const char *msgid2, unsigned long int n)
Preliminary: | MT-Safe env | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
dngettext 在选择消息目录的方式上与 dgettext 函数类似。不同之处在于它需要两个额外的参数来提供正确的复数形式。这两个参数的处理方式与 ngettext 处理它们的方式相同。
函数:char * dcngettext (const char *domain, const char *msgid1, const char *msgid2, unsigned long int n, int category)
Preliminary: | MT-Safe env | AS-Unsafe corrupt heap lock dlopen | AC-Unsafe corrupt lock fd mem | See POSIX Safety Concepts.
dcngettext 在选择消息目录的方式上与 dcgettext 函数类似。不同之处在于它需要两个额外的参数来提供正确的复数形式。这两个参数的处理方式与 ngettext 处理它们的方式相同。
可以在上一节的开头找到对问题的描述。现在有一个问题是如何解决它。如果没有语言学家的输入(这是不可用的),就不可能确定是否只有几种不同的形式可以形成复数形式,或者数量是否会随着每种新支持的语言而增加。
因此实现的解决方案是允许翻译者指定如何选择复数形式的规则。由于公式因每种语言而异,这是唯一可行的解决方案,除了硬编码代码中的信息(这仍然需要扩展的可能性以不阻止使用新语言)。细节在 GNU gettext 手册中有解释。这里只提供一点信息。
有关复数形式选择的信息必须存储在标题条目中(带有空 msgid 字符串的条目)。它看起来像这样:
Plural-Forms: nplurals=2; plural=n == 1 ? 0 : 1;
nplurals 值必须是一个十进制数,用于指定该语言存在多少种不同的复数形式。复数后面的字符串是使用 C 语言语法的表达式。例外情况是不允许负数,数字必须是十进制,并且唯一允许的变量是 n。每当调用函数 ngettext、dngettext 或 dcngettext 之一时,都会计算此表达式。然后将传递给这些函数的数值替换为表达式中变量 n 的所有使用。然后,结果值必须大于或等于 0 并且小于作为 nplurals 的值给出的值。
在这一点上,以下规则是已知的。列出了带有家庭的语言。但这并不一定意味着信息可以推广到整个家庭(如下表所示)。
只有一种形式: 有些语言只需要一种形式。单数和复数形式之间没有区别。适当的标题条目如下所示:
Plural-Forms: nplurals=1; plural=0;
具有此属性的语言包括:
Finno-Ugric family
Hungarian
Asian family
Japanese, Korean
Turkic/Altaic family
Turkish
两种形式,单数只用于一种 这是大多数现有程序中使用的形式,因为它是英语使用的形式。标题条目如下所示:
Plural-Forms: nplurals=2; plural=n != 1;
(注意:这使用了 C 表达式的特性,即布尔表达式必须值为 0 或 1。)
具有此属性的语言包括:
Germanic family
Danish, Dutch, English, German, Norwegian, Swedish
Finno-Ugric family
Estonian, Finnish
Latin/Greek family
Greek
Semitic family
Hebrew
Romance family
Italian, Portuguese, Spanish
Artificial
Esperanto
两种形式,单数用于零和一 语言家族中的特例。标题条目将是:
Plural-Forms: nplurals=2; plural=n>1;
具有此属性的语言包括:
Romanic family
French, Brazilian Portuguese
三种形式,零的特例 标题条目将是:
Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;
具有此属性的语言包括:
Baltic family
Latvian
三种形式,一种和两种的特殊情况 标题条目将是:
Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;
具有此属性的语言包括:
Celtic
Gaeilge (Irish)
三种形式,以 1[2-9] 结尾的数字的特殊情况 标题条目如下所示:
Plural-Forms: nplurals=3; \
plural=n%10==1 && n%100!=11 ? 0 : \
n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2;
具有此属性的语言包括:
Baltic family
Lithuanian
三种形式,以 1 和 2、3、4 结尾的数字的特殊情况,以 1 结尾的数字除外[1-4] 标题条目如下所示:
Plural-Forms: nplurals=3; \
plural=n%100/10==1 ? 2 : n%10==1 ? 0 : (n+9)%10>3 ? 2 : 1;
具有此属性的语言包括:
Slavic family
Croatian, Czech, Russian, Ukrainian
三种形式,1和2、3、4的特例 标题条目如下所示:
Plural-Forms: nplurals=3; \
plural=(n==1) ? 1 : (n>=2 && n<=4) ? 2 : 0;
具有此属性的语言包括:
Slavic family
Slovak
三种形式,一种特殊情况和一些以 2、3 或 4 结尾的数字 标题条目如下所示:
Plural-Forms: nplurals=3; \
plural=n==1 ? 0 : \
n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;
具有此属性的语言包括:
Slavic family
Polish
四种形式,一个和所有以 02、03 或 04 结尾的数字的特殊情况 标题条目如下所示:
Plural-Forms: nplurals=4; \
plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;
具有此属性的语言包括:
Slavic family
Slovenian
2.2.1.4. 如何指定 gettext 使用的输出字符集
How to specify the output character set gettext uses
gettext 不仅在消息目录中查找翻译,它还即时将翻译转换为所需的输出字符集。如果用户正在使用与创建消息目录的翻译人员不同的字符集,这将很有用,因为它避免了分发仅在字符集上有所不同的消息目录的变体。
默认情况下,输出字符集是 nl_langinfo (CODESET) 的值,它取决于当前语言环境的 LC_CTYPE 部分。但是以独立于语言环境的方式(例如 UTF-8)存储字符串的程序可以通过使用 bind_textdomain_codeset 函数请求 gettext 和相关函数以该编码返回翻译。
请注意,gettext 的 msgid 参数不受字符集转换的影响。此外,当 gettext 没有找到 msgid 的翻译时,它会原封不动地返回 msgid——与当前输出字符集无关。因此,建议所有 msgid 都是 US-ASCII 字符串。
函数:char * bind_textdomain_codeset (const char *domainname, const char *codeset)
Preliminary: | MT-Safe | AS-Unsafe heap | AC-Unsafe mem | See POSIX Safety Concepts.
bind_textdomain_codeset 函数可用于为域 domainname 的消息目录指定输出字符集。代码集参数必须是可用于 iconv_open 函数的有效代码集名称,或者是空指针。
如果 codeset 参数是空指针,则 bind_textdomain_codeset 返回名称为 domainname 的域的当前选定代码集。如果尚未选择任何代码集,则返回 NULL。
bind_textdomain_codeset 函数可以多次使用。如果多次使用同一个域名参数,后面的调用会覆盖前面的设置。
bind_textdomain_codeset 函数返回一个指向包含所选代码集名称的字符串的指针。该字符串在函数内部分配,用户不得更改。如果在bind_textdomain_codeset执行过程中系统出核,则返回值为NULL,并相应设置全局变量errno。
2.2.1.5. 如何在 GUI 程序中使用 gettext
How to use gettext in GUI programs
如果正常使用,gettext 函数会出现大问题的一个地方是具有图形用户界面 (GUI) 的程序。问题是许多必须翻译的字符串都很短。它们必须出现在限制长度的下拉菜单中。但是不包含整个句子或至少一个句子的大片段的字符串可能会出现在程序中的不止一种情况下,但可能有不同的翻译。对于 GUI 程序中经常使用的单字字符串尤其如此。
因此,许多人说 gettext 方法是错误的,而应该使用确实没有这个问题的 catgets。但是有一种非常简单而强大的方法可以使用 gettext 函数来处理这类问题。
例如,考虑以下虚构的情况。GUI 程序有一个带有以下条目的菜单栏:
+------------+------------+--------------------------------------+
| File | Printer | |
+------------+------------+--------------------------------------+
| Open | | Select |
| New | | Open |
+----------+ | Connect |
+----------+
要翻译字符串 File、Printer、Open、New、Select 和 Connect,必须在代码中的某个位置调用 gettext 系列的函数。但是在两个地方,传递给函数的字符串是 Open。翻译可能不一样,因此我们处于上述困境。
该问题的一种解决方案是人为地扩展字符串以使其明确。但是,如果没有可用的翻译,程序会怎么做?扩展字符串不是应该打印的。所以我们应该使用稍微修改过的函数版本。
要扩展字符串,应该使用统一的方法。例如,在上面的示例中,字符串可以选择为
Menu|File
Menu|Printer
Menu|File|Open
Menu|File|New
Menu|Printer|Select
Menu|Printer|Open
Menu|Printer|Connect
现在所有的字符串都不同了,如果现在使用下面的小包装函数而不是 gettext,一切正常:
char *
sgettext (const char *msgid)
{
char *msgval = gettext (msgid);
if (msgval == msgid)
msgval = strrchr (msgid, '|') + 1;
return msgval;
}
这个小功能的作用是识别没有翻译可用的情况。这可以通过指针比较非常有效地完成,因为返回值是输入值。如果没有翻译,我们知道输入字符串是我们用于菜单条目的格式,因此包含一个 |特点。我们只需搜索该字符的最后一次出现并返回指向其后字符的指针。就是这样!
如果现在一直使用扩展字符串形式并用对 sgettext 的调用替换 gettext 调用(这通常仅限于 GUI 实现中的极少数地方),那么就有可能产生一个可以国际化的程序。
使用高级编译器(例如 GNU C),可以将 sgettext 函数编写为内联函数或宏,如下所示:
#define sgettext(msgid) \ ({
const char *__msgid = (msgid); \ char *__msgstr = gettext (__msgid); \ if (__msgval == __msgid) \ __msgval = strrchr (__msgid, '|') + 1; \ __msgval; })
其他 gettext 函数(dgettext、dcgettext 和 ngettext 等效项)也可以并且应该具有看起来几乎相同的相应函数,除了参数和对底层函数的调用。
现在当然有一个问题,为什么 GNU C 库中不存在这样的函数?这个问题的答案有两个部分。
-
它们很容易编写,因此可以由使用它们的项目提供。这本身不是一个答案,必须与第二部分一起看,