阐述 WordPress `load_textdomain()` 函数的源码:如何加载插件或主题的翻译文件。

各位观众老爷们,大家好!今天咱们来聊聊WordPress里一个“低调奢华有内涵”的函数:load_textdomain()。别看它名字平平无奇,它可是实现WordPress插件和主题国际化的幕后英雄。咱们今天就扒开它的源码,看看它到底是怎么把翻译文件加载进来的。

(一) 暖场:什么是 Text Domain 和 Locale?

在深入 load_textdomain() 之前,先得搞清楚两个概念:Text Domain 和 Locale。

  • Text Domain (文本域): 这玩意儿就像一个命名空间,用来区分不同插件或主题的翻译字符串。 想象一下,如果所有的插件都用同样的“翻译标签”,那翻译文件岂不乱成一锅粥?Text Domain 就像给每个插件/主题的翻译字符串贴上一个专属的标签,避免冲突。 通常情况下,Text Domain 就是你的插件或主题的名称(建议小写,用短横线分隔单词)。

  • Locale (本地化): 这代表一种特定的语言和地区。比如,en_US 代表美式英语,zh_CN 代表简体中文,fr_FR 代表法国法语。 WordPress 会根据用户的设置选择对应的 Locale,然后加载相应的翻译文件。

好,有了这两个概念,咱们就可以开始深入 load_textdomain() 了。

(二) load_textdomain() 的“庐山真面目”

load_textdomain() 函数的定义位于 wp-includes/l10n.php 文件中。 精简后的函数原型如下(省略了一些错误处理和兼容性代码,方便大家理解):

function load_textdomain( $domain, $mofile, $locale = null ) {
    global $l10n, $wp_locale;

    if ( null === $locale ) {
        $locale = determine_locale();
    }

    $mofile = validate_file( $mofile );

    if ( ! $mofile ) {
        return false;
    }

    // If the locale exists, load it.
    if ( isset( $l10n[ $domain ] ) && isset( $l10n[ $domain ]->headers['Plural-Forms'] ) ) {
        unload_textdomain( $domain );
    }

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

    if ( true === $loaded ) {
        $l10n[ $domain ]->set_domain( $domain );
        return true;
    }

    unset( $l10n[ $domain ] );

    return false;
}

看起来有点复杂,咱们一步步拆解:

  1. 参数:

    • $domain (string): Text Domain,也就是你插件或主题的“翻译标签”。
    • $mofile (string): .mo 文件的完整路径。.mo 文件是编译后的二进制翻译文件,是 .po 文件的“最终形态”。
    • $locale (string, optional): Locale 代码。如果为空,函数会自动检测。
  2. 确定 Locale:

    if ( null === $locale ) {
        $locale = determine_locale();
    }

    如果没提供 Locale,determine_locale() 函数会负责找出当前站点的 Locale 设置。 这个函数会考虑 WordPress 设置、环境变量等因素,最终确定正确的 Locale。

  3. 验证 .mo 文件路径:

    $mofile = validate_file( $mofile );
    
    if ( ! $mofile ) {
        return false;
    }

    validate_file() 函数会检查 $mofile 路径是否安全,防止恶意文件被加载。 如果路径无效或不安全,函数会返回 false,加载失败。

  4. 卸载已存在的 Text Domain (如果存在):

    if ( isset( $l10n[ $domain ] ) && isset( $l10n[ $domain ]->headers['Plural-Forms'] ) ) {
        unload_textdomain( $domain );
    }

    如果这个 Text Domain 已经被加载过了,unload_textdomain() 函数会先把它卸载掉,避免冲突。Plural-Forms.mo 文件头中的一个关键信息,用于处理不同语言的复数形式。

  5. 加载 .mo 文件:

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

    这部分是核心。

    • $l10n[ $domain ] = new MO(); 创建一个 MO 类的实例。 MO 类负责解析 .mo 文件。 $l10n 是一个全局数组,用于存储所有已加载的 Text Domain 及其对应的 MO 对象。
    • $l10n[ $domain ]->import_from_file( $mofile ); 调用 MO 对象的 import_from_file() 方法,从 $mofile 指定的路径加载 .mo 文件。 import_from_file() 方法会读取 .mo 文件的内容,并将其解析成内部数据结构,方便后续的翻译查找。
  6. 设置 Text Domain:

    if ( true === $loaded ) {
        $l10n[ $domain ]->set_domain( $domain );
        return true;
    }

    如果 .mo 文件加载成功,set_domain() 方法会设置 MO 对象的 Text Domain。 然后函数返回 true,表示加载成功。

  7. 加载失败处理:

    unset( $l10n[ $domain ] );
    
    return false;

    如果 .mo 文件加载失败,$l10n 数组中对应的 Text Domain 会被移除,然后函数返回 false

(三) MO 类:.mo 文件的“翻译官”

MO 类是 WordPress 用来处理 .mo 文件的核心类。 它的主要职责是:

  • 读取 .mo 文件
  • 解析 .mo 文件,提取翻译字符串
  • 提供查找翻译字符串的功能

MO 类的源码位于 wp-includes/pomo/mo.php 文件中。 咱们来看看 import_from_file() 方法(同样是精简版):

function import_from_file( $filename ) {
    $f  = fopen( $filename, 'rb' );
    if ( ! $f ) {
        return false;
    }

    $this->entries = array();
    $this->headers = array();

    // Read magic number
    $magic = fread( $f, 4 );
    if ( "xdex12x04x95" != $magic && "x95x04x12xde" != $magic ) {
        return false;
    }

    // Read version
    $version = $this->readint( $f );

    // Read number of strings
    $count = $this->readint( $f );

    // Read originals index
    $originals_offset = $this->readint( $f );

    // Read translations index
    $translations_offset = $this->readint( $f );

    // Read size and offset for original strings
    fseek( $f, $originals_offset );
    $original_lengths = array();
    $original_offsets = array();
    for ( $i = 0; $i < $count; ++$i ) {
        $original_lengths[$i]  = $this->readint( $f );
        $original_offsets[$i]  = $this->readint( $f );
    }

    // Read size and offset for translation strings
    fseek( $f, $translations_offset );
    $translation_lengths = array();
    $translation_offsets = array();
    for ( $i = 0; $i < $count; ++$i ) {
        $translation_lengths[$i] = $this->readint( $f );
        $translation_offsets[$i] = $this->readint( $f );
    }

    // Read original strings
    for ( $i = 0; $i < $count; ++$i ) {
        fseek( $f, $original_offsets[$i] );
        $original = fread( $f, $original_lengths[$i] );

        fseek( $f, $translation_offsets[$i] );
        $translation = fread( $f, $translation_lengths[$i] );

        if ( ! empty( $original ) ) {
            $this->entries[$original] = $translation;
        }
    }

    fclose( $f );

    $this->set_headers( $this->entries[''] );
    unset( $this->entries[''] );

    return true;
}

这个方法的核心逻辑是:

  1. 打开 .mo 文件: 使用 fopen() 函数以二进制读取模式打开 .mo 文件。
  2. 读取魔数和版本号: .mo 文件开头包含魔数和版本号,用于验证文件格式是否正确。
  3. 读取字符串数量、原文索引和译文索引: 这些信息用于定位原文和译文在文件中的位置。
  4. 读取原文长度和偏移量、译文长度和偏移量: 这些信息用于精确读取原文和译文。
  5. 读取原文和译文: 根据长度和偏移量,从文件中读取原文和译文,并将它们存储在 $this->entries 数组中。 $this->entries 数组是一个关联数组,键是原文,值是译文。
  6. 关闭文件: 使用 fclose() 函数关闭文件。
  7. 设置头部信息: .mo 文件中可能包含一些头部信息,例如 Plural-Forms。 这些信息存储在 $this->headers 数组中。

简单来说,import_from_file() 方法就是把 .mo 文件里的数据“搬运”到 MO 对象的内部存储结构中,方便后续的翻译查找。

(四) 如何使用 load_textdomain()

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

通常,你会在插件或主题的主文件中调用 load_textdomain()。 示例代码:

<?php
/**
 * Plugin Name: My Awesome Plugin
 * Description: A super 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' ); 这行代码告诉 WordPress,当所有插件加载完成后,执行 my_awesome_plugin_load_textdomain() 函数。
  2. 定义 my_awesome_plugin_load_textdomain() 函数: 这个函数负责调用 load_plugin_textdomain() 函数加载翻译文件。
  3. 调用 load_plugin_textdomain() 函数:

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

    load_plugin_textdomain() 是一个辅助函数,它实际上是 load_textdomain() 的一个“包装”。 它简化了插件翻译文件的加载过程。

    • 'my-awesome-plugin': Text Domain。 务必与你代码中使用的 Text Domain 保持一致。
    • false: 是否加载全局的翻译文件。 通常设置为 false,表示只加载插件目录下的翻译文件。
    • dirname( plugin_basename( __FILE__ ) ) . '/languages/': 翻译文件目录的相对路径。 这个路径相对于插件的主文件。 通常,你会把 .mo 文件放在插件目录下的 languages 目录中。

关键点:

  • 确保 .mo 文件存在于指定的目录下,并且文件名符合 WordPress 的命名规范:[text-domain]-[locale].mo。 例如,my-awesome-plugin-zh_CN.mo
  • 在你的代码中使用 __(), _e(), _x(), _n() 等翻译函数,并指定正确的 Text Domain。

(五) load_theme_textdomain():主题的“翻译官”

主题加载翻译文件的方式与插件类似,但使用的是 load_theme_textdomain() 函数。

示例代码:

<?php
/**
 * Functions.php
 */

add_action( 'after_setup_theme', 'my_awesome_theme_load_textdomain' );

function my_awesome_theme_load_textdomain() {
    load_theme_textdomain( 'my-awesome-theme', get_template_directory() . '/languages' );
}

这段代码与插件的示例非常相似:

  1. 注册 after_setup_theme 钩子: add_action( 'after_setup_theme', 'my_awesome_theme_load_textdomain' ); 这行代码告诉 WordPress,当主题设置完成后,执行 my_awesome_theme_load_textdomain() 函数。
  2. 定义 my_awesome_theme_load_textdomain() 函数: 这个函数负责调用 load_theme_textdomain() 函数加载翻译文件。
  3. 调用 load_theme_textdomain() 函数:

    load_theme_textdomain( 'my-awesome-theme', get_template_directory() . '/languages' );
    • 'my-awesome-theme': Text Domain。
    • get_template_directory() . '/languages': 翻译文件目录的绝对路径。 这个路径指向主题目录下的 languages 目录。

关键点:

  • 确保 .mo 文件存在于指定的目录下,并且文件名符合 WordPress 的命名规范:[text-domain]-[locale].mo。 例如,my-awesome-theme-zh_CN.mo
  • 在你的主题模板中使用 __(), _e(), _x(), _n() 等翻译函数,并指定正确的 Text Domain。

(六) 总结:load_textdomain() 的“一生”

咱们来总结一下 load_textdomain() 函数的“一生”:

步骤 描述
1. 接收参数 接收 Text Domain、.mo 文件路径和可选的 Locale 代码。
2. 确定 Locale 如果没有提供 Locale,则自动检测。
3. 验证文件路径 验证 .mo 文件路径的安全性。
4. 卸载旧版本 如果 Text Domain 已经加载,则卸载旧版本。
5. 加载 .mo 文件 创建 MO 对象,并调用 import_from_file() 方法加载 .mo 文件,将翻译字符串存储在 MO 对象的内部数据结构中。
6. 设置 Text Domain 设置 MO 对象的 Text Domain。
7. 返回结果 如果加载成功,则返回 true;否则,返回 false

load_textdomain() 函数是 WordPress 国际化机制的关键组成部分。 理解它的工作原理,可以帮助你更好地开发多语言插件和主题。

(七) 灵魂拷问:为什么要用 Text Domain?

最后,咱们来个灵魂拷问:为什么要用 Text Domain?

  • 避免冲突: 不同的插件和主题可能使用相同的翻译字符串。 Text Domain 可以将它们区分开来,避免翻译混乱。
  • 代码组织: Text Domain 可以将翻译字符串与特定的插件或主题关联起来,方便代码维护和管理。
  • 可移植性: 使用 Text Domain 可以使你的插件和主题更容易移植到不同的 WordPress 站点,而无需担心翻译冲突。

好了,今天的讲座就到这里。 希望大家对 load_textdomain() 函数有了更深入的了解。 记住,国际化是软件开发的重要组成部分,load_textdomain() 函数是实现 WordPress 国际化的重要工具。 感谢大家的观看!

发表回复

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