9.1第48章

From PostgreSQL wiki
Jump to navigationJump to search

45.1. 寄语翻译家

PostgreSQL程序(服务器和客户端)可以用你喜爱的语言发出信息 — 只要那些信息被翻译过。 创建和维护翻译过的信息集需要那些熟悉自己的语言并且希望为PostgreSQL做一些事情的人。 实际上要干这件事你完全不必是个程序员。本章讲的就是如何帮助 PostgreSQL 做这件事。 45.1.1. 要求

我们不会评判你的语言能力 — 本章是有关软件工具的。 理论上,你只需要一个文本编辑器。 但这种情况只存在于你不想看看自己翻译的信息的情况下,这种情况很难发生吧。 在你配置你的源程序的时候,记住要使用 --enable-nls 选项。这样也会检查 libintl 库和 msgfmt 程序,它们是所有最终用户都需要的东西.要试验你的工作, 遵照安装指导中相应的部分就可以了。

如果你想开始一个新的翻译工作或者想做信息表融合工作(下面描述), 那么你就还分别需要有 GNU 兼容的 xgettext 和 msgmerge. 稍后我们将试着安排它,这样,如果你使用的是一个打包的源码发布, 那么你就不再需要xgettext。(从 CVS 里下源码的话, 你还是需要的。)我们现在推荐 GNU Gettext 0.10.36 或者更高。

你的本地 gettext 实现应该和它自身的文档在一起发布。 其中有一些可能和下面的内容重复,但如果要知道额外的细节,你应该看看它们。 45.1.2. 概念

信息原文(英语)和它们的(可能)翻译过的等价物都放在信息表里, 每个程序一个(不过相关的程序可以共享一个信息表)以及每种目标语言一个。 信息表有两种文件格式: 第一种是 "PO"(Portable Object)文件(可移植对象),它是纯文本文件,带一些翻译者编辑的特殊语法。 第二种是 "MO"(Machine Objece) 文件(意思是机器对象), 它是从相应的 PO 文件生成的二进制文件,在国际化了的程序运行的时候使用。 翻译者并不处理 MO 文件;实际上几乎没人处理它。

信息表的文件扩展名分别是 .po 或 .mo 就一点也不奇怪了。主文件名要么是它对应的程序名,要么是该文件适用的语言,视情况而定。 这种做法有点让人混淆。 例子是 psql.po(psql 的PO 文件)或者 fr.mo(法语的 MO 文件)。

PO 文件的文件格式如下所示:

  1. comment

msgid "original string" msgstr "translated string"

msgid "more original" msgstr "another translated" "string can be broken up like this"

...

msgid 是从程序源代码中抽取的。(其实不必是从源码,但这是最常用的方法。) msgstr 行初始为空,由翻译者填充有用的字串。 该字串可以包含 C 风格的逃逸字符并且可以象我们演示的那样跨行继续。(下一行必须从该行的开头开始。)

  1. 字符引入一个注释。如果 # 后面紧跟着空白,那么这是翻译者维护的注释。 文件里也可能有自动注释,它们是在 # 后面紧跟着非空白字符。这些是由各种在 PO 文件上操作的工具生成的, 主要目的是帮助翻译者。
  1. . automatic comment
    filename.c:1023
  2. , flags, flags
  1. . 风格的注释是从使用信息的源文件中抽取的。 程序员可能已经插入了一些信息给翻译者,比如那些预期的格式等。 #: 注释表示该信息在源程序中使用的准确位置。 翻译者不需要查看程序源文件,不过如果他觉得翻译得不对劲, 那么他也可以查看。#, 注释包含从某种程度上描述信息的标志。 目前有两个标志:如果该信息因为程序源文件的修改变得过时了, 那么设置 fuzzy(模糊)。翻译者然后就可以核实这些, 然后删除这个模糊标志。请注意模式信息是最终用户不可见的。 另外一个标志是 c-format,它表示该信息是一个 printf风格的格式模版。 这就意味着翻译也应该是一个格式化字串,带有相同数目和相同类型的占位符。 我们有用于核实这些的工具,它们生成 c 格式(c-format)标志的键。

45.1.3. 创建和维护信息表

好,那我们怎么创建一个"空白"的信息表呢? 首先,进入包含你想翻译信息的程序所在的目录。如果那里有一个文件叫 nls.mk, 那么这个程序就已经准备好翻译了。

如果目录里已经有一些 .po 文件了, 那么就是有人已经做了一些翻译工作了。这些文件是用 language.po 命名的,这里的 language 是 ISO639-1 规定的两字母语言代码(小写),比如, fr.po 指的是法语。如果每种语言需要多过一种的翻译,那么这些文件也可以叫做 language_region.po 这里的 region 是 ISO3166-1 规定的两字母国家代码(大写),比如, pt_BR.po 指的是巴西葡萄牙语。如果你找到了你想要的语言文件,那么你就可以在那个文件上干活。

如果你需要开始一个新的翻译工作,那么首先运行下面的命令

gmake init-po

这样将创建一个文件 progname.pot (用 .pot 和"生产中"使用的 PO 文件区分开。T代表"template"。)把这个文件拷贝成 language.po 然后编辑它.要让程序知道有新语言可以用,还要编辑文件 nls.mk,增加该语言(或者语言和国家)代码到类似下面这样的行:

AVAIL_LANGUAGES := de fr

(当然可能还有其它国家。)

随着下层的程序或者库的改变,程序员可能修改或者增加信息。 这个时候,你不必从头再来。只需要运行下面的命令

gmake update-po

它将创建一个新的空信息表文件(你开始时用的 pot 文件)并且会把它和现有的 PO 文件融合起来。 如果融合算法不能确定某条具体的信息是否融合正确,那么它就会象上面解释那样把它定义为 "fuzzy(模糊)"。如果有些事情真是出错了,那么旧的 PO 文件就会保存为带 .po.old 扩展的文件。 45.1.4. 编辑 PO 文件

PO 文件可以用普通的文本编辑器编辑。翻译者应该只修改那些在 msgstr 指示符后面的双引号中间的内容, 也可以增加评注和修改模糊(fuzzy)标志。Emacs 有一个用于 PO 的模式,我觉得相当好用。

我们不需要把 PO 文件完全填满。如果有些字串没有翻译(或者是一个空白的翻译),那么软件会自动使用原始的字串。 也就是说提交一个未完成的翻译包括再源码树中并不是一个问题; 那样可以留下让其它人继续你的工作的空间。不过, 我们鼓励你在完成一次融合之后首先消除模糊的条目。 要知道模糊的条目是不会被安装的;它们只起到表示可能是正确翻译的引用的作用。

下面是一些编辑翻译信息的时候要记住的事情:

   *
     确保如果原始信息是以换行结尾的话,翻译信息也如此。类似的情况适用于 tab 等。
   *
     如果最初的字串是 printf 格式的字串,那么翻译串也必须如此。 翻译串还要有同样的格式声明词,并且顺序相同。 有时候语言的自然规则会让这么做几乎不可能或者及其难看。 这时候你可以用下面的格式:
     msgstr "Die Datei %2$s hat %1$u Zeichen."
     这样,第一个占位符实际上使用列表里的第二个参数。 digits$ 应该跟在 % 后面并且在其它格式操作符之前。 (这个特性实际上存在于 printf 家族函数中。 你可能从来没有听说过它,因为除了信息国际化以外它没有什么用处。)
   *
     如果原始的字串包含语言错误,那么请报告它(或者在程序源文件中直接修补它), 然后按照正常翻译。正确的字串可能跟在程序源文件更新的时候融合进来。 如果最初的字串与事实不符,那么请报告它(或者自己修补它) 但是不要翻译它。相反,你可以在 PO 文件中用注解给该字串做个标记。
   *
     维护风格和原始字串的语气。特别是那些不成句子的信息(cannot open file %s)可能不应该以大写字符开头 (如果你的语言区分大小写的话)或者用句号结束(如果你的语言里有符号标志)。
   *
     如果你不知道一条消息是什么意思,或者它很含糊, 那么在开发者邮递列表上询问。因为可能说英语的最终用户也可能不明白它或者觉得它不好理解,所以我们最好改善这些信息。 


45.2. 寄语程序员 45.2.1. 机理

本节描述如何在属于PostgreSQL 版本的程序或者库里面支持本地语言。 目前它只适用于 C 语言.

向程序中增加 NLS 支持

  1.
     把下面的代码插入到程序的开头∶
     #ifdef ENABLE_NLS
     #include <locale.h>
     #endif
     ...
     #ifdef ENABLE_NLS
     setlocale(LC_ALL, "");
     bindtextdomain("progname", LOCALEDIR);
     textdomain("progname");
     #endif
     (这里的 progname 实际上可以自由选择。)
  2.
     如果发现一条需要翻译的信息,那么就需要插入一个对 gettext() 的调用。比如
     fprintf(stderr, "panic level %d\n", lvl);
     会改成
     fprintf(stderr, gettext("panic level %d\n"), lvl);
     (如果没有配置 NLS,那么gettext 会定义成一个无操作。)
     这么干会出现一堆东西.一种常用的缩写是
     #define _(x) gettext((x))
     如果程序通过一个或者少数几个函数做了大量的通讯, 比如后端里的 elog(), 那么也可以用另外一个方法.然后你另这些函数在所有输入值上内部调用 gettext。
  3.
     在带程序源代码的目录里加一个文件 nls.mk. 这个文件将被当做 makefile 读取.在这里需要做下面一些变量的 赋值∶
     CATALOG_NAME
         程序的名字,就是那些在 textdomain() 调用里提供的。 
     AVAIL_LANGUAGES
         提供的翻译的语言列表 — 开始的时候是空的。 
     GETTEXT_FILES
         一列包含可翻译字串的文件,也就是那些用 gettext 或者其它相应手段标记了的文件。最终,这里会包括几乎所有的程序源文件。 如果列表太长,你可以把第一个"文件"写成一个 +和第二个词组成,第二个词是一个文件,在这个文件里每行包含一个文件名。 
     GETTEXT_TRIGGERS
         生成给翻译者使用的信息表的工具,以便知道哪些函数调用包含可翻译字串。 缺省时只知道 gettext() 调用。 如果你使用了 _ 或其它标识符,那么你需要把它们列在这里。 如果可翻译字串不是第一个参数,那么该项需要是下面的形式: func:2(用于第二给参数)。 

制作系统将自动处理制作和安装信息表。 45.2.2. 消息书写指导

这里是一些如何书写消息就可以简化消息翻译方面的指导:

   *
     不要偷懒在运行时构造语句,比如象
     printf("Files where %s.\n", flag ? "copied" : "removed");
     语句里这样的单词顺序会让其它语言很难翻译。 而且,即使你记得在每个片断上调用 gettext(), 这些片断也不一定能很好地独立翻译。 我们最好重复一些代码,好让每条消息可以当作有机的整体进行翻译。 只有数字,文件名和类似的运行时变量才应该在运行时插入消息文本。
   *
     出于类似的原因,下面的东西不能用∶
     printf("copied %d file%s", n, n!=1 ? "s" : "");
     因为它假设了如何找复数。如果你看到这样的东西,你可以用下面方法解决
     if (n==1)
         printf("copied 1 file");
     else
         printf("copied %d files", n):
     不过还是有让人失望的时候,有些语言在某些特殊规则上有超过两种形式, 我们可能在将来解决这个问题,但是就目前而言,我们最好还是完全 避免这些东西.你可以这样写∶
     printf("number of copied files: %d", n);
   *
     如果你想和翻译者进行交流,比如说一条信息是如何与其它输出对齐的,那么在该字串出现之前, 放上一条以 translator 开头的注释,比如
     /* translator: This message is not what it seems to be. */
     这些注释都拷贝到信息表文件里,这样翻译者就可以看见它们了. 

Chapter 46. 书写一个过程语言句柄

调用函数的时候,如果函数的书写语言不是目前的"版本 1"的编译语言接口(这包括用户定义的过程语言写的函数, 用 SQL 写的函数,以及用版本 0 的编译语言接口写的函数),都会通过一个 调用句柄处理具体的语言。 调用句柄有责任用一种有意义的方法执行这个函数,比如说解析所提供的文本等等。本章简介如何书写一个新的过程语言调用句柄。

过程语言的调用句柄是一个"普通"的函数, 必须使用一种编译语言来写,比如 C,使用版本-1的接口,并且在 PostgreSQL 里注册成接受零个参数并且返回类型 language_handler。 这个特殊的伪类型标识该函数为一个调用句柄并且避免它直接在 SQL 命令中被调用。

调用句柄的调用方式和其它函数一样:它接受一个指向一个 FunctionCallInfoData struct 的指针,这个指针包含参数值和有关被调用的函数的信息, 并且预期它返回一个 Datum 结果(如果它希望返回一个 SQL 的空结果, 那么可能设置 isnull 字段)。 调用句柄和普通的被调函数的区别是 FunctionCallInfoData 结构的 flinfo->fn_oid 字段强包含实际要调用的函数的 OID, 而不是调用句柄自身。调用句柄必须使用这个字段判断要执行哪个函数。 通常,传递进来的参数列表也是按照目标函数的声明设置的,而不是给调用句柄设置的。

从系统表 pg_proc 里抓取函数入口以及分析被调函数的参数和返回类型就是调用句柄的事了。 来自 CREATE FUNCTION 命令中的 AS 子句将会在 pg_proc 行的 prosrc 字段中找到。这个通常是过程语言本身的源文本,但也可以是别的东西,比如一个指向某个文件的路径名, 或者任何告诉调用句柄如何详细处理的东西。

通常,每个 SQL 语句里面可能要调用同一个函数多次。 调用句柄可以利用 flinfo->fn_extra 字段避免重复地查找有关被调函数地信息。 这个字段初始为 NULL,但是可以被调用句柄设置为指向有关被调函数的信息。 在随后的调用中,如果 flinfo->fn_extra 已经为非NULL,那么就可以直接使用它而免于重新查找信息。 调用句柄必须确保 flinfo->fn_extra 是用于指向一块至少会生存到当前查询结束的内存区里, 因为一个 FmgrInfo 数据结构将会保存那么长的时间。 一个实现这个要求的方法是在 flinfo->fn_mcxt 声明的内存环境里分配一块额外的数据; 这样的数据通常和 FmgrInfo 自己有一样的生命期。 但是句柄也可以同样选择使用一个更长生存期的环境,这样它就可以跨查询缓存函数定义。

在过程语言函数以触发器的形式调用的时候,就没有什么参数以通常的方式传递进来, 而是 FunctionCallInfoData 的 context 字段指向一个 TriggerData 结构, 而不是像普通函数调用里面的 NULL 那样。一个语言句柄应该为过程语言函数提供获取触发器信息的机制。

下面是一个用 C 写的存储过程语言句柄的模版:

  1. include "postgres.h"
  2. include "executor/spi.h"
  3. include "commands/trigger.h"
  4. include "fmgr.h"
  5. include "access/heapam.h"
  6. include "utils/syscache.h"
  7. include "catalog/pg_proc.h"
  8. include "catalog/pg_type.h"

PG_FUNCTION_INFO_V1(plsample_call_handler);

Datum plsample_call_handler(PG_FUNCTION_ARGS) {

   Datum          retval;
   if (CALLED_AS_TRIGGER(fcinfo))
   {
       /*
        * 以触发器过程调用
        */
       TriggerData    *trigdata = (TriggerData *) fcinfo->context;
       retval = ...
   }
   else
   {
       /*
        * 以函数调用
        */
       retval = ...
   }
   return retval;

}

在打点的地方放上几千行代码就可以完成调用句柄。

在把句柄函数编译成一个可装载的模块(参阅 Section 32.9.6)之后, 下面的命令就可以注册这个例子过程语言:

CREATE FUNCTION plsample_call_handler() RETURNS language_handler

   AS 'filename'
   LANGUAGE C;

CREATE LANGUAGE plsample

   HANDLER plsample_call_handler;

如果想书写自己的调用句柄,那么包含在标准发布里面的过程语言是很好的例子。 参考一下源代码树中的 src/pl 子目录。