glibc 知:手册08:消息翻译

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

上一节提到的问题源于以下事实:

  1. 这些号码被分配一次,由于可能经常使用它们,以后很难更改号码。
  2. 这些数字不允许对字符串进行任何猜测,因此很容易发生冲突。

通过不断使用符号名称并提供一种将字符串内容映射到符号名称的方法(但是这会发生),可以防止上述两个问题。这样做的代价是程序员在编写程序本身时必须编写完整的消息目录文件。

这是必要的,因为在编译程序源之前必须将符号名称映射到数字。在上一节中,描述了如何生成包含名称映射的标头。例如,对于上一节中给出的示例消息文件,我们可以如下调用 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 函数执行:

  1. 找到一组消息目录。有许多不同语言的文件都属于该包。通常它们都存储在某个目录下的文件系统中。

    可以安装任意多个软件包,并且它们可以遵循不同的文件放置准则。

  2. 相对于包指定的位置,必须根据用户的意愿搜索实际的翻译文件。即,对于用户选择的每种语言,程序应该能够找到适当的文件。

这是 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 库中不存在这样的函数?这个问题的答案有两个部分。

  • 它们很容易编写,因此可以由使用它们的项目提供。这本身不是一个答案,必须与第二部分一起看,第二部分是:

  • C 库不可能包含可以在任何地方工作的版本。问题是选择字符以将前缀与扩展字符串中的实际字符串分开。上面使用的例子 |这是一个很好的选择,因为它类似于此上下文中经常使用的符号,而且它也是消息字符串中不经常使用的字符。

    但是,如果在消息字符串中使用了该字符怎么办。或者,如果所选字符在编译机器上的字符集中不可用(例如,ISO C 不需要存在 |;这就是 ISO C 编程环境中存在 iso646.h 文件的原因)。

剩下的只有一条评论了。上面的包装函数要求翻译字符串本身不被扩展。这只是合乎逻辑的。没有必要消除字符串的歧义(因为它们从不用作搜索的键),这样做还可以节省相当多的内存和磁盘空间。

2.2.1.6. 用户对 gettext 的影响

User influence on gettext

最后几节描述了程序员可以做些什么来使程序的消息国际化。但是最终由用户来选择他/她想要看到的消息。他/她必须理解他们。

POSIX 语言环境模型使用环境变量 LC_COLLATE、LC_CTYPE、LC_MESSAGES、LC_MONETARY、LC_NUMERIC 和 LC_TIME 来选择要使用的语言环境。这样用户可以影响很多功能。正如我们上面提到的,gettext 函数也利用了这一点。

要了解这是如何发生的,有必要查看文件名的各个组成部分,这些组成部分被计算出来以定位消息目录。它的组成如下:

dir_name/locale/LC_category/domain_name.mo

dir_name 的默认值是系统特定的。它是根据在配置 C 库时作为前缀给出的值计算得出的。该值通常是 /usr 或 /。对于前者,完整的 dir_name 是:

/usr/share/locale

我们可以使用 /usr/share,因为包含消息目录的 .mo 文件是独立于系统的,因此所有系统都可以使用相同的文件。如果程序为当前处理的消息域执行了 bindtextdomain 函数,则 dir_name 组件正是作为第二个参数提供给该函数的值。即,bindtextdomain 允许覆盖唯一的系统相关和固定值,从而可以在文件系统中的任何位置寻址文件。

类别是在程序代码中选择的语言环境类别的名称。对于 gettext 和 dgettext,这始终是 LC_MESSAGES,对于 dcgettext,这由第三个参数的值选择。如上所述,应避免使用 LC_MESSAGES 以外的类别。

语言环境组件是根据使用的类别计算的。就像这里的 setlocale 函数一样,用户选择也随之而来。某些环境变量以固定顺序检查,第一个环境变量集确定查找过程的返回值。详细地,对于类别 LC_xxx,按此顺序检查以下变量:

LANGUAGE
LC_ALL
LC_xxx
LANG

这看起来很熟悉。除了 LANGUAGE 环境变量之外,这正是 setlocale 函数使用的查找顺序。但是为什么要引入 LANGUAGE 变量呢?

原因是这些变量可以具有的值的语法与 setlocale 函数所期望的不同。如果我们将 LC_ALL 设置为遵循扩展语法的值,则意味着 setlocale 函数也将永远无法使用此变量的值。一个额外的变量消除了这个问题,而且我们可以独立于区域设置来选择语言,这有时很有用。

而对于 LC_xxx 变量,该值应该只包含一个语言环境规范,而​​ LANGUAGE 变量的值可以由一个冒号分隔的语言环境名称列表组成。细心的读者会意识到这是我们设法实现上述附加要求之一的方式:我们希望能够指定语言的有序列表。

回到构造的文件名,我们只缺少一个组件。domain_name 部分是使用 textdomain 函数注册或作为第一个参数提供给 dgettext 或 dcgettext 的名称。现在很明显,程序代码中域名的一个不错的选择是与程序/包名称密切相关的字符串。例如,对于 GNU C 库,域名是 libc。

一段有限的示例代码应该显示程序应该如何工作:

{
  setlocale (LC_ALL, "");
  textdomain ("test-package");
  bindtextdomain ("test-package", "/usr/local/share/locale");
  puts (gettext ("Hello, world!"));
}

在程序启动时,默认域是消息,默认语言环境是“C”。setlocale 调用根据用户的环境变量设置语言环境;请记住,gettext 的正确运行依赖于 LC_MESSAGES 语言环境(用于查找消息目录)和 LC_CTYPE 语言环境(用于字符集转换)的正确设置。textdomain 调用将默认域更改为 test-package。bindtextdomain 调用指定域 test-package 的消息目录可以在目录 /usr/local/share/locale 下找到。

如果用户在她/他的环境中将变量 LANGUAGE 设置为 de gettext 函数将尝试使用文件中的翻译

/usr/local/share/locale/de/LC_MESSAGES/test-package.mo

从上面的描述应该清楚这个文件名的哪个部分是由哪个来源决定的。

在上面的示例中,我们假设 LANGUAGE 环境变量为 de。这可能是一个合适的选择,但是如果用户想要使用 LC_ALL 因为更广泛的可用性而需要的值是 de_DE.ISO-8859-1,会发生什么?我们在上面已经提到,这样的情况并不少见。例如,一个人可能更喜欢阅读方言,如果这不可用,则使用标准语言。

gettext 函数知道这样的情况并且可以优雅地处理它们。这些函数识别环境变量值的格式。它可以将值拆分为不同的部分,并且通过省略唯一的部分或其他部分,它可以构造新的值。这当然是以可预测的方式发生的。要理解这一点,必须知道环境变量值的格式。有一种或多或少的标准化形式,最初来自 X/Open 规范:

language[_territory[.codeset]][@modifier]

不太具体的语言环境名称将按以下列表的顺序被剥离:

  1. odeset
  2. normalized codeset
  3. territory
  4. modifier
    语言字段永远不会因为显而易见的原因而被删除。

唯一的新事物是规范化的代码集条目。这是另一个好处,旨在帮助减少由于人们无法标准化字符集名称而导致的混乱。通常可以看到 8859-1、88591、iso8859-1 或 iso_8859-1,而不是 ISO-8859-1。通过应用以下规则,从用户提供的字符集名称生成规范化代码集值:

  1. 删除除数字和字母之外的所有字符。
  2. 将字母折叠成小写。
  3. 如果相同仅包含数字,则在字符串“iso”之前添加。

所以上面所有的名字都会被标准化为iso88591。这允许程序用户在选择语言环境名称时有更多的自由。

即使这个扩展功能仍然无助于解决可以使用完全不同的名称来表示相同的语言环境(例如,de 和 German)的问题。为了在这种情况下有所帮助,语言环境实现以及 gettext 函数都知道别名。

文件 /usr/share/locale/locale.alias(将 /usr 替换为您用于配置 C 库的任何前缀)包含替代名称到更常规名称的映射。系统管理员可以自由添加新条目来满足她/他自己的需要。从环境中选择的语言环境将与此文件第一列中的条目进行比较,忽略大小写。如果它们匹配,则使用第二列的值进行进一步处理。

在环境变量格式的描述中,我们已经提到字符集是选择消息目录的一个因素。实际上,只能使用包含使用系统/程序字符集编写的文本的目录(直接使用;总有一天会有解决方案)。这意味着对于用户来说,他/她将始终需要注意这一点。如果在消息目录的集合中存在相同语言但使用不同字符集编码的文件,则用户必须小心。

2.2.2. 为 gettext 处理消息目录的程序

Programs to handle message catalogs for gettext

GNU C 库不包含用于处理 gettext 函数的消息目录的程序的源代码。作为 GNU 项目的一部分,GNU gettext 包包含开发人员需要的一切。这个包中的工具提供的功能远远超过了上面描述的 gencat 程序的 catgets 功能的能力。

有一个程序 msgfmt 与 gencat 程序等效。它从消息目录的人类可读和可编辑形式生成一个二进制文件,gettext 函数可以使用该文件。但是还有更多可用的程序。

xgettext 程序可用于自动从源文件中提取可翻译的消息。即,程序员不需要处理翻译和必须翻译的消息列表。他/她将简单地将可翻译的字符串包装在对 gettext et.al 的调用中,其余的将由 xgettext 完成。该程序有很多选项可以帮助自定义输出或帮助更好地理解输入。

当新消息出现在源文件中或出现新的消息翻译时,其他程序有助于管理开发周期。这里只需要注意的是,使用 GNU gettext 中的所有工具可以完全自动化处理消息目录。除了在源代码中标记可翻译的字符串并生成翻译之外,开发人员无需自己做任何事情。

3. 参考

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值