[WordPress] 外掛國際化(i18n)教學 – 讓外掛支援多國語言

本篇文章更新時間:2026/03/03
如有資訊過時或語誤之處,歡迎使用 Contact 功能通知或向一介資男的 LINE 社群反應。
如果本站內容對你有幫助,歡迎贊助支持


本系列文參考自 WordPress.org 官方外掛開發文件 - Internationalization 的繁體中文版本,並加入作者實務開發經驗補充。

WordPress 外掛國際化(Internationalization,簡稱 i18n)是讓你的外掛能被翻譯成多國語言的標準化流程。WordPress 本身就是一個支援超過 200 種語言的全球化平台,如果你開發的外掛只有英文介面,等於直接放棄了全球大部分的使用者。國際化並不是「翻譯」本身,而是在程式碼層面做好準備,讓所有使用者介面上的字串都能被替換為其他語言的對應文字。本文將完整介紹 WordPress 外掛國際化的核心觀念、翻譯函式用法、Text Domain 設定、翻譯檔案格式,以及如何使用 WP-CLI 快速產生翻譯範本。

國際化基本概念

在 WordPress 生態系中,有兩個經常被混用但意義不同的術語:

  • 國際化(Internationalization,i18n):指的是在開發階段將程式碼中的字串標記為「可翻譯」的過程。i18n 這個縮寫來自 Internationalization 的首字母 i、尾字母 n,以及中間的 18 個字母。國際化是開發者的工作,目標是讓程式碼具備多語言支援的能力。
  • 在地化(Localization,l10n):指的是針對特定語言或地區,將標記過的字串翻譯成目標語言的過程。l10n 的命名邏輯與 i18n 相同。在地化是翻譯者的工作,通常在開發完成後進行。

簡單來說:i18n 是「讓程式碼準備好被翻譯」,l10n 是「實際進行翻譯」。身為外掛開發者,你的首要任務是做好 i18n,確保每一段面向使用者的文字都被正確標記。至於翻譯工作,可以由你自己完成,也可以交給社群翻譯者透過 WordPress.org 翻譯平台來協作。

WordPress 使用 GNU gettext 作為底層的國際化框架。gettext 是一套成熟且廣泛使用的國際化工具鏈,PHP 原生就支援。WordPress 在此基礎上封裝了一系列翻譯函式,讓開發者不需要直接操作 gettext API,就能輕鬆完成國際化工作。

翻譯函式

WordPress 提供了多個翻譯函式,每個都有其特定的使用情境。以下逐一介紹最常用的函式及其使用方式。

__() - 取得翻譯字串

__() 是最基本的翻譯函式,它接受一個英文字串和 Text Domain,回傳翻譯後的字串。適用於需要將翻譯結果存入變數、傳入函式參數,或進行後續處理的情境。

// 基本用法:取得翻譯字串
$label = __( 'Settings', 'my-plugin' );

// 在函式參數中使用
add_menu_page(
    __( 'My Plugin Settings', 'my-plugin' ),  // 頁面標題
    __( 'My Plugin', 'my-plugin' ),            // 選單標題
    'manage_options',
    'my-plugin-settings',
    'my_plugin_settings_page'
);

// 在陣列中使用
$options = array(
    'draft'   => __( 'Draft', 'my-plugin' ),
    'pending' => __( 'Pending Review', 'my-plugin' ),
    'publish' => __( 'Published', 'my-plugin' ),
);

_e() - 直接輸出翻譯字串

_e()__() 功能相同,差別在於 _e() 會直接 echo 翻譯結果,而不是回傳。適用於在模板或 HTML 中直接輸出文字的場景。

// 在 HTML 模板中直接輸出

何時用 __()、何時用 _e() 規則很簡單:如果你需要把翻譯結果當作值來使用(存變數、傳參數、做字串串接),就用 __();如果只是要在 HTML 中直接顯示文字,就用 _e()

_n() - 處理複數形式

不同語言的複數規則差異極大。英文只有單數和複數兩種形式,但有些語言(如阿拉伯文)有六種複數形式。_n() 讓 WordPress 能根據數量自動選擇正確的語言形式。

/**
 * _n( $single, $plural, $number, $domain )
 *
 * @param string $single 單數形式的字串
 * @param string $plural 複數形式的字串
 * @param int    $number 用來判斷單複數的數字
 * @param string $domain Text Domain
 */

// 基本用法
$message = sprintf(
    _n(
        '%d item found.',     // 單數
        '%d items found.',    // 複數
        $count,               // 數量
        'my-plugin'           // Text Domain
    ),
    $count
);

// 實務範例:顯示評論數量
$comment_count = get_comments_number();
$comment_text  = sprintf(
    _n(
        'This post has %d comment.',
        'This post has %d comments.',
        $comment_count,
        'my-plugin'
    ),
    number_format_i18n( $comment_count )
);

請注意,_n() 只負責根據數量選擇正確的字串形式,它不會自動替換 %d 佔位符。你需要搭配 sprintf() 來完成字串的最終組裝。另外,建議使用 number_format_i18n() 來格式化數字,這樣數字的千位分隔符也會根據語系自動調整。

_x() - 帶上下文的翻譯

有些英文單字在不同上下文中有不同意思。例如「Post」可以是名詞(文章)也可以是動詞(發布)。_x() 允許你為同一個字串提供不同的上下文提示,讓翻譯者能根據語境做出正確的翻譯。

/**
 * _x( $text, $context, $domain )
 *
 * @param string $text    要翻譯的字串
 * @param string $context 給翻譯者的上下文說明
 * @param string $domain  Text Domain
 */

// 「Post」作為名詞(文章)
$label = _x( 'Post', 'noun - a blog post', 'my-plugin' );

// 「Post」作為動詞(發布)
$button = _x( 'Post', 'verb - to publish', 'my-plugin' );

// 「Order」在電商和排序的不同語境
$shop_order = _x( 'Order', 'noun - a purchase order', 'my-plugin' );
$sort_order = _x( 'Order', 'noun - sorting order', 'my-plugin' );

// 對應的 echo 版本 _ex()
_ex( 'Post', 'verb - to publish', 'my-plugin' );

上下文字串不會出現在前端,它只會出現在翻譯檔案中,幫助翻譯者理解這段文字的使用場景。建議養成習慣,遇到有歧義的單字一律使用 _x()

_nx() - 帶上下文的複數形式

_nx() 結合了 _n()_x() 的功能,同時處理複數和上下文。

// 帶上下文的複數翻譯
$message = sprintf(
    _nx(
        '%d item',            // 單數
        '%d items',           // 複數
        $count,               // 數量
        'cart items count',   // 上下文
        'my-plugin'           // Text Domain
    ),
    $count
);

Escaped 翻譯函式 - 安全輸出

WordPress 提供了帶有跳脫處理的翻譯函式變體,這些函式在翻譯的同時會對輸出做 HTML 跳脫,防止 XSS 攻擊。這在安全性上至關重要,因為翻譯字串可能來自不受信任的來源。

// esc_html__() - 取得經過 HTML 跳脫的翻譯字串
$safe_text = esc_html__( 'Settings saved.', 'my-plugin' );

// esc_html_e() - 直接輸出經過 HTML 跳脫的翻譯字串
esc_html_e( 'Plugin Settings', 'my-plugin' );

// esc_attr__() - 取得經過屬性跳脫的翻譯字串(用於 HTML 屬性)
echo '';

// esc_attr_e() - 直接輸出經過屬性跳脫的翻譯字串
echo '';

最佳實務:只要翻譯字串會被輸出到 HTML 中,就應該使用 escaped 版本的翻譯函式。特別是當字串會被放在 HTML 屬性中時,一定要使用 esc_attr__()esc_attr_e()。更多關於安全性的實踐,可以參考外掛安全性教學

Text Domain 設定

Text Domain 是 WordPress 用來區分不同外掛翻譯字串的唯一識別碼。每個外掛都應該有自己的 Text Domain,而且必須與外掛的 slug 一致。

在外掛標頭宣告 Text Domain

Text Domain 必須在外掛主檔案的標頭註解中宣告:

/**
 * Plugin Name: My Awesome Plugin
 * Plugin URI:  https://example.com/my-awesome-plugin
 * Description: A plugin that does awesome things.
 * Version:     1.0.0
 * Author:      Developer Name
 * Author URI:  https://example.com
 * Text Domain: my-awesome-plugin
 * Domain Path: /languages
 * License:     GPL-2.0+
 */

其中有兩個與國際化相關的欄位:

  • Text Domain:外掛的翻譯識別碼,必須與外掛的目錄名稱(slug)完全一致。如果你的外掛目錄是 wp-content/plugins/my-awesome-plugin/,那 Text Domain 就是 my-awesome-plugin
  • Domain Path:翻譯檔案的存放路徑,相對於外掛根目錄。慣例上使用 /languages

載入翻譯檔案 - load_plugin_textdomain()

光是宣告 Text Domain 還不夠,你還需要在外掛初始化時告訴 WordPress 去哪裡載入翻譯檔案。這個工作由 load_plugin_textdomain() 完成:

/**
 * 載入外掛翻譯檔案
 */
function my_plugin_load_textdomain() {
    load_plugin_textdomain(
        'my-awesome-plugin',                       // Text Domain
        false,                                      // 已棄用的參數,傳 false
        dirname( plugin_basename( __FILE__ ) ) . '/languages'  // 翻譯檔案路徑
    );
}
add_action( 'init', 'my_plugin_load_textdomain' );

這個函式應該掛在 init Hook 上執行。第二個參數已被棄用,永遠傳入 false。第三個參數是翻譯檔案的相對路徑(相對於 wp-content/plugins/ 目錄)。

從 WordPress 4.6 開始,WordPress 會優先從 wp-content/languages/plugins/ 目錄載入翻譯檔案,然後才會去外掛自身的 /languages 目錄尋找。這表示使用者可以在不修改外掛檔案的情況下,將自訂翻譯放在 wp-content/languages/plugins/ 目錄中,而且不會被外掛更新覆蓋。

JavaScript 翻譯的載入

如果你的外掛有 JavaScript 檔案需要國際化,從 WordPress 5.0 起可以使用 wp_set_script_translations()

/**
 * 載入前端腳本的翻譯
 */
function my_plugin_enqueue_scripts() {
    wp_enqueue_script(
        'my-plugin-script',
        plugins_url( 'js/admin.js', __FILE__ ),
        array( 'wp-i18n' ),  // 必須依賴 wp-i18n
        '1.0.0',
        true
    );

    wp_set_script_translations(
        'my-plugin-script',       // 腳本 handle
        'my-awesome-plugin',      // Text Domain
        plugin_dir_path( __FILE__ ) . 'languages'  // 翻譯檔案路徑
    );
}
add_action( 'admin_enqueue_scripts', 'my_plugin_enqueue_scripts' );

在 JavaScript 中,你可以使用 wp.i18n 物件提供的翻譯函式:

const { __, _n, _x, sprintf } = wp.i18n;

// 基本翻譯
const title = __( 'Settings', 'my-awesome-plugin' );

// 複數形式
const message = sprintf(
    _n( '%d item selected', '%d items selected', count, 'my-awesome-plugin' ),
    count
);

// 帶上下文
const label = _x( 'Post', 'verb', 'my-awesome-plugin' );

翻譯檔案

WordPress 的翻譯系統使用三種主要檔案格式,各有不同用途。了解這些格式的角色與關係,對於管理外掛的多語言支援至關重要。

POT 檔案(Portable Object Template)

POT 是翻譯範本檔案,包含外掛中所有可翻譯字串的清單,但不包含任何翻譯。它是翻譯流程的起點,翻譯者會以此為基礎建立各語言的翻譯。

# POT 檔案範例片段
#: includes/admin.php:42
msgid "Settings"
msgstr ""

#: includes/admin.php:58
msgid "Save Changes"
msgstr ""

#. translators: %d is the number of items
#: includes/list.php:15
msgid "%d item found."
msgid_plural "%d items found."
msgstr[0] ""
msgstr[1] ""

POT 檔案的命名慣例是 {text-domain}.pot,例如 my-awesome-plugin.pot

PO 檔案(Portable Object)

PO 是人類可讀的翻譯檔案。翻譯者會複製一份 POT 檔案,重新命名為 PO 檔案,然後在其中填入各語言的翻譯。PO 檔案的命名格式是 {text-domain}-{locale}.po,例如 my-awesome-plugin-zh_TW.po

# PO 檔案範例(繁體中文翻譯)
#: includes/admin.php:42
msgid "Settings"
msgstr "設定"

#: includes/admin.php:58
msgid "Save Changes"
msgstr "儲存變更"

#: includes/list.php:15
msgid "%d item found."
msgid_plural "%d items found."
msgstr[0] "找到 %d 個項目。"

繁體中文的 locale 代碼是 zh_TW,簡體中文是 zh_CN,日文是 ja,韓文是 ko_KR

MO 檔案(Machine Object)

MO 是 PO 檔案的二進位編譯版本。WordPress 實際運行時讀取的是 MO 檔案,因為二進位格式的讀取效率遠高於純文字的 PO 檔案。每次更新 PO 檔案後,都需要重新編譯為 MO 檔案。命名格式與 PO 相同,但副檔名為 .mo{text-domain}-{locale}.mo

常用的翻譯工具如 Poedit 會在儲存 PO 檔案時自動產生對應的 MO 檔案。你也可以使用 WP-CLI 的 wp i18n make-mo 指令來手動編譯。

JSON 翻譯檔案(JavaScript 專用)

從 WordPress 5.0 開始,JavaScript 檔案的翻譯使用 JSON 格式。當你使用 wp_set_script_translations() 載入前端翻譯時,WordPress 會從 JSON 格式的翻譯檔案中讀取對應的翻譯。

JSON 翻譯檔案的命名格式是 {text-domain}-{locale}-{md5-hash}.json,其中 md5 hash 是根據 JavaScript 檔案的相對路徑計算出來的。這種命名方式讓每個 JavaScript 檔案都能有自己專屬的翻譯檔案,避免載入不必要的翻譯資料。

翻譯檔案的目錄結構

一個完整的外掛翻譯檔案結構如下:

my-awesome-plugin/
├── languages/
│   ├── my-awesome-plugin.pot                          # 翻譯範本
│   ├── my-awesome-plugin-zh_TW.po                     # 繁體中文 PO
│   ├── my-awesome-plugin-zh_TW.mo                     # 繁體中文 MO
│   ├── my-awesome-plugin-ja.po                        # 日文 PO
│   ├── my-awesome-plugin-ja.mo                        # 日文 MO
│   └── my-awesome-plugin-zh_TW-admin-js.json          # JS 翻譯
├── includes/
├── js/
└── my-awesome-plugin.php

使用 WP-CLI 產生翻譯範本

WP-CLI 提供了 wp i18n 系列指令,可以自動掃描外掛程式碼中的翻譯函式呼叫,並產生對應的翻譯檔案。這比手動維護 POT 檔案高效且不容易出錯。

wp i18n make-pot - 產生 POT 翻譯範本

make-pot 是最常使用的指令,它會掃描指定目錄中所有 PHP 和 JavaScript 檔案,找出所有翻譯函式呼叫,然後產生 POT 檔案。

# 基本用法:在外掛目錄下執行
wp i18n make-pot . languages/my-awesome-plugin.pot

# 指定 Text Domain(通常自動從外掛標頭讀取)
wp i18n make-pot . languages/my-awesome-plugin.pot --domain=my-awesome-plugin

# 排除特定目錄(例如 node_modules、vendor、tests)
wp i18n make-pot . languages/my-awesome-plugin.pot \
    --exclude=node_modules,vendor,tests

# 只掃描特定目錄
wp i18n make-pot . languages/my-awesome-plugin.pot \
    --include=includes,admin,public

# 加入翻譯者備註的前綴
wp i18n make-pot . languages/my-awesome-plugin.pot \
    --headers='{"Report-Msgid-Bugs-To":"https://example.com/support"}'

wp i18n make-json - 產生 JavaScript 翻譯檔案

如果你的外掛有 JavaScript 需要翻譯,需要從 PO 檔案中擷取 JavaScript 的翻譯並轉為 JSON 格式:

# 從 PO 檔案產生 JSON 翻譯
wp i18n make-json languages/my-awesome-plugin-zh_TW.po languages/

# 這會在 languages/ 目錄下產生類似以下的 JSON 檔案:
# my-awesome-plugin-zh_TW-{hash}.json

wp i18n make-mo - 編譯 MO 檔案

# 從 PO 檔案編譯 MO 檔案
wp i18n make-mo languages/my-awesome-plugin-zh_TW.po

# 批次編譯目錄中所有 PO 檔案
wp i18n make-mo languages/

整合到開發流程中

建議在你的外掛建置流程中加入翻譯範本的自動產生。以下是一個簡單的 npm script 或 Makefile 範例:

# Makefile
.PHONY: i18n

i18n:
	wp i18n make-pot . languages/my-awesome-plugin.pot \
		--exclude=node_modules,vendor,tests
	@echo "POT file generated successfully."

i18n-compile:
	wp i18n make-mo languages/
	wp i18n make-json languages/ --no-purge
	@echo "MO and JSON files compiled."

每次修改外掛程式碼中的翻譯字串後,都應該重新執行 make-pot 來更新 POT 檔案,然後通知翻譯者更新各語言的 PO 檔案。

實務建議

以下是多年外掛開發經驗中關於國際化的實務建議:

  • 從專案第一天就做 i18n:國際化不是事後加上去的功能,而是開發過程中的基本紀律。如果等到外掛開發完成才回頭補 i18n,你會發現有大量字串散落在程式碼各處,補起來既費時又容易遺漏。從寫第一行面向使用者的文字開始,就用翻譯函式包起來。
  • 善用 WordPress.org 翻譯平台:如果你的外掛託管在 WordPress.org 上,可以利用 translate.wordpress.org 翻譯平台。只要你正確設定了 Text Domain 並上傳 POT 檔案,全球的翻譯社群就能直接在平台上為你的外掛貢獻翻譯。翻譯完成後,WordPress 會自動將翻譯檔案分發給使用者,你不需要將翻譯檔案打包在外掛中。
  • 一律使用 Escaped 翻譯函式:翻譯字串可能來自 PO 檔案或翻譯平台,這些內容理論上是不受信任的第三方輸入。如果翻譯者(無論是否有意)在翻譯中插入了惡意的 HTML 或 JavaScript,未跳脫的翻譯函式會直接輸出這些內容。因此,凡是翻譯結果會出現在 HTML 中的場景,都應該使用 esc_html__()esc_html_e()esc_attr__()esc_attr_e() 等帶有跳脫處理的版本。更多安全性細節請參考外掛安全性教學
  • 不要翻譯不該翻譯的東西:程式邏輯中使用的字串(如 post type slug、meta key、option name、Hook 名稱)不應該被翻譯。只有使用者介面上會看到的文字才需要國際化。翻譯內部字串不只多此一舉,還可能導致嚴重的程式錯誤。
  • 提供翻譯者足夠的上下文:使用 translators 註解來說明佔位符的意義。這些註解會被擷取到 POT 檔案中,幫助翻譯者理解原始字串。
// 為翻譯者提供上下文的註解範例
/* translators: %s: user display name */
$welcome = sprintf( esc_html__( 'Welcome, %s!', 'my-plugin' ), $user->display_name );

/* translators: 1: start date, 2: end date */
$period = sprintf(
    esc_html__( 'Report period: %1$s to %2$s', 'my-plugin' ),
    $start_date,
    $end_date
);

/* translators: %d: number of days */
$notice = sprintf(
    esc_html( _n(
        'Your trial expires in %d day.',
        'Your trial expires in %d days.',
        $days_left,
        'my-plugin'
    ) ),
    $days_left
);

國際化看似增加了開發工作量,但長遠來看,它讓你的外掛能觸及全球使用者、獲得社群的翻譯協助,並且符合 WordPress 生態系的開發規範。做好 i18n,是專業外掛開發者的基本功。


本文是「WordPress 外掛開發完整指南」系列的第 13 篇。

上一篇:[WordPress] WP-Cron 排程任務開發教學

下一篇:[WordPress] 外掛開發之隱私權該注意的設計方法


Share:

作者: Chun

WordPress 社群貢獻者、開源社群推廣者。專注於 WordPress 外掛開發、網站效能最佳化、伺服器管理,以及 iDempiere 開源 ERP 導入與客製開發。曾參與 WordCamp Taipei 等社群活動,GitHub Arctic Code Vault Contributor。提供資訊顧問、WordPress 開發教學、主機最佳化與企業 ERP 整合服務。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *


文章
Filter
Apply Filters
Mastodon