分析 WordPress `load_plugin_textdomain()` 函数源码:如何加载插件的国际化文件并支持多语言。

WordPress 插件国际化:load_plugin_textdomain() 函数源码剖析(讲座模式)

各位听众,欢迎来到今天的“WordPress 插件国际化:load_plugin_textdomain() 函数源码剖析”讲座。我是今天的讲师,代号“码农李”,很高兴能和大家一起探索 WordPress 插件国际化的奥秘。今天,我们重点聚焦 load_plugin_textdomain() 这个神奇的函数,看看它如何让你的插件说“世界语”,支持各种语言,走向国际。

在开始之前,先给大家讲个笑话:一个程序员去面试,面试官问:“你擅长什么?”程序员自信地说:“我擅长复制粘贴!”面试官:“那好,请你把简历复制一遍。” 程序员:“……” 这告诉我们,光会复制粘贴是不行的,要理解原理,才能真正掌握技术。今天,我们就一起深入 load_plugin_textdomain() 的源码,看看它背后的原理。

一、什么是国际化 (i18n) 和本地化 (l10n)?

在深入 load_plugin_textdomain() 之前,我们需要先搞清楚两个概念:国际化 (i18n) 和本地化 (l10n)。

  • 国际化 (i18n, Internationalization): 指的是设计和开发产品,使其能够适应不同的语言、区域和文化的技术。简单来说,就是让你的代码具有“国际范儿”,为支持多种语言做好准备。
  • 本地化 (l10n, Localization): 指的是将产品改编成特定语言、区域或文化的过程。比如,将英文版的 WordPress 插件翻译成中文,就是本地化的过程。

WordPress 已经为我们提供了强大的国际化和本地化支持,而 load_plugin_textdomain() 就是其中的关键一环。

二、load_plugin_textdomain() 函数:插件国际化的核心

load_plugin_textdomain() 函数是 WordPress 中用于加载插件的文本域(text domain)的函数。文本域就像一个命名空间,用于区分不同插件的翻译文件,避免冲突。

函数签名:

<?php
function load_plugin_textdomain(
    string $domain,
    bool $deprecated = false,
    string $plugin_rel_path = null
): bool;

参数说明:

参数 类型 描述
$domain string 插件的文本域。这是一个唯一的标识符,用于区分不同插件的翻译文件。建议使用插件的 slug 作为文本域。
$deprecated bool (已弃用) 始终应为 false。用于向后兼容。
$plugin_rel_path string 插件的相对路径。通常是翻译文件存放的目录相对于插件主文件的路径。例如,如果你的翻译文件存放在 languages 目录下,那么 $plugin_rel_path 就应该是 languages。如果为 null,则 WordPress 会自动尝试查找默认位置 (插件根目录下的 languages 目录)。

返回值:

  • true: 如果成功加载了翻译文件。
  • false: 如果加载失败。

使用示例:

<?php
/**
 * Plugin Name: My Awesome Plugin
 * Description: An example plugin that demonstrates internationalization.
 */

add_action( 'plugins_loaded', 'my_awesome_plugin_load_textdomain' );

function my_awesome_plugin_load_textdomain() {
    load_plugin_textdomain( 'my-awesome-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
}

这段代码做了什么呢?

  1. add_action( 'plugins_loaded', 'my_awesome_plugin_load_textdomain' );: 这行代码告诉 WordPress,在 plugins_loaded 动作被触发时,执行 my_awesome_plugin_load_textdomain 函数。plugins_loaded 是一个钩子,在所有插件都加载完毕后触发,是加载翻译文件的最佳时机。
  2. load_plugin_textdomain( 'my-awesome-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );: 这行代码调用了 load_plugin_textdomain 函数,加载了 my-awesome-plugin 文本域的翻译文件。
    • 'my-awesome-plugin' 是文本域,必须唯一。
    • false 是为了兼容旧版本,现在已经没用了,始终设置为 false 即可。
    • dirname( plugin_basename( __FILE__ ) ) . '/languages/' 是翻译文件存放的目录。plugin_basename( __FILE__ ) 获取当前插件主文件的路径,dirname() 获取该路径的目录名,然后拼接上 /languages/,就得到了翻译文件的完整路径。

三、load_plugin_textdomain() 源码剖析:深入内部

现在,让我们深入 load_plugin_textdomain() 的源码,看看它是如何工作的。

(以下源码基于 WordPress 6.4.2)

<?php
function load_plugin_textdomain( string $domain, bool $deprecated = false, string $plugin_rel_path = null ): bool {
    global $l10n, $wp_filesystem;

    // Deprecated.
    if ( ! is_bool( $deprecated ) ) {
        _deprecated_argument( __FUNCTION__, '2.7' );
    }

    // Initialize the locale.
    $locale = determine_locale();

    $locale = apply_filters( 'plugin_locale', $locale, $domain );

    // Bail if no locale is defined.
    if ( empty( $locale ) ) {
        return false;
    }

    $mofile_global  = WP_LANG_DIR . '/plugins/' . $domain . '-' . $locale . '.mo';
    $mofile_local   = plugin_dir_path( __FILE__ ) . $plugin_rel_path . '/' . $domain . '-' . $locale . '.mo';
    $mofile_default = trailingslashit( WP_PLUGIN_DIR ) . dirname( plugin_basename( __FILE__ ) ) . '/' . $plugin_rel_path . '/' . $domain . '-' . $locale . '.mo';

    if ( file_exists( $mofile_global ) ) {
        return load_textdomain( $domain, $mofile_global );
    }

    if ( file_exists( $mofile_local ) ) {
        return load_textdomain( $domain, $mofile_local );
    }

    if ( file_exists( $mofile_default ) ) {
        return load_textdomain( $domain, $mofile_default );
    }

    return false;
}

我们来逐行分析这段代码:

  1. global $l10n, $wp_filesystem;: 声明全局变量 $l10n$wp_filesystem$l10n 是一个数组,用于存储已加载的文本域和对应的翻译数据。$wp_filesystem 是 WordPress 文件系统抽象层,用于访问文件。
  2. if ( ! is_bool( $deprecated ) ) { _deprecated_argument( __FUNCTION__, '2.7' ); }: 检查 $deprecated 参数是否为布尔值,如果不是,则触发一个弃用警告。这个参数在 WordPress 2.7 之后已经不再使用。
  3. $locale = determine_locale();: 调用 determine_locale() 函数获取当前的语言环境 (locale)。语言环境是一个字符串,用于表示特定的语言和区域,例如 zh_CN (简体中文)。
  4. $locale = apply_filters( 'plugin_locale', $locale, $domain );: 使用 plugin_locale 过滤器修改语言环境。这允许开发者根据自己的需要,自定义插件的语言环境。
  5. if ( empty( $locale ) ) { return false; }: 如果语言环境为空,则直接返回 false,表示加载失败。
  6. $mofile_global = WP_LANG_DIR . '/plugins/' . $domain . '-' . $locale . '.mo';
    $mofile_local = plugin_dir_path( __FILE__ ) . $plugin_rel_path . '/' . $domain . '-' . $locale . '.mo';
    $mofile_default = trailingslashit( WP_PLUGIN_DIR ) . dirname( plugin_basename( __FILE__ ) ) . '/' . $plugin_rel_path . '/' . $domain . '-' . $locale . '.mo';
    这三行代码分别构建了三个可能的 .mo 文件的路径。.mo 文件是 Machine Object 的缩写,是编译后的二进制翻译文件,包含了翻译后的字符串。

    • $mofile_global: 全局 .mo 文件路径,位于 WP_LANG_DIR/plugins/ 目录下。WP_LANG_DIR 是 WordPress 语言文件的目录,通常是 wp-content/languages
    • $mofile_local: 本地 .mo 文件路径,位于插件目录下的 $plugin_rel_path 目录下。
    • $mofile_default: 默认 .mo 文件路径,也位于插件目录下的 $plugin_rel_path 目录下。
  7. if ( file_exists( $mofile_global ) ) { return load_textdomain( $domain, $mofile_global ); }
    if ( file_exists( $mofile_local ) ) { return load_textdomain( $domain, $mofile_local ); }
    if ( file_exists( $mofile_default ) ) { return load_textdomain( $domain, $mofile_default ); }
    这三行代码依次检查上述三个 .mo 文件是否存在。如果存在,则调用 load_textdomain() 函数加载该文件,并返回 true
  8. return false;: 如果所有 .mo 文件都不存在,则返回 false,表示加载失败。

总结:load_plugin_textdomain() 的工作流程

load_plugin_textdomain() 函数的工作流程可以总结为以下几个步骤:

  1. 获取当前的语言环境 (locale)。
  2. 构建可能的 .mo 文件路径 (全局、本地、默认)。
  3. 依次检查这些 .mo 文件是否存在。
  4. 如果存在,则调用 load_textdomain() 函数加载该文件。
  5. 如果所有 .mo 文件都不存在,则返回 false

四、load_textdomain() 函数:加载翻译文件的核心

load_plugin_textdomain() 函数实际上是调用 load_textdomain() 函数来加载翻译文件的。load_textdomain() 函数才是真正加载翻译数据的核心。

函数签名:

<?php
function load_textdomain( string $domain, string $mofile ): bool;

参数说明:

参数 类型 描述
$domain string 文本域。
$mofile string .mo 文件的完整路径。

返回值:

  • true: 如果成功加载了翻译文件。
  • false: 如果加载失败。

load_textdomain() 的源码比较复杂,这里我们只分析关键部分:

<?php
function load_textdomain( string $domain, string $mofile ): bool {
    global $l10n, $wp_filesystem;

    /**
     * Filters whether to load the text domain.
     *
     * @since 3.0.0
     *
     * @param bool   $load   Whether to load the text domain. Default true.
     * @param string $domain Text domain. Unique identifier for retrieving translated strings.
     * @param string $mofile Path to the MO file.
     */
    $load = apply_filters( 'load_textdomain', true, $domain, $mofile );
    if ( ! $load ) {
        return false;
    }

    if ( ! is_readable( $mofile ) ) {
        return false;
    }

    $l10n[ $domain ] = new MO();
    $loaded           = $l10n[ $domain ]->import_from_file( $mofile );

    if ( ! $loaded ) {
        unset( $l10n[ $domain ] );
        return false;
    }

    $l10n[ $domain ]->domain = $domain;

    /**
     * Fires after the text domain is loaded.
     *
     * @since 2.9.0
     *
     * @param string $domain Text domain. Unique identifier for retrieving translated strings.
     * @param string $mofile Path to the MO file.
     */
    do_action( 'textdomain_loaded', $domain, $mofile );

    return true;
}
  1. $load = apply_filters( 'load_textdomain', true, $domain, $mofile );: 使用 load_textdomain 过滤器,允许开发者控制是否加载指定的文本域。
  2. if ( ! is_readable( $mofile ) ) { return false; }: 检查 .mo 文件是否可读。如果不可读,则返回 false
  3. $l10n[ $domain ] = new MO();: 创建一个 MO 类的实例。MO 类是 WordPress 中用于处理 .mo 文件的类。
  4. $loaded = $l10n[ $domain ]->import_from_file( $mofile );: 调用 MO 类的 import_from_file() 方法,从 .mo 文件中导入翻译数据。
  5. if ( ! $loaded ) { unset( $l10n[ $domain ] ); return false; }: 如果导入失败,则从 $l10n 数组中移除该文本域,并返回 false
  6. $l10n[ $domain ]->domain = $domain;: 设置 MO 实例的 domain 属性。
  7. do_action( 'textdomain_loaded', $domain, $mofile );: 触发 textdomain_loaded 动作,允许开发者在文本域加载完成后执行一些操作。
  8. return true;: 返回 true,表示加载成功。

五、如何使用翻译函数?

加载了翻译文件之后,我们就可以使用 WordPress 提供的翻译函数来显示翻译后的字符串了。常用的翻译函数有:

  • __(): 返回翻译后的字符串。
  • _e(): 输出翻译后的字符串。
  • _x(): 返回带上下文的翻译后的字符串。
  • _ex(): 输出带上下文的翻译后的字符串。
  • _n(): 返回单复数形式的翻译后的字符串。
  • _nx(): 返回带上下文的单复数形式的翻译后的字符串。

示例:

<?php
// 返回翻译后的字符串
$translated_string = __( 'Hello, world!', 'my-awesome-plugin' );
echo $translated_string;

// 输出翻译后的字符串
_e( 'Hello, world!', 'my-awesome-plugin' );

// 返回带上下文的翻译后的字符串
$translated_string = _x( 'Post', 'noun', 'my-awesome-plugin' );
echo $translated_string;

// 返回单复数形式的翻译后的字符串
$translated_string = _n( '%s comment', '%s comments', $comment_count, 'my-awesome-plugin' );
echo $translated_string;

注意:

  • 所有翻译函数都需要指定文本域,即你在 load_plugin_textdomain() 函数中使用的 $domain 参数。
  • 翻译函数中的第一个参数是要翻译的原始字符串。这个字符串应该尽量使用英文,并且保持简洁明了。

六、创建翻译文件:.po.mo

要进行本地化,我们需要创建翻译文件。WordPress 使用两种类型的翻译文件:

  • .po (Portable Object): 是人类可读的文本文件,包含了原始字符串和翻译后的字符串。
  • .mo (Machine Object): 是编译后的二进制文件,用于 WordPress 加载翻译数据。

创建 .po 文件:

  1. 使用 Poedit 或其他类似的翻译工具创建一个新的 .po 文件。
  2. .po 文件中,你需要添加所有需要翻译的字符串。
  3. 对于每个字符串,你需要提供翻译后的字符串。

.po 文件示例:

msgid "Hello, world!"
msgstr "你好,世界!"

msgid "Welcome to my awesome plugin!"
msgstr "欢迎使用我的超棒插件!"

.po 文件编译成 .mo 文件:

  1. 使用 Poedit 或其他类似的翻译工具打开 .po 文件。
  2. 保存 .po 文件。Poedit 会自动创建一个与 .po 文件同名的 .mo 文件。

七、最佳实践

  • 使用唯一的文本域: 确保你的插件使用唯一的文本域,避免与其他插件冲突。建议使用插件的 slug 作为文本域。
  • 将翻译文件放在 languages 目录下: 建议将翻译文件放在插件根目录下的 languages 目录下,这是一个标准的做法。
  • 使用合适的翻译函数: 根据需要选择合适的翻译函数,例如 __()_e()_x()_ex()_n()_nx()
  • 保持原始字符串的简洁明了: 原始字符串应该尽量使用英文,并且保持简洁明了,方便翻译人员理解。
  • 定期更新翻译文件: 随着插件的更新,可能会添加新的字符串或修改现有的字符串。因此,需要定期更新翻译文件,确保翻译的准确性。
  • 利用在线翻译平台: 可以利用一些在线翻译平台,例如 Transifex 或 GlotPress,来管理和协作翻译工作。

八、总结

load_plugin_textdomain() 函数是 WordPress 插件国际化的核心,它负责加载插件的翻译文件,让你的插件能够支持多种语言。通过深入了解 load_plugin_textdomain() 函数的源码,我们可以更好地理解 WordPress 的国际化机制,为我们的插件提供更好的本地化支持。

希望今天的讲座对大家有所帮助。记住,让你的插件说“世界语”,走向国际,从 load_plugin_textdomain() 开始!

感谢大家的聆听!

发表回复

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