深入理解 WordPress `load_plugin_textdomain()` 函数的源码:如何加载插件的翻译文件。

各位代码爱好者,大家好!今天咱们来聊聊 WordPress 插件国际化中一个非常关键的函数:load_plugin_textdomain()。这货听起来很高大上,但其实就是负责加载你插件的翻译文件,让你的插件能说各国语言,变得更国际范儿。

咱们今天就深入它的源码,看看它到底是怎么运作的,以及在使用过程中需要注意哪些坑。准备好了吗? Let’s dive in!

1. 什么是 Text Domain? 为什么需要它?

在深入 load_plugin_textdomain() 之前,先得搞清楚什么是 "Text Domain"。 简单来说,Text Domain 就是你插件的身份标识,一个唯一的字符串,用来区分不同插件的翻译文件。 想象一下,如果没有 Text Domain,所有插件的翻译文件都叫 default.mo,那还不乱套了? WordPress 就不知道该用哪个翻译文件来显示你的插件文本了。

Text Domain 就像是你的插件的身份证号码,确保翻译文件能正确地对应到你的插件。

为什么需要 Text Domain?

  • 避免冲突: 不同的插件可以使用相同的字符串,但他们的翻译可能不同。Text Domain 确保翻译能正确对应插件。
  • 组织翻译: Text Domain 将插件的翻译文件组织在一起,方便管理和维护。
  • WordPress 识别: WordPress 使用 Text Domain 来识别和加载正确的翻译文件。

2. load_plugin_textdomain() 函数的庐山真面目

load_plugin_textdomain() 函数的原型如下:

/**
 * Loads a plugin's translated strings.
 *
 * @since 2.7.0
 *
 * @param string      $domain          Unique identifier for retrieving translated strings.
 * @param string      $deprecated      Deprecated since 2.7.0. Use the plugin_rel_path parameter instead.
 * @param string|false $plugin_rel_path Path to the plugin folder, relative to the plugins folder.
 *                                     Default false.
 * @return bool True when textdomain is successfully loaded, false otherwise.
 */
function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path = false ) {
    global $l10n, $wp_version;

    // First, check if the function has been called without the optional arguments.  If so, we may need to process
    // the arguments.
    if ( false !== $deprecated ) {
        _deprecated_argument( __FUNCTION__, '2.7', sprintf(
            /* translators: 1: Function name, 2: Argument name. */
            __( 'The %1$s function's %2$s argument is deprecated.' ),
            '<code>load_plugin_textdomain()</code>',
            '<code>$path</code>'
        ) );
    }

    // If no plugin_rel_path path is given, define it.
    if ( false === $plugin_rel_path ) {
        $plugin_rel_path = dirname( plugin_basename( __FILE__ ) );
    }

    // Sanitize the domain.
    $domain = sanitize_key( $domain );

    // Load languages for backwards compatibility.
    if ( isset( $l10n[ $domain ] ) && ( ! is_a( $l10n[ $domain ], 'NOOP_Translations' ) ) ) {
        return true;
    }

    // Determine plugin locale location.
    $locale = apply_filters( 'plugin_locale', get_locale(), $domain );
    $mofile = $domain . '-' . $locale . '.mo';

    $plugin_path = trailingslashit( WP_PLUGIN_DIR ) . trailingslashit( dirname( $plugin_rel_path ) );
    $mofile_global = WP_LANG_DIR . '/plugins/' . $mofile;
    $mofile_local  = $plugin_path . $mofile;

    if ( is_readable( $mofile_global ) ) {
        return load_textdomain( $domain, $mofile_global );
    } elseif ( is_readable( $mofile_local ) ) {
        return load_textdomain( $domain, $mofile_local );
    }

    return false;
}

别被这坨代码吓到,其实它做的事情很简单,咱们把它拆解一下:

参数说明:

参数 类型 描述
$domain string 你的 Text Domain,也就是插件的唯一标识。
$deprecated string 已经废弃的参数,不用管它。
$plugin_rel_path string 插件目录相对于 plugins 目录的路径。 例如,如果你的插件目录是 wp-content/plugins/my-awesome-plugin/,那么这个参数就是 my-awesome-plugin/。 如果设置为 false (默认值),函数会自动计算。 推荐设置为 false,让 WordPress 自己算,省事。

函数做了什么:

  1. 参数处理: 检查参数,处理废弃参数,如果 $plugin_rel_pathfalse,则自动计算。
  2. 清理 Text Domain: 使用 sanitize_key() 函数清理 $domain,确保其符合 WordPress 的命名规范。
  3. 检查是否已加载: 检查全局变量 $l10n,看看这个 Text Domain 的翻译文件是否已经加载过了。如果已经加载,就直接返回 true,避免重复加载。
  4. 确定语言区域 (Locale): 使用 apply_filters( 'plugin_locale', get_locale(), $domain ) 获取当前站点的语言区域。get_locale() 获取 WordPress 设置的语言,apply_filters 允许其他插件修改这个语言区域。
  5. 构建翻译文件路径: 根据 Text Domain 和语言区域,构建翻译文件的名称 (.mo 文件)。例如,如果 Text Domain 是 my-awesome-plugin,语言区域是 zh_CN,那么翻译文件的名称就是 my-awesome-plugin-zh_CN.mo
  6. 查找翻译文件: 按照以下顺序查找翻译文件:
    • 全局翻译目录: WP_LANG_DIR . '/plugins/' . $mofileWP_LANG_DIR 通常是 wp-content/languages/。 所以全局路径可能是 wp-content/languages/plugins/my-awesome-plugin-zh_CN.mo
    • 插件目录: $plugin_path . $mofile$plugin_path 是插件的完整路径。 所以插件目录路径可能是 wp-content/plugins/my-awesome-plugin/my-awesome-plugin-zh_CN.mo
  7. 加载翻译文件: 如果找到翻译文件,就使用 load_textdomain() 函数加载它。
  8. 返回结果: 如果成功加载翻译文件,返回 true,否则返回 false

3. 源码剖析 (重点)

咱们来重点看看几个关键部分:

  • 自动计算 $plugin_rel_path:

    if ( false === $plugin_rel_path ) {
        $plugin_rel_path = dirname( plugin_basename( __FILE__ ) );
    }

    这段代码负责在 $plugin_rel_pathfalse 时,自动计算插件目录相对于 plugins 目录的路径。它使用了两个函数:

    • plugin_basename( __FILE__ ): 返回当前文件的插件主文件相对于 plugins 目录的路径。 例如,如果你的插件主文件是 wp-content/plugins/my-awesome-plugin/my-awesome-plugin.php,那么 plugin_basename( __FILE__ ) 会返回 my-awesome-plugin/my-awesome-plugin.php
    • dirname(): 返回路径的目录部分。 例如,dirname( 'my-awesome-plugin/my-awesome-plugin.php' ) 会返回 my-awesome-plugin

    所以,这段代码实际上是获取了插件主文件所在的目录,作为 $plugin_rel_path。 这也是为什么我们通常建议把 load_plugin_textdomain() 放在插件主文件中,这样 WordPress 才能正确地计算路径。

  • 构建翻译文件路径:

    $locale = apply_filters( 'plugin_locale', get_locale(), $domain );
    $mofile = $domain . '-' . $locale . '.mo';
    
    $plugin_path = trailingslashit( WP_PLUGIN_DIR ) . trailingslashit( dirname( $plugin_rel_path ) );
    $mofile_global = WP_LANG_DIR . '/plugins/' . $mofile;
    $mofile_local  = $plugin_path . $mofile;

    这段代码构建了全局和本地两种翻译文件的路径。 注意 trailingslashit() 函数,它可以确保路径以斜杠结尾,避免路径错误。

  • 查找和加载翻译文件:

    if ( is_readable( $mofile_global ) ) {
        return load_textdomain( $domain, $mofile_global );
    } elseif ( is_readable( $mofile_local ) ) {
        return load_textdomain( $domain, $mofile_local );
    }

    这段代码首先检查全局翻译目录是否存在翻译文件,如果存在,就加载它。 否则,检查插件目录是否存在翻译文件,如果存在,就加载它。 注意 is_readable() 函数,它会检查文件是否存在并且可读。

    load_textdomain() 函数是真正加载翻译文件的函数,它会将翻译文件中的字符串加载到全局变量 $l10n 中。

4. 如何使用 load_plugin_textdomain()

说了这么多,咱们来看看怎么在实际代码中使用 load_plugin_textdomain()

<?php
/**
 * Plugin Name: My Awesome Plugin
 * Description: A simple plugin to demonstrate internationalization.
 * Version: 1.0.0
 */

// 在插件激活时加载翻译文件 (推荐)
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' );
}

// 使用翻译函数
function my_awesome_plugin_output_text() {
    echo __( 'Hello, world!', 'my-awesome-plugin' );
}

add_action( 'wp_footer', 'my_awesome_plugin_output_text' );

代码解释:

  1. 插件头部: 定义插件的名称、描述和版本等信息。
  2. plugins_loaded 钩子: 使用 add_action( 'plugins_loaded', 'my_awesome_plugin_load_textdomain' )my_awesome_plugin_load_textdomain() 函数绑定到 plugins_loaded 钩子。 plugins_loaded 钩子在 WordPress 加载所有插件后触发,是加载翻译文件的推荐时机。
  3. my_awesome_plugin_load_textdomain() 函数: 调用 load_plugin_textdomain() 函数加载翻译文件。
    • 'my-awesome-plugin' 是 Text Domain。
    • false 表示使用默认的翻译文件路径。
    • dirname( plugin_basename( __FILE__ ) ) . '/languages' 指定翻译文件存放的目录。 这里假设你的翻译文件放在插件目录下的 languages 目录中。 例如,wp-content/plugins/my-awesome-plugin/languages/my-awesome-plugin-zh_CN.mo
  4. my_awesome_plugin_output_text() 函数: 使用 __() 函数输出翻译后的文本。 __() 函数是 WordPress 提供的翻译函数,它会根据当前的语言区域,查找 Text Domain 对应的翻译,并返回翻译后的文本。
  5. wp_footer 钩子: 使用 add_action( 'wp_footer', 'my_awesome_plugin_output_text' )my_awesome_plugin_output_text() 函数绑定到 wp_footer 钩子,在页面底部输出翻译后的文本。

翻译文件的目录结构:

你的插件目录结构应该类似这样:

my-awesome-plugin/
├── my-awesome-plugin.php      (插件主文件)
├── languages/
│   ├── my-awesome-plugin-zh_CN.mo
│   ├── my-awesome-plugin-zh_CN.po
│   └── ...
└── ...

5. 注意事项 (踩坑指南)

  • Text Domain 必须唯一: 确保你的 Text Domain 在所有插件中是唯一的。 通常建议使用插件的 slug 作为 Text Domain。
  • 正确指定翻译文件路径: 确保 load_plugin_textdomain() 函数的第三个参数指定了正确的翻译文件路径。 如果路径不正确,WordPress 无法找到翻译文件。
  • 使用正确的翻译函数: 使用 __()_e()_x()_ex() 等 WordPress 提供的翻译函数来输出文本。 不要直接输出文本,否则无法进行翻译。
  • 生成 .mo 文件: .mo 文件是二进制的翻译文件,WordPress 才能读取。 你需要使用工具 (例如 Poedit) 将 .po 文件转换为 .mo 文件。
  • 清理缓存: 如果你修改了翻译文件,需要清理 WordPress 的缓存,才能看到更新后的翻译。
  • 加载时机: plugins_loaded 钩子是加载翻译文件的推荐时机。 不要在 init 钩子之前加载翻译文件,否则可能无法获取到正确的语言区域。
  • 更新翻译文件: 如果你的插件更新了,并且添加了新的文本,需要更新翻译文件,并重新生成 .mo 文件。

6. load_textdomain() 函数 (幕后英雄)

load_plugin_textdomain() 函数最终会调用 load_textdomain() 函数来加载翻译文件。 咱们也简单了解一下 load_textdomain()

/**
 * Loads a .mo file into the text domain.
 *
 * If the text domain already exists, the translations will be merged. If both
 * sets of translations have the same string, the translation from the original
 * text domain will be used.
 *
 * @since 1.5.0
 *
 * @param string $domain  Text domain. Unique identifier for retrieving translated strings.
 * @param string $mofile  Path to the MO file.
 * @return bool True on success, false on failure.
 */
function load_textdomain( $domain, $mofile ) {
    global $l10n;

    $domain = sanitize_key( $domain );

    // Has the text domain already been loaded?
    if ( isset( $l10n[ $domain ] ) && ( ! is_a( $l10n[ $domain ], 'NOOP_Translations' ) ) ) {
        return true;
    }

    /**
     * Fires before the MO file 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( 'load_textdomain', $domain, $mofile );

    $mo = new MO();
    if ( ! $mo->import_from_file( $mofile ) ) {
        return false;
    }

    $l10n[ $domain ] = &$mo;

    unset( $mo );

    /**
     * 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;
}

load_textdomain() 函数主要做了以下事情:

  1. 检查是否已加载:load_plugin_textdomain() 类似,先检查是否已经加载过该 Text Domain 的翻译文件。
  2. 触发钩子: 触发 load_textdomain 钩子,允许其他插件在加载翻译文件之前执行一些操作。
  3. 加载 .mo 文件: 使用 MO 类 (一个 WordPress 内部类,用于处理 .mo 文件) 加载 .mo 文件。
  4. 存储翻译数据: 将加载的翻译数据存储到全局变量 $l10n 中。 $l10n 是一个关联数组,以 Text Domain 为键,以 MO 对象为值。
  5. 触发钩子: 触发 textdomain_loaded 钩子,允许其他插件在加载翻译文件之后执行一些操作。

7. 总结

load_plugin_textdomain() 函数是 WordPress 插件国际化的关键函数。 理解它的源码,可以帮助你更好地管理插件的翻译文件,避免踩坑,让你的插件能够更好地服务全球用户。

记住以下几点:

  • Text Domain 是插件的唯一标识。
  • 使用 load_plugin_textdomain() 函数加载翻译文件。
  • 确保翻译文件路径正确。
  • 使用 WordPress 提供的翻译函数。
  • 生成 .mo 文件。
  • plugins_loaded 钩子加载翻译文件。

希望今天的讲座对你有所帮助! 以后开发插件的时候,记得让你的插件也说几句外语,走向世界! 咱们下次再见!

发表回复

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