如何让 WordPress 插件支持国际化

如何让 WordPress 插件支持国际化

要让应用中的字符串可以翻译,需要把原始字符串包装在一组专用函数之一的调用中。这些函数统称为 gettext。

Gettext 简介

WordPress 使用 gettext 库和工具实现国际化,但并不直接调用它们,而是提供了一组专门用于字符串翻译的函数。下文列出的就是这些函数;插件中应使用它们。

要深入了解 gettext,请阅读 gettext 在线手册

文本域

使用文本域标识属于插件的全部文本。文本域是唯一标识符,确保 WordPress 能区分所有已加载的翻译。这可以提高可移植性,也更容易与现有 WordPress 工具协作。

文本域必须与插件的 slug 一致。如果插件是名为 my-plugin.php 的单个文件,或者位于 my-plugin 文件夹中,文本域必须为 my-plugin。如果插件托管在 wordpress.org,文本域必须是插件 URL 中的 slug 部分,即 wordpress.org/plugins/<slug>。

文本域名称必须使用短横线而非下划线,全部小写,且不能包含空格。

还需要在插件头部声明文本域。这样即使插件被停用,WordPress 也能对插件元数据进行国际化。这里的文本域应与加载文本域时使用的名称一致。 (loading the text domain)

头部示例:

/* 
 * Plugin Name: My Plugin
 * Author: Plugin Author
 * Text Domain: my-plugin
 */
同样,请把 my-plugin 换成你自己插件的 slug。
从 WordPress 4.6 开始,Text Domain 头部字段是可选的,因为它必须与插件 slug 相同。保留它没有问题,但并非必需。

文本域路径

文本域路径定义插件翻译所在的位置。它有多种用途,尤其是让 WordPress 在插件停用时仍知道去哪里查找翻译。默认位置是插件所在文件夹。

例如,如果翻译放在插件内部名为 languages 的文件夹中,Domain Path 就是 /languages,开头的斜杠不可省略:

头部示例:

/*
 * Plugin Name: My Plugin
 * Author: Plugin Author
 * Text Domain: my-plugin
 * Domain Path: /languages
 */
Domain Path 头部字段在插件位于官方 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.'

这里的 $city 是变量,不应成为待翻译文本的一部分。解决方法是用占位符代替变量,并配合 printf 系列函数,尤其是 printf 和 sprintf。正确写法如下:

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

注意,待翻译的字符串只有模板 "Your city is %s.",在源代码和运行时都保持一致。

同时,还为译者提供了提示,说明占位符的上下文。

如果字符串中有多个占位符,建议使用参数交换。这时必须用单引号包围字符串,因为双引号会让 PHP 把 $s 解释成变量 s,而这并不是我们想要的行为。 <span class=”source-reference”>(argument swapping)span>

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
);

这里邮政编码显示在城市名之后。有些语言更适合反过来排列城市名和邮政编码。示例中使用带位置编号的 %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 找不到相应翻译,只能原样返回参数 "Your city is London.",最终无法正确翻译。

复数形式

基本复数处理

如果字符串会随条目数量改变,就需要在翻译中反映这种变化。例如英语有 "One comment" 和 "Two comments";其他语言可能有多种复数形式。WordPress 中应使用 _n() 处理。

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

_n() 接收四个参数:

  • singular:字符串的单数形式。注意,有些语言也会对不等于一的数字使用这种形式,因此应使用 '%s item',而不是 'One item'。
  • plural:字符串的复数形式。
  • count:对象数量,用来决定返回单数还是复数形式;有些语言的形式远不止两种。
  • text domain:插件的文本域。

函数返回与给定数量相对应的正确翻译形式。

注意,有些语言会对其他数字使用单数形式,例如 21、31 等,类似英语中的 21st、31st。如果需要对真正的单数特殊处理,应明确检查:

if ( 1 === $count ) {
	printf( esc_html__( 'Last thing!', 'my-text-domain' ), $count );
} else {
	printf( esc_html( _n( '%d thing.', '%d things.', $count, 'my-text-domain' ) ), $count );
}

另请注意,$count 参数经常使用两次:先传给 _n(),确定使用哪个翻译字符串;再传给 printf(),把数字代入翻译后的字符串。

延后处理复数

首先使用 _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 your comment",也可以是名词,如 "Edit this post"。这种情况应使用 _x() 或 _ex()。它们类似 __() 和 _e(),但多接收一个上下文参数:

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

这种方法使两个位置在原始版本中仍显示同一个字符串,但译者会看到两个带不同上下文的条目,从而分别翻译。原文此处将字符串写作 Comment,而紧邻示例实际使用 Post。

与 __() 类似,_x() 也有直接输出的版本 _ex()。前面的示例可以写成:

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

选择你认为更容易阅读和编写的形式即可。

说明注释

为了让译者知道如何翻译 __( 'g:i:s a' ) 这样的字符串,可以在源代码中添加解释性注释。注释必须以 translators: 开头,并且是 gettext 调用之前最后一条 PHP 注释。示例如下:

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

这也可以用来解释 _n_noop( '<strong>Version %1$s</strong> addressed %2$s bug.','<strong>Version %1$s</strong> addressed %2$s bugs.' ) 这类字符串中的占位符。

/* 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>strong> addressed %2$s bugs.' );

换行字符

Gettext 不适合处理可翻译字符串中的回车字符 \r(ASCII 码 13),应避免使用它,改用换行字符 \n。原页面将这两个转义写作 r 和 n。

空字符串

空字符串保留给 Gettext 内部使用,不应尝试将它国际化。这也没有实际意义,因为译者看不到任何上下文。

如果确实有合理场景需要国际化空字符串,请添加上下文,既帮助译者,也避免与 Gettext 的内部用法冲突。

字符串转义

最好对所有字符串进行转义,防止翻译内容执行恶意代码。有些函数已经把转义与国际化整合在一起。

本地化函数

基本函数

翻译并转义的函数

需要翻译且用于 HTML 标签属性的字符串,必须进行转义。

日期与数字函数

编写字符串的最佳实践

编写字符串时,请遵循以下最佳实践:

  • 使用规范的英语,尽量少用俚语和缩写。
  • 使用完整句子:大多数语言的语序与英语不同。
  • 按段落拆分:将有关联的句子放在一起,但不要把整页文字塞进一个字符串。
  • 可翻译短语的开头和末尾不要留下空白。
  • 预留足够空间,假设翻译后的字符串可能增长到原长度的两倍。
  • 避免不常见的标记和控制字符,也不要把包围文本的标签包含进待译内容。
  • 不要在待翻译字符串中加入不必要的 HTML 标记。
  • 不要让译者翻译 URL,除非 URL 确实存在其他语言版本。
  • 把变量作为占位符加入字符串,因为占位符在某些语言中可能需要改变位置。
printf(
	__( 'Search results for: %s', 'my-plugin' ),
	get_search_query()
);
  • 使用格式字符串,而不是拼接字符串;翻译完整短语,而非孤立单词。printf( __( 'Your city is %1$s, and your zip code is %2$s.', 'my-plugin' ), $city, $zipcode ); 总是优于 __( ‘Your city is ‘, ‘my-plugin’ ) . $city . __( ‘, and your zip code is ‘, ‘my-plugin’ ) . $zipcode;。
  • 尽量使用一致的单词和符号,避免产生多个需要单独翻译的字符串,例如 __( 'Posts:', 'my-plugin' ); 与 __( 'Posts', 'my-plugin' );。

为字符串添加文本域

每次调用 __()、_e() 和 _n() 等 gettext 函数时,都必须把文本域作为参数传入,否则翻译不能正常工作。原文此处写作 __n(),后面的示例与函数清单使用的是 _n()。

示例:

  • __( 'Post' ) 应改为 __( 'Post', 'my-theme' )。
  • _e( 'Post' ) 应改为 _e( 'Post', 'my-theme' )。
  • _n( '%s post', '%s posts', $count ) 应改为 _n( '%s post', '%s posts', $count, 'my-theme' )。

即使插件中的某些字符串也被 WordPress 核心使用,例如 Settings,仍应添加插件自己的文本域。否则,一旦核心字符串改变,这些字符串就可能失去翻译,而核心字符串确实可能发生变化。

如果没有在编写代码时持续添加文本域,事后手工补齐会很繁琐,因此可以自动处理:

  • 下载 add-textdomain.php 脚本,将它放到需要添加文本域的文件所在文件夹。
  • 在命令行切换到该文件所在目录。
  • 运行以下命令,生成已添加文本域的新文件:
php add-textdomain.php my-plugin my-plugin.php > new-my-plugin.php

如果希望将 add-textdomain.php 放在另一个文件夹,只需在命令中明确脚本位置。

php /path/to/add-textdomain.php my-plugin my-plugin.php > 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 调用末尾都会加上文本域。已经存在的文本域不会被替换。

加载文本域

可以使用 load_plugin_textdomain 加载翻译,例如:

add_action( 'init', 'wpdocs_load_textdomain' );

function wpdocs_load_textdomain() {
	load_plugin_textdomain( 'wpdocs_textdomain', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); 
}

托管在 WordPress.org 的插件

从 WordPress 4.6 开始,翻译优先使用 translate.wordpress.org 提供的版本,因此通过 translate.wordpress.org 翻译的插件不一定还需要 load_plugin_textdomain()。如果不想在插件中添加 load_plugin_textdomain() 调用,就必须将 readme.txt 中的 Requires at least: 字段设为 4.6 或更高版本。

如果仍希望加载自己的翻译,而非翻译平台提供的版本,需要使用名为 load_textdomain_mofile 的过滤器钩子。下面的示例将 .mo 文件放在插件的 /languages/ 目录,代码则插入插件主文件:

function my_plugin_load_my_own_textdomain( $mofile, $domain ) {
	if ( 'my-domain' === $domain && false !== strpos( $mofile, WP_LANG_DIR . '/plugins/' ) ) {
		$locale = apply_filters( 'plugin_locale', determine_locale(), $domain );
		$mofile = WP_PLUGIN_DIR . '/' . dirname( plugin_basename( __FILE__ ) ) . '/languages/' . $domain . '-' . $locale . '.mo';
	}
	return $mofile;
}
add_filter( 'load_textdomain_mofile', 'my_plugin_load_my_own_textdomain', 10, 2 );

处理 JavaScript 文件

请参阅 Common APIs Handbook 的 JavaScript 国际化章节,了解如何正确加载翻译文件;也可以参考 Gutenberg 插件文档页面。 (Internationalizing javascript) (Gutenburg plugin docs page)

语言包

如果想了解语言包,以及翻译如何导入 translate.wordpress.org,请阅读 Meta Handbook 中关于翻译的页面。 (Meta Handbook page about Translations)

另可参阅 Polyglots 手册中的插件与主题作者指南,了解如何为项目组织翻译。 (Plugin/Theme Authors Guide in Polyglots Handbooks)

原文:How to Internationalize Your Plugin。WordPress 插件手册贡献团队(页面无个人署名)。中文翻译与排版改编。原文及示例版权归原权利人;依转载授权保留来源及原有权利声明。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容