WordPress插件开发教程手册 — 国际化和本地化

国际化

什么是国际化?

国际化是插件开发的一个过程,国际化后,插件可以很方便的翻译成其他语言,国际化通常缩写为 i18n(Internationalization 中的第一个字母 i 和最后一个字母 n 中间有 18 个字母)。

为什么要进行国际化

WordPress 在世界各地使用,很多国家的主要语言不是英语,WordPress插件中的字符需要用特殊的方式进行编码,以便可以很容易翻译成其他语言。作为开发者,我们不可能为所有的用户提供本地化服务,插件进行了国际化后,使用各种语言的翻译者不用修改代码就可以为插件或主题提供本地化翻译。

进一步了解国际化可以阅读 “怎么国际化我们的插件文章“。

资源

本地化

什么是本地化?

本地化是插件国际化之后,翻译为各种本地语言的过程,本地化的缩写为 l10n (localization 第一个字母 l 和最后一个字母 n 之间有 10 个字母)。

本地换文件

POT (Portable Object Template,便携对象模版) 文件

该文本包含插件中的原始字符串(英文),下面是一个 POT 文件的片段。

#: plugin-name.php:123
msgid Page Title
msgstr 

PO (Portable Object 便携对象) 文件

每个翻译者都将使用 POT 文件,把 msgstr 部分翻译为自己的语言。翻译的结果就是 PO 文件,PO 文件的格式和 POT 文件相同,但带有翻译后的字符串和一些特定的头信息,每种语言都有一个对应的 PO 文件。

MO (Machine Object 机器对象) 文件

每个翻译后的 PO 文件最终都会被 msgfmt 工具编译成 MO 文件。MO 文件是供机器读取的二进制文件,由 gettext 函数使用(机器不关心 PO 和 POT 文件)。根据需要,应用程序中多个不同的模块可以使用自己的 MO 文件(比如 WordPress 内核、插件、主题都有自己的 MO 文件),各个模块之间通过文本域(text domain)区分。

生成 POT 文件

POT 文件是我们需要交给翻译人员的文件,以便他们根据此文件进行翻译,POT 和 PO 文件可以很容易的进行转换,直接修改文件名就可以了。把 POT 文件包含在插件中通常是一个不错的做法,这样翻译人员就可以直接使用该文件进行翻译了,有几种方法可以为我们的插件生成一个 POT 文件。

插件目录管理工具

如果我们的插件托管在 WordPress.org 插件目录,打开插件的 “Admin” 页面,然后在  “Generate POT file” 区域,点击 ‘Continue’ 按钮。

然后点击 Get POT 按钮下载 POT 文件。

WordPress i18n 工具

如果我们的插件不在 WordPress插件目录中,我们可以可以中 SVN 中检出 WordPress Trunk 目录(使用方法参见 Using Subversion 文章)。我们需要检出整个主干,因为 wordpress-i18n 工具 使用 WordPress 核心代码来生成 POT 文件。运行下面的命令之前,我们需要安装 gettext(GNU国际化实用程序)工具和 PHP。

打开命令行并切换到插件的 language 文件夹。

cd plugin-name/languages

然后,使用下面命令生成 POT 文件

php path/to/makepot.php wp-plugin path/to/your-plugin-directory

使用 Poedit 软件

翻译插件时,我们可以使用 Poedit 软件,这是一个开软的翻译工具,支持所有主流操作系统。免费版本支持使用 Gettext 函数手动扫描所有源代码。专业版还可以帮我们扫描 WordPress插件并生成 POT 文件,在保存的时候,Poedit 会自动帮我们编译 MO 文件,该文件不是必须的,我们可以删除这个文件。如果不像买专业版,我们可以下载一个空白 POT 文件,把空白 POT 文件放入语言文件夹,然后点击 Poedit 中的 “更新” 按钮来扫描插件文件更新 POT 文件。Poedit 的界面如下。

Grunt 任务

如果我么之前使用过 Grunt,有一些 Grunt 任务可以帮助我们创建 POT 文件. grunt-wp-i18n 和 grunt-pot,使用这些工具我们需要安装 node.js。然后在插件目录中安装 Grunt 工具。安装通过命令行完成. 这里有一个Grunt.js 和 package.json 模版,我们可以放到插件的根目录中,然后我们可以使用一个简单的命令生成 POT 文件。

翻译 PO 文件

有很多种方式来翻译 PO 文件。

我们可以直接在文本编辑器中翻译。比如,未翻译的 PO 文件片段如下:

#: plugin-name.php:123
msgid Page Title
msgstr 

翻译的时候,我们直接把译文输入到 msgstr 的引号中间就可以了,如下:

#: plugin-name.php:123
msgid Page Title
msgstr 页面标题

我们也可以使用 Poedit 软件打开 PO 文件直接翻译。

除了在本地翻译 PO 文件,我们还可以使用一些在线翻译服务。整体思路是上传 POT 文件,然后授权用户或翻译人员翻译我们的插件。这中方法可以保持我们的翻译始终是最新的,避免重复翻译。

François-XavierBénard 正在运行 WP-Translations,一个 “译者与开发人员会面” 的社区。它在 Transifex 上运行,你可以作为开发者提交翻译项目让翻译人员翻译,或者把现有的插件或主题翻译为自己的语言。

下面是其他可以用来在线翻译 POT 文件的工具。

我们甚至可以使用 WordPress插件来进行翻译。

翻译后的文件将被保存为  my-plugin-{locale}.mo 文件,locale 是我们在 wp-config.php 中定义的语言代码/国家代码,例如,简体中文的语言环境是 zh_CN,在上面的示例中,文本域是 my-plugin,那么简体中文的 PO 和 MO 文件应该为 my-plugin-zh_CN.po 和 my-plugin-zh-CN.mo。有关语言和国家代码的更多信息,请参阅以您的语言安装 WordPress

生成 MO 文件

通过命令行

我们使用 msgfmt 来生成 MO 文件。 msgfmt 是 Gettext 工具包的一部分。否则,典型的 msgfmt 命令使用方法如下:

msgfmt -o filename.mo filename.po

如果我们有很多语言的 PO 文件需要生成,可以使用 bash 命令批量操作。

# Find PO files, process each with msgfmt and rename the result to MO
for file in `find . -name *.po` ; do msgfmt -o ${file/.po/.mo} $file ; done

使用 Poedit

Poedit 软件集成了 msgfmt,可以让我们来生成 MO 文件,我们可以在软件设置中启用这禁用这个功能。

Grunt 命令

有一个 grunt-po2mo 可以帮我们转换所有 PO 文件。

良好翻译小提示

不要直译,要意译

每种语言都有不同的语言习惯和结构,翻译为中文的时候,我们不应该按照英文的语言结构来做翻译,应该提取英文所表达的意思,然后用自然的中文表达出来。不要使用直接复制机器翻译工具的翻译结果,机器翻译目前还没有发展到能够照顾到我们语言习惯的程度。

尽量保持同样的翻译风格

每条语句都可以是非正式的或正式的,太正式的语气会让用户有距离感,太不正式的语气又会让用户觉得不严谨。WordPress 中的用于倾向于保持有礼貌的非正式语气,在翻译的时候,我们可以使用同样感觉的语气。

不要使用俚语或方言词汇

一些俚语或方言词汇,离开了某个地方,能听懂的人就比较少了,在 WordPress 中,尽量不要使用这样的词汇来翻译,因为 WordPress 是一个国际化的平台,世界各地都有人在使用。如果有一些术语很难翻译,保持原样会让使用者更加容易理解。比如 WordPress 中的 pingback、trackback 或 feed 等。

参考其他主题或插件的本地化翻译

如果我们刚接触 WordPress 本地化,或者对本地化用语没有把握,可以参考 WordPress 核心、或者其他优秀主题、插件的本地化翻译,看他们是如何对一些字段进行翻译的。

使用本地化文件

我们可以把本地化文件放到插件目录中,WordPress 3.7 以后,我们也可以把翻译文件提取出来,放到 WordPress 的 wp-content/languanges 目录中,后者可以避免自定义翻译文件在插件更新时被覆盖。

从 WordPress 4.0 开始,我们可以在 “常规设置” 中更改语言,如果在选项中看不到自己需要的语言,说明语言文件没有下载。比如我们想切换到法语,可以通过下面几个步骤实现。

  • 在 wp-config.php 文件中定义需要使用的语言代码常量 WPLANG ,如,我们需要切换到法语,在 wp-config.php 中加入 define (‘WPLANG’, ‘fr_FR’);
  • 然后打开 wp-admin/options-general.php 或 “设置” -> “常规”
  • 在 “站点语言” 下拉选项中选择我们需要的语言
  • 开发 wp-admin/update-core.php
  • 点击 “更新翻译”
  • WordPress 会帮我们下载对应的语言文件

资源

怎么国际化插件

为了让我们程序中的字符可以翻译为其他语言,我们需要将原始字符放到一个特殊函数中。

Gettext 函数

WordPress 使用 i18n 的 gettext 库和工具来实现国际化,如果我们在代码中看到 _() 函数,该函数使用的就是本地的 PHP Gettext 兼容翻译功能。在 WordPress 中,我们应该使用WordPress 定义的 __() 函数,如果我们想获得更广泛更深入的 Gettext 功能,建议阅读一下 Gettext 手册。

文本域

一个文本可能在多个主题或插件中出现,我们使用文本域来区分这些文本的来源,文本域是一个唯一标识符,用来确保 WordPress 可以区分加载的所有翻译。这种机制增强了可移植性,可以更好的和现有的 WordPress 工具结合。插件的文本域必须和插件的 Slug 相同,如果我们的插件主文件为 my-plugin.php 文件,或者包含在一个名为 my-plugin.php 文件夹中,文本域应该是 my-plugin。如果我们的插件托管在 wordpress.org,文本域必须是插件 URL(wordpress.org/plugins/<slug>)中的 <slug>,文本域必须使用英文破折号而不是下划线。

字符串示例:

__( 'String (text to be internationalized)', 'text-domain' );

在实际的代码中,需要修改 “text-domain” 为我们的插件 slug。

不要使用变量名称作为gettext函数的文本域。如,不要这样做:__(‘Translate me.’, $text_domain);

文本域同时需要添加到插件头,及时在插件被禁用的情况下,WordPress 也会使用他来实现元数据的国际化。文本域应该与 加载文本域 中使用的相同。示例如下:

/*
 * Plugin Name: My Plugin
 * Author: Plugin Author
 * Text Domain: my-plugin
 */

文本域路径

我们需要设置一个文本域路径,以便 WordPress 知道在插件停用时从哪里可以找到翻译,文本域路径仅在插件使用单独的语言文件夹时才有用。例如,如果 .mo 文件位于插件的 languages 文件夹中,Domain Path 应该写成 “/languages”,路径的第一个字符必须为 /,代表插件的根目录。示例如下:

/*
 * Plugin Name: My Plugin
 * Author: Plugin Author
 * Text Domain: my-plugin
 * Domain Path: /languages
 */

基本字符串

在 WordPress 国际化的时候,我们使用最多的是 __() 函数,用来返回参数的翻译:

__( 'Blog Options', 'my-plugin' );

另外一个比较常用的是  _e() 函数,用来直接输入参数的翻译,相当于是下面代码的一个快捷方式。

echo __( 'WordPress is the best!', 'my-plugin' );

可以直接写成:

_e( 'WordPress is the best!', 'my-plugin' );

变量

如果我们需要在字符串中使用变量,需要使用占位符来代替变量。如下面的代码:

echo 'Your city is $city.'

在国际化的时候,我们需要使用 printf 和 sprintf 来输入带变量的字符串。如下:

printf(
/* translators: %s: Name of a city */
   __( 'Your city is %s.', 'my-plugin' ),
   $city
);

注意,上面的字符串中,可以翻译的只是字符串中不包含占位符的部分,占位符 %s 在翻译时保持不变,页面中显示时,所代表的字符串也不变。

如果我们的字符串中有多个占位符,可以使用参数交换,在这种情况下,必须使用单引号,双引号会把 $s 当作 s 变量,示例如下。

printf(
/* translators: 1: Name of a city 2: ZIP code */
   __( 'Your city is %1$s, and your zip code is %2$s.', 'my-plugin' ),
   $city,
   $zipcode
);

在上面的代码中, % 和 $ 中间的数字代表后面变量的位置,%1$s 对应 $city, %2$s 对应 $zipcode,有时候,我们需要把后面变量 $zipcode 显示在前面, 这种情况下,交互一下翻译模版中 %1$s 和 %2$s 的位置就可以了,如下:
printf(
/* translators: 1: Name of a city 2: ZIP code */
__( ‘Your zip code is %2$s, and your city is %1$s.’, ‘my-plugin’ ),
$city,
$zipcode
);
注意!下面的代码是不正确的。

// This is incorrect do not use.
_e( Your city is $city., 'my-plugin' );

翻译字符串是从源代码中提取的,所以翻译人员翻译的时候会看到 Your city is $city. 的源字符串。

在应用程序中,如果翻译函数 _e() 找不到源字符串对应的翻译,显示的时候会直接使用源字符串。如 “Your city is London”,而翻译人员翻译的时候没有翻译这个字符串,gettext 函数会直接返回这个字符串。

复数

基本复数

如果我们的字符串在数量为复数是发生变化。比如在英文中的 One comment 和 Two comments,在其他语言中,可能会有更多复数形式。在 WordPress 国际化和本地化的时候,我们可以使用_n() 函数。

printf(
   _n(
      '%s comment',
      '%s comments',
      get_comments_number(),
      'my-plugin'
   ),
   number_format_i18n( get_comments_number() )
);

_n() 函数接受 4 个参数:

  • singular –字符串的单数形式(请注意,在某些语言中,可以用数字以为的字符表示,所以 ‘%s item’ 应该写成 ‘One item’)
  • plural – 字符串的复数形式
  • count – 对象的数量,这将决定返回单数或复数形式(有些语言中,有两种以上的形式)
  • text domain – 插件的文本域

函数会根据函数指定的数量确定返回值是单数还是复数形式。

推迟的复数形式

首先,我们需要使用 _n_noop() 或 _nx_noop() 设置复数字符串。

$comments_plural = _n_noop(
   '%s comment.',
   '%s comments.'
);

在稍后的代码中,我们可以使用  translate_nooped_plural() 函数来皆在字符串。

printf(
   translate_nooped_plural(
      $comments_plural,
      get_comments_number(),
      'my-plugin'
   ),
   number_format_i18n( get_comments_number() )
);

上下文消除歧义

有时候,一个字符串可能会在多个语境中使用,尽管该字符串是相同的,他们表达的意思却不同,在其他语言中对应的翻译也不同。举个例子,Post 这个词,既可以作为动词使用 “Click here to post comment”,也可以作为名词使用 “Edit the post”,在这种情况下,我们应该使用 _x() 或 _ex() 函数,这两个函数类似于 __() 和 _e(),但是有一个附加的语境参数。如下:

_x( 'Post', 'noun', 'my-plugin' );
_x( 'Post', 'verb', 'my-plugin' );

这个语境参数会作为注释显示给翻译人员,翻译人员翻译的时候,会根据这个注释,选择合适的翻译本文。

__()_x() 一样,echo _x 可以直接写为 _ex(),上面的例子在直接输出的时候,可以直接写为:

_ex( 'Post', 'noun', 'my-plugin' );
_ex( 'Post', 'verb', 'my-plugin' );

翻译说明

有时候,翻译人员会不明白某些字符串所表达的意思,我们需要在代码中添加说明来帮助翻译人员理解,增加翻译说明的方法是在翻译字符串的前面一行加一个 PHP 当行注释,注释以字符 “translators:” 开头,示例如下:

/* translators: draft saved date format, see http://php.net/date */
$saved_date_format = __( 'g:i:s a' );

翻译注释也可以用来解释字符串中的占位符,如下:

/* translators: 1: WordPress version number, 2: plural number of bugs. */
_n_noop( '<strong>Version %1$s</strong> addressed %2$s bug.', '<strong>Version %1$s</strong> addressed %2$s bugs.' );

换行符

在字符串需要标记另起一行的时候,使用换行符号 \n,不要使用 \r,在 Gettext 中使用这个字符换行会有问题。

空字符串

空字符串是 Gettext 中的内部保留字符,所以不要尝试国际化一个空字符串,也不需要这样做,因为即便翻译了,用户看到的效果也是一样的。如果我们有一个有效的用例来使一个空字符串国际化,则可以同时添加上下文来帮助译员,并与Gettext系统保持和谐。

处理 JavaScript 文件

JavaScript 是在前端运行的,所以我们没办使用 Gettext 来国际化 JavaScript 文件,如果需要,使用 wp_localize_script() 来添加翻译字符串到前端,然后通过 JavaScript 的国际化方法来对 JavaScript 文件进行国际化。

转义字符

转义所有的字符串是好事,这样翻译者就没有机会运行恶意代码。WordPress 提供了一些与国际化相结合的转义函数。

基本函数

翻译并转义的函数

用于 HTML 标签属性的字符串必须被转义,国际化的时候也一样,使用下面的函数确保这些字符在国际化的时候被转义。

日期和数字函数

撰写字符串的最佳实践

下面是一下撰写国际化字符串的最佳实践

  • 使用正式英式风格 – 尽量不要使用俚语和缩写。
  • 使用整个句子 – 在大多数语言中,词序与英语中的不同。
  • 合并相关句子,但不要在一个字符串中包含整个页面的文本。
  • 不要将前导或尾随空白留在可翻译的短语中。
  • 假设翻译时,字符串的长度会加倍
  • 避免不常用的标记和不常用的控制字符 – 不要包含文字周围的标签
  • 不要将不必要的HTML标记放入翻译的字符串中
  • 除非可以使用其他语言的版本,否则不要在字符中包含 URL。
  • 将变量作为占位符添加到字符串中,就像在某些语言中占位符更改位置一样。
  • 使用格式字符串,而不是字符串连接 – 翻译短语而不是单词
  • 不同位置相同的字符串只需要翻译一个,使用相同的单词和符号,可以减少字符串数量,减少翻译人员的工作量例如。如:__( 'Posts:', 'my-plugin' ); 和 __( 'Posts', 'my-plugin' );

加载文本域带字符串

我们必须把文本域添加在 __()_e() 和 __n() 等国际化函数中,否则我们的翻译不会工作。如:__( 'Post' ) 应该为 __( 'Post', 'my-theme' )
如果插件中的字符串也在 WordPress 核心中使用(如 “Settings”,我们也需要添加自己的文本域到国际化函数),否则如果 WordPress 核心字符串发生了变化,我们的字符串会变成未翻译的了。

在编写代码时不断地添加文本域可能是一个负担,我们可以自动执行该操作:

  • 如果我们的插件位于 WordPress.org 插件目录中,转到 “管理” 页面,滚动到 “Add Domain to Gettext Calls”。
  • 上传要需要添加文本域的文件。
  • 然后点击 “Get domainified file”。

另外,我们也可以通过下面的方法在本地添加。

  • 下载 add-textdomain.php 文件到我们想要添加文本域的文件夹中。
  • 运行下面的命令 php add-textdomain.php my-plugin my-plugin.php &gt; new-my-plugin.php 生成一个添加了文本域的新文件
  • 如果我们想把 add-textdomain.php 放在不同的文件夹中,您只需要在命令中定义位置即可。php \path\to\add-textdomain.php my-plugin my-plugin.php &gt; new-my-plugin.php
  • 如果您不想输出新文件,请使用此命令。php add-textdomain.php -i my-plugin my-plugin.php
  • 如果要更改目录中的多个文件,可以将目录传递给脚本。php add-textdomain.php -i my-plugin my-plugin-directory

完成后,文本域将被添加到所有 gettext 函数的结尾。如果有一个现有的文本域,上面的命令不会替换他。

加载文本域

WordPress-4.6以后,我们可以直接在translate.wordpress.org翻译我们的插件了,通过translate.wordpress.org翻译的插件不需要 load_plugin_textdomain() 。如果我们不想将load_plugin_textdomain()添加到我们的插件,必须在readme.txt,指定Requires least:为4.6

最后,我们需要在插件中通过load_plugin_textdomain() 加载翻译后的 MO 文件,该函数从我们的指定插件文件夹中加载 {text-domain}-{locale}.mo,locale 是 WordPress 设置的国家/地区代码,有关语言和国家代码的更多信息,请参阅以您的语言安装 WordPress

在上面的代码示例中,文本域是 my-plugin,因此简体中文的 MO 和 PO 文件应该被命名为 my-plugin-zh_CN.mo 和 my-plugin-zh_CN.po。加载翻译文件的代码如下:

function my_plugin_load_plugin_textdomain() {
   load_plugin_textdomain( 'my-plugin', FALSE, basename( dirname( __FILE__ ) ) . '/languages/' );
}
add_action( 'plugins_loaded', 'my_plugin_load_plugin_textdomain' );

语言包

如果您对语言包感兴趣,以及怎么将语言包导入到 translate.wordpress.org 中,请阅读 关于翻译的 Meta Handbook 页面

国际化安全问题

在进行 WordPress 国际化时,安全问题往往会被忽视,有一些重要事项需要在这里提一下。

检查垃圾信息和其他恶意字符串

翻译人员向我们提交本地化信息时,要确认一下,他们的翻译中是否包含垃圾广告信息和其他恶意字符串,我们可以使用 Google 翻译将其翻译为我们的母语,然后比较一下原始字符串和翻译后的字符串。

转义国际化字符串

我们不能相信翻所有开发者只会添加正常的翻译文字,如果有一些开发者不怀好意,他们可以添加具有破坏性的 JavaScript 或其他代码。为了防止这种情况,我们需要向处理其他用户输入一样处理国际化字符串。

如果我们需要输入字符串,我们应该在输入的时候进行转义。

不安全的:

<?php _e( 'The REST API content endpoints were added in WordPress 4.7.', 'your-text-domain' ); ?>

安全的:

<?php esc_html_e( 'The REST API content endpoints were added in WordPress 4.7.', 'your-text-domain' ); ?>

另外,有些开发者选择依靠翻译验证机制而不是转义来避免这个问题,一个例子就是给予 WordPress Polyglotes 团队在 translate.wordpress.org 的编辑角色。这样做会确保不受信任的贡献者提交的任何翻译在被接受之前都已经被受信任的翻译人员验证过了。

对 URL 使用占位符

不要在国际化字符串中包含网址,因为不怀好意的翻译人员可能会把这些网站修改掉,指向其他网址。正确的做法是使用 printf() 或 sprintf()占位符。

不安全的做法:

<?php _e(
   'Please <a href=https://wordpress.org/support/register.php>
     register for a WordPress.org account</a>.',
   'your-text-domain'
); ?>

安全的做法:

<?php printf(
   __(
      'Please <a href=%s>register for a WordPress.org account</a>.',
      'your-text-domain'
   ),
   'https://wordpress.org/support/register.php'
); ?>

编译自己的 MO 文件

翻译人员通常会把翻译后的 MO 文件和 PO 文件一起发送给我们,我们应该丢弃他们的 MO 文件,而自己编译一份。因为 MO 文件是二进制的,我们无法查看他们发送的 MO 文件是由相同的 PO 文件编译的,还是由包含垃圾信息和其他恶意字符串的 PO 文件翻译的。

使用 Poedit 生成的二进制文件将覆盖 .po 文件中的头信息,所以最好使用命令行编译。

msgfmt -cv -o /path/to/output.mo /path/to/input.po

我们提供 WordPress主题和插件定制开发服务

本站长期承接 WordPress主题、插件、基于 WooCommerce 的商店商城开发业务。 我们有 10 年WordPress开发经验,如果你想 用WordPress开发网站, 请联系微信: iwillhappy1314,或邮箱: amos@wpcio.com 咨询。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

*