剖析 WordPress `_n()` 函数的源码:如何处理单复数形式的翻译。

咳咳,各位同学,欢迎来到今天的“WordPress 源码解剖系列”讲座。今天我们要拆解的是 WordPress 中一个非常实用,但也容易被忽略的函数:_n()。 别看它名字短小精悍,它可是负责处理单复数形式翻译的大功臣。准备好,我们这就深入它的“骨髓”一探究竟!

一、 什么是 _n()? 为什么要用它?

在很多语言中,名词的单复数形式会影响到句子结构,尤其是在涉及到数量的时候。 比如:

  • 英文: "1 comment", "2 comments"
  • 法文: "1 commentaire", "2 commentaires"
  • 俄文: "1 комментарий", "2 комментария", "5 комментариев" (俄语更复杂,还有变格)

直接硬编码这些变化既不优雅,也不方便维护。 WordPress 为了解决这个问题,引入了 _n() 函数。 它的作用就是:根据给定的数字,选择合适的单数或复数形式的翻译文本。

_n() 函数的基本语法如下:

_n( $single, $plural, $number, $domain = 'default' );
  • $single: 单数形式的文本。
  • $plural: 复数形式的文本。
  • $number: 用于判断单复数的数字。
  • $domain: 翻译文本所属的文本域(text domain)。 默认是 ‘default’。

举个栗子:

printf( _n( '%s comment', '%s comments', $comment_count ), number_format_i18n( $comment_count ) );

这段代码会根据 $comment_count 的值,输出 "1 comment" 或 "2 comments" 等。 number_format_i18n() 函数是为了格式化数字,让其符合当前语言环境的数字格式。

二、 _n() 函数的源码剖析

好了,废话不多说,让我们直接进入 _n() 的源码。 _n() 函数实际上是对 translate_plural() 函数的一个简单封装。 在 wp-includes/l10n.php 文件中,你可以找到它的定义:

function _n( $single, $plural, $number, $domain = 'default' ) {
    return translate_plural( $single, $plural, $number, $domain );
}

可以看到,_n() 函数只是简单地调用了 translate_plural() 函数。 所以,真正的逻辑都在 translate_plural() 函数中。

三、 translate_plural() 函数的源码解析

translate_plural() 函数才是真正干活的家伙。 它的源码稍微复杂一些,我们一步一步来分析。

function translate_plural( $singular, $plural, $number, $domain = 'default' ) {
    global $l10n;

    $domain = sanitize_key( $domain );

    if ( isset( $l10n[ $domain ] ) ) {
        $translations = $l10n[ $domain ];
    } else {
        $translations = null;
    }

    if ( isset( $translations->headers['Plural-Forms'] ) ) {
        $plural_forms = $translations->headers['Plural-Forms'];
    } else {
        $plural_forms = false;
    }

    if ( ! is_null( $translations ) && false !== $plural_forms ) {
        $index = _n_noop_plural( $singular, $plural, $number, $domain );
        $index = $translations->select_plural_form( $number );
        $translation = $translations->translate_plural( $singular, $plural, $number, $index );

        if ( false !== $translation ) {
            return $translation;
        }
    }

    if ( 1 == $number ) {
        return $singular;
    } else {
        return $plural;
    }
}

我们来逐行解释:

  1. global $l10n;: 声明全局变量 $l10n$l10n 是一个全局数组,存储了所有已加载的翻译数据。 它的键是文本域(text domain),值是一个 Translation_Entry 对象,包含了翻译文本和元数据。

  2. $domain = sanitize_key( $domain );: 对文本域进行安全过滤。 确保文本域的格式是合法的。

  3. if ( isset( $l10n[ $domain ] ) ) { ... }: 检查指定的文本域是否已经加载了翻译数据。 如果已经加载,就从 $l10n 数组中获取 Translation_Entry 对象。

  4. if ( isset( $translations->headers['Plural-Forms'] ) ) { ... }: 检查 Translation_Entry 对象中是否包含了 "Plural-Forms" 头信息。 "Plural-Forms" 头信息定义了如何根据数字选择单复数形式。 这是最关键的部分。

  5. if ( ! is_null( $translations ) && false !== $plural_forms ) { ... }: 如果翻译数据和 "Plural-Forms" 头信息都存在,就执行翻译逻辑。

  6. $index = _n_noop_plural( $singular, $plural, $number, $domain );: 这行代码看起来有点奇怪,但实际上什么也没做。 _n_noop_plural() 函数只是为了让 POT 文件提取工具能够识别出需要翻译的单复数形式。 它返回的值并没有被真正使用。

  7. $index = $translations->select_plural_form( $number );: 这才是真正选择单复数形式索引的关键代码! 调用 Translation_Entry 对象的 select_plural_form() 方法,根据数字 $number 和 "Plural-Forms" 头信息,计算出应该使用哪个翻译文本。

  8. $translation = $translations->translate_plural( $singular, $plural, $number, $index );: 调用 Translation_Entry 对象的 translate_plural() 方法,根据索引 $index 获取翻译文本。

  9. if ( false !== $translation ) { return $translation; }: 如果找到了翻译文本,就直接返回。

  10. if ( 1 == $number ) { return $singular; } else { return $plural; }: 如果没有找到翻译文本,就根据 $number 的值,返回原始的单数或复数形式。 这是一个默认的 fallback 机制。

四、 "Plural-Forms" 头信息: 单复数形式选择的秘密

"Plural-Forms" 头信息是决定如何选择单复数形式的关键。 它定义了一个规则,用于根据数字计算出一个索引值。 这个索引值对应于翻译文本数组中的一个元素。

"Plural-Forms" 头信息的格式如下:

Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;
  • nplurals: 指定了该语言有多少种复数形式。
  • plural: 定义了一个表达式,用于计算索引值。 表达式可以使用数字 $n 作为变量。

举几个例子:

  • 英文: Plural-Forms: nplurals=2; plural=(n != 1);

    • nplurals=2: 英文有两种复数形式(单数和复数)。
    • plural=(n != 1): 如果 $n 不等于 1,则索引值为 1(复数),否则索引值为 0(单数)。
  • 法文: Plural-Forms: nplurals=2; plural=(n > 1);

    • nplurals=2: 法文有两种复数形式。
    • plural=(n > 1): 如果 $n 大于 1,则索引值为 1(复数),否则索引值为 0(单数)。
  • 俄文: Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);

    • nplurals=3: 俄文有三种复数形式。
    • plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2): 这个表达式比较复杂,但它定义了俄语的复数规则。

五、 Translation_Entry 类的 select_plural_form() 方法

Translation_Entry 类的 select_plural_form() 方法负责解析 "Plural-Forms" 头信息,并根据数字计算出索引值。

这个方法的源码比较复杂,涉及到编译和执行 PHP 代码,这里就不深入讲解了。 简单来说,它会:

  1. 从 "Plural-Forms" 头信息中提取 npluralsplural 表达式。
  2. plural 表达式编译成 PHP 代码。
  3. 使用给定的数字 $number 执行 PHP 代码,得到索引值。

六、 总结: _n() 函数的工作流程

现在,我们来总结一下 _n() 函数的工作流程:

  1. _n() 函数接收单数形式、复数形式、数字和文本域作为参数。
  2. _n() 函数调用 translate_plural() 函数。
  3. translate_plural() 函数从全局变量 $l10n 中获取指定文本域的翻译数据。
  4. translate_plural() 函数检查翻译数据中是否包含 "Plural-Forms" 头信息。
  5. 如果包含 "Plural-Forms" 头信息,translate_plural() 函数调用 Translation_Entry 对象的 select_plural_form() 方法,根据数字和 "Plural-Forms" 头信息计算出索引值。
  6. translate_plural() 函数根据索引值获取翻译文本。
  7. translate_plural() 函数返回翻译文本,或者原始的单数或复数形式。

可以用下面的表格来概括这个流程:

步骤 函数/方法 描述
1 _n() 接收参数,调用 translate_plural()
2 translate_plural() 获取翻译数据,检查 "Plural-Forms"
3 Translation_Entry->select_plural_form() 根据 "Plural-Forms" 和数字计算索引
4 Translation_Entry->translate_plural() 根据索引获取翻译文本
5 translate_plural() 返回翻译文本或默认的单复数形式

七、 如何使用 _n() 函数

明白了 _n() 函数的原理,我们来看看如何正确使用它。

  1. 准备好单数和复数形式的文本。 确保单数和复数形式的文本都正确无误。

  2. 使用 _n() 函数进行翻译。 将单数形式、复数形式和数字传递给 _n() 函数。

  3. 使用 printf() 函数格式化输出。 _n() 函数返回的是翻译文本,你需要使用 printf() 函数将其格式化输出。

例如:

$post_count = get_the_modified_date( 'Ymd' ); //假设你取到的是一个文章数量
printf( _n( 'One post modified today', '%s posts modified today', $post_count ), number_format_i18n( $post_count ) );

八、 总结与思考

_n() 函数是 WordPress 国际化和本地化 (i18n/l10n) 的一个重要组成部分。 它巧妙地利用 "Plural-Forms" 头信息,实现了对各种语言的单复数形式的灵活处理。

通过深入分析 _n() 函数的源码,我们不仅了解了它的工作原理,也学习了 WordPress 如何处理翻译数据和国际化逻辑。 这对于我们更好地理解 WordPress 源码,以及开发高质量的 WordPress 插件和主题都非常有帮助。

当然,_n() 函数只是 WordPress i18n/l10n 系统中的冰山一角。 还有很多其他的函数和机制,比如 __(), _e(), _x(), load_textdomain() 等,都值得我们深入学习和研究。

好了,今天的讲座就到这里。 希望大家有所收获! 如果有什么问题,欢迎提问。 下课!

发表回复

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