分析 `load_textdomain()` 函数的源码,它是如何加载插件或主题的翻译文件的?

各位程序猿朋友们,早上好!今天咱们来聊聊 WordPress 中一个至关重要的函数——load_textdomain()。这玩意儿就像是 WordPress 语言包的“搬运工”,负责把那些写满了各种语言的 .mo 文件加载到你的 WordPress 世界里,让你的插件和主题瞬间“口吐莲花”,能说会道各种语言。

准备好了吗?咱们这就开始扒它的“底裤”,看看它到底是怎么工作的。

一、load_textdomain() 是个啥?

首先,咱们来明确一下 load_textdomain() 的作用。简单来说,它负责:

  1. 找到翻译文件: 根据你提供的文本域(text domain)和语言环境,去预定的位置寻找对应的 .mo 文件。
  2. 加载翻译文件: 把找到的 .mo 文件加载到 WordPress 的全局翻译对象 $l10n 中。
  3. 绑定翻译: 将翻译文件和指定的文本域绑定起来,这样 WordPress 就能知道哪个文本域应该使用哪个翻译。

二、函数签名和参数

load_textdomain() 函数的签名如下:

/**
 * Loads a MO file into the text domain.
 *
 * If the text domain already exists, the translations will be merged. If the
 * MO file doesn't exist a warning will be produced.
 *
 * @since 1.5.0
 *
 * @global array $l10n An array of translations.
 *
 * @param string      $domain  Text domain. Unique identifier for retrieving translated strings.
 * @param string|bool $mofile  Path to the MO file. Use false to just remove the domain.
 * @return bool True on success, false on failure.
 */
function load_textdomain( $domain, $mofile ) {
  // 函数的具体实现将在后面详细讨论
}

我们来解读一下这些参数:

  • $domain (string): 文本域,这是个非常重要的参数。它是你的插件或主题的唯一标识符,WordPress 通过它来区分不同的翻译文件。通常,建议你使用插件或主题的名称作为文本域,例如 'my-awesome-plugin''super-cool-theme'
  • $mofile (string|bool): .mo 文件的路径。这是一个指向翻译文件的绝对或相对路径。如果设置为 false,则会从 $l10n 数组中移除指定的文本域。

三、load_textdomain() 的内部运作机制

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

function load_textdomain( $domain, $mofile ) {
    global $l10n, $wp_filesystem;

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

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

    // If no MO file is specified, then remove the domain.
    if ( false === $mofile ) {
        unset( $l10n[ $domain ] );

        /**
         * Fires after the text domain is unloaded.
         *
         * @since 2.9.0
         *
         * @param string $domain Text domain. Unique identifier for retrieving translated strings.
         */
        do_action( 'unload_textdomain', $domain );

        return true;
    }

    // Shortcuts.
    $l10n_temp = &$l10n;
    $mo = new MO();

    // Load the .mo file, merging existing entries.
    if ( $mo->import_from_file( $mofile ) ) {
        if ( isset( $l10n_temp[ $domain ] ) ) {
            $mo->merge_with( $l10n_temp[ $domain ] );
        }
        $l10n_temp[ $domain ] = &$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;
    }

    return false;
}

让我们一步步地分析这段代码:

  1. 全局变量: 首先,它声明了两个全局变量:$l10n$wp_filesystem$l10n 是一个数组,用于存储所有已加载的翻译数据。$wp_filesystem 是 WordPress 文件系统 API 的实例,用于访问文件系统。虽然这里用到了$wp_filesystem,但是在这个函数中并没有直接使用它,这是历史遗留问题,早期版本可能需要通过$wp_filesystem来读取文件。
  2. load_textdomain 钩子: 接下来,它触发了一个名为 load_textdomain 的动作钩子。这允许其他插件或主题在 .mo 文件加载之前执行一些操作,例如验证文件路径或修改文本域。
    do_action( 'load_textdomain', $domain, $mofile );
  3. 文本域清理: 然后,它使用 sanitize_key() 函数对文本域进行清理,以确保它是一个有效的键名。这有助于防止潜在的安全问题。
    $domain = sanitize_key( $domain );
  4. 移除文本域: 如果 $mofile 参数为 false,则表示要移除指定的文本域。它会从 $l10n 数组中移除对应的条目,并触发 unload_textdomain 动作钩子。
    if ( false === $mofile ) {
      unset( $l10n[ $domain ] );
      do_action( 'unload_textdomain', $domain );
      return true;
    }
  5. 加载 .mo 文件: 这是核心部分。它创建了一个 MO 类的实例,并使用 import_from_file() 方法加载 .mo 文件。MO 类是 WordPress 中用于处理 .mo 文件的类。
    $mo = new MO();
    if ( $mo->import_from_file( $mofile ) ) {
      // ...
    }
  6. 合并翻译: 如果指定的文本域已经存在于 $l10n 数组中,则会将新的翻译数据与现有的翻译数据合并。这允许你覆盖或扩展现有的翻译。
    if ( isset( $l10n_temp[ $domain ] ) ) {
      $mo->merge_with( $l10n_temp[ $domain ] );
    }
  7. 存储翻译数据: 最后,它将 $mo 对象存储到 $l10n 数组中,并触发 textdomain_loaded 动作钩子。
    $l10n_temp[ $domain ] = &$mo;
    do_action( 'textdomain_loaded', $domain, $mofile );
  8. 返回值: 如果成功加载 .mo 文件,则返回 true;否则,返回 false

四、MO 类:.mo 文件的秘密

MO 类是 WordPress 用来处理 .mo 文件的核心类。.mo 文件是一种二进制文件,它存储了翻译后的字符串。MO 类负责读取 .mo 文件,并将其中的翻译数据存储在内存中。

MO 类的一些重要方法包括:

  • import_from_file( $filename ):从指定的文件加载 .mo 数据。
  • merge_with( $other ):将另一个 MO 对象中的数据合并到当前对象中。
  • translate( $singular, $context ):根据指定的单数形式和上下文查找翻译后的字符串。

由于 MO 类的实现比较复杂,涉及到二进制文件的读取和解析,这里就不深入讲解了。但是,了解 MO 类的作用对于理解 load_textdomain() 函数的工作原理至关重要。

五、如何使用 load_textdomain()

现在,我们来看看如何在插件或主题中使用 load_textdomain() 函数。通常,你会在插件或主题的主文件中调用 load_textdomain() 函数。

<?php
/**
 * Plugin Name: My Awesome Plugin
 * Description: This is an awesome plugin.
 */

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. 注册 plugins_loaded 动作: 它使用 add_action() 函数注册了一个名为 plugins_loaded 的动作。这意味着 my_awesome_plugin_load_textdomain() 函数将在所有插件加载完毕后执行。
  2. 定义 my_awesome_plugin_load_textdomain() 函数: 这个函数负责调用 load_plugin_textdomain() 函数来加载翻译文件。
  3. 调用 load_plugin_textdomain() 函数: load_plugin_textdomain() 是一个便捷函数,它封装了 load_textdomain() 函数,并提供了一些默认值。它接受三个参数:
    • 'my-awesome-plugin':文本域。
    • false:表示不使用绝对路径。
    • dirname( plugin_basename( __FILE__ ) ) . '/languages/':翻译文件相对于插件主文件的路径。通常,我们会把翻译文件放在插件的 languages 目录下。

六、load_plugin_textdomain()load_theme_textdomain()

WordPress 提供了两个便捷函数来加载插件和主题的翻译文件:

  • load_plugin_textdomain( $domain, $deprecated, $plugin_rel_path ):用于加载插件的翻译文件。
  • load_theme_textdomain( $domain, $template_directory ):用于加载主题的翻译文件。

这两个函数实际上都是对 load_textdomain() 函数的封装,它们会自动计算 .mo 文件的路径。

我们来看一下 load_plugin_textdomain() 的源码:

function load_plugin_textdomain( $domain, $deprecated, $plugin_rel_path ) {
    global $l10n, $wp_version;

    if ( ! empty( $deprecated ) ) {
        _deprecated_argument( __FUNCTION__, '2.7' );
    }

    // Set the path to the translation functions.
    $locale = apply_filters( 'plugin_locale', determine_locale(), $domain );

    $mofile = $plugin_rel_path . '/' . $locale . '.mo';

    return load_textdomain( $domain, WP_PLUGIN_DIR . '/' . $mofile );
}

可以看到,它首先获取当前的语言环境,然后计算 .mo 文件的路径,最后调用 load_textdomain() 函数来加载翻译文件。

load_theme_textdomain() 函数的实现也类似,只是计算 .mo 文件路径的方式略有不同。

七、最佳实践

在使用 load_textdomain() 函数时,有一些最佳实践需要注意:

  • 使用唯一的文本域: 确保你的插件或主题使用一个唯一的文本域,以避免与其他插件或主题冲突。
  • 将翻译文件放在 languages 目录下: 建议将翻译文件放在插件或主题的 languages 目录下,这样可以方便管理。
  • 使用 load_plugin_textdomain()load_theme_textdomain() 函数: 尽量使用这两个便捷函数,它们可以简化代码并提供一些默认值。
  • plugins_loadedafter_setup_theme 动作中调用: 确保在插件加载完毕或主题设置完成后调用 load_textdomain() 函数,这样可以确保 WordPress 已经初始化完毕。
  • 正确使用翻译函数: 使用 __(), _e(), _x(), _ex(), _n(), _nx() 等翻译函数来包裹需要翻译的字符串。

八、一个完整的例子

让我们来看一个完整的例子,演示如何在插件中使用 load_plugin_textdomain() 函数。

<?php
/**
 * Plugin Name: My Super Plugin
 * Description: A super cool plugin with translations!
 */

add_action( 'plugins_loaded', 'my_super_plugin_load_textdomain' );

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

function my_super_plugin_output() {
  echo '<p>' . __( 'Hello, world!', 'my-super-plugin' ) . '</p>';
  echo '<p>' . sprintf( __( 'You have %d item(s) in your cart.', 'my-super-plugin' ), 5 ) . '</p>';
}

add_action( 'wp_footer', 'my_super_plugin_output' );

在这个例子中,我们首先加载了翻译文件,然后在 my_super_plugin_output() 函数中使用了 __()sprintf() 函数来输出翻译后的字符串。

为了使这个例子正常工作,你需要创建一个名为 my-super-plugin-zh_CN.mo 的文件,并将其放在插件的 languages 目录下。这个文件应该包含以下翻译:

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

msgid "You have %d item(s) in your cart."
msgstr "你的购物车里有 %d 件商品。"

九、总结

load_textdomain() 函数是 WordPress 国际化和本地化的基石。通过理解它的工作原理,你可以轻松地为你的插件和主题添加多语言支持,让你的作品走向世界!

希望今天的讲座对你有所帮助。记住,熟能生巧,多实践才能真正掌握这些知识。下次再见!

发表回复

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