本篇文章更新時間: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] 外掛開發之隱私權該注意的設計方法
