WordPress 外掛:完整 i18n + 發佈至 .org 的 SVN
使用時機
- 外掛剛通過 WP.org 審核,需要準備好讓 translate.wordpress.org 進行翻譯。
- 可翻譯字串 (
__(),_e(),wp.i18n.__()等) 是硬編碼的西班牙文,導致其他語系 (法文、德文、葡萄牙文、日文等) 在 GlotPress 上無法翻譯。 - 需要首次加入初始翻譯或
.pot檔案。 - 任何需要增加版本號並發佈至 plugins.svn.wordpress.org 的版本。
前置需求
| 元件 | 如何驗證 | 如果缺少 |
|---|---|---|
| TortoiseSVN (svn.exe) | Test-Path "C:\Program Files\TortoiseSVN\bin\svn.exe" | 從 https://tortoisesvn.net/downloads.html 安裝 (勾選「command line tools」) |
| PHP 8.x 在 PATH 中 | 在非 UNC 目錄下執行 php --version | Herd legacy 即可;或使用獨立的 PHP |
| wp-cli.phar | 從 https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar 下載 | Invoke-WebRequest -Uri ... -OutFile wp-cli.phar |
| 外掛的 SVN 存取權 | 來自 [email protected] 的核准郵件,內含 SVN URL、使用者名稱與密碼 | 如果缺少,請向團隊申請 |
完整工作流程
- 驗證環境與外掛
- 從 SVN 取出 (Checkout) 外掛
- 在程式碼中找出可翻譯字串 (PHP + JS)
- 將字串改寫為英文,同時保持技術語意
- 使用
wp i18n make-pot重新產生.pot - 產生初始翻譯的
.po+.mo(例如:中性西班牙文) - 同步增加版本號 (標頭
Version、常數、readme 穩定標籤、CHANGELOG) - 重新產生乾淨的建置版本,排除開發檔案 (
.distignore) - 提交至 trunk + 在 SVN 中建立標籤
- (可選) 同步本機目錄,保留 Git 檔案
1. 驗證環境與外掛
# Windows 上的 SVN (通常不在 PATH 中;使用完整路徑或加入 PATH)
$env:Path = "$env:ProgramFiles\TortoiseSVN\bin;$env:Path"
svn --version
# PHP
php --version
在修改程式碼之前,先閱讀外掛:
- 外掛標頭 (Name, Version, Text Domain, Domain Path)
readme.txt(Stable tag, Changelog)CHANGELOG.md(如果存在)includes/和assets/目錄中的 PHP/JS 檔案列表
2. 從 SVN 取出 (Checkout) 外掛
$svnUser = '<wp-org-user>' # WP.org 的使用者名稱 (區分大小寫)
$svnPass = 'svn_XXXXX...' # 來自 profiles.wordpress.org 的「Account & Security」
$slug = 'mi-plugin' # 外掛的 slug
$checkout = "C:\Temp\opencode\$slug-svn"
svn checkout --non-interactive --trust-server-cert `
--username $svnUser --password $svnPass `
"https://plugins.svn.wordpress.org/$slug" $checkout
SVN 已經包含 trunk/、tags/、assets/。如果你剛上傳第一個版本,tags/ 會是空的,trunk/ 也可能是空的 (剛建立的初始 trunk)。
3. 找出可翻譯字串
找出所有 i18n 函式呼叫。使用 grep 配合以下模式:
# PHP
Select-String -Path 'includes/*.php' -Pattern '__\(|_e\(|_x\(|esc_html__\(|esc_attr__\(|esc_html_e\(|esc_attr_e\(|_n\(|_nx\('
# JS (wp.i18n 或沒有函式包裝的硬編碼字串)
Select-String -Path 'assets/js/*.js' -Pattern 'wp\.i18n|__\('
Select-String -Path 'assets/js/*.js' -Pattern '"[^"]{6,80}"' # 偵測硬編碼字串
常見的反模式:JS 中串接的 HTML 內的西班牙文字串 (例如:html += '<option>Todas</option>';)。直接改寫為英文是可行的;如果希望它們在執行時可翻譯,則使用 wp.i18n.__() 更好。
4. 將字串改寫為英文
嚴格規則:
- 只翻譯 i18n 函式的第一個參數。不要動到 text-domain (第二個參數)。
- 不要動到 i18n 函式呼叫之外的任何內容。
- 不要修改 PHP 的格式 (空格、引號、括號)。
- 諸如
ExampleStore、WooCommerce、API、Correlation ID、Premium、JSON、HTTP、URL、CLP等技術術語維持英文 (因為它們已經是專有名詞/縮寫)。 - 精確保留 sprintf/printf 的佔位符 (
%s、%d、%1$s等)。 - 如果字串已經是英文,就不要動它。
建議的翻譯對照表 (根據上下文調整):
| 西班牙文 | 英文 |
|---|---|
| "Activo" | "Enabled" |
| "Título" | "Title" |
| "Configurar" | "Configure" |
| "Guardar" | "Save" |
| "Eliminar" | "Delete" |
| "Comuna" | "Commune" |
| "Región" | "Region" |
| "Selecciona una región" | "Select a region" |
| "Sin reglas guardadas" | "No saved rules" |
| "Configura la URL de API y el token" | "Configure API URL and token" |
| "Diagnóstico ejecutado correctamente" | "Diagnostic executed successfully" |
| "No se pudo {verbo}" | "Could not {verb}" |
| "Debes ingresar X" | "You must enter X" |
| "Cotización API recibida" | "API quote received" |
| "Tarifa por defecto (CLP)" | "Default rate (CLP)" |
對於大型檔案,將任務委派給一個代理 (agent),提供精確的指示和翻譯對照表。
5. 使用 wp-cli 重新產生 .pot
# 從外掛目錄 (本機鏡像) 執行
$wpCli = 'C:\Temp\opencode\wp-cli.phar'
php $wpCli i18n make-pot . "languages/mi-plugin.pot" --domain="mi-plugin"
.pot 必須位於 /languages/ 目錄,檔案名稱為 <text-domain>.pot。
6. 建立翻譯 .po + .mo
關鍵決定:使用哪個語言代碼?為使外掛能自動翻譯給所有西班牙語使用者而無需地區變體,使用 <text-domain>-es.po (根代碼 es)。WordPress 會進行串接後備:es_CL → es → 英文。
建議策略:
es.po/.mo(根代碼) → 適用於 es_CL、es_ES、es_MX、es_AR、es_PE 等。- (稍後可選) 針對智利的
es_CL.po/.mo,包含當地慣用語。
產生 .po:使用一個讀取 .pot 並套用 EN→ES 翻譯陣列的 PHP 腳本。使用 mb_chr(0x00f3) 來處理帶重音符號的字元 (Windows 上的 PHP 在使用 UTF-8 字面值時會有代碼頁問題)。
// build-es.php
declare(encoding='UTF-8');
$pot = file_get_contents('languages/mi-plugin.pot');
$po_es = 'languages/mi-plugin-es.po';
// 預載入 `mb_chr` 以處理帶重音符號的字元
function A() { return mb_chr(0x00e1); } // á
function E() { return mb_chr(0x00e9); } // é
function O() { return mb_chr(0x00f3); } // ó
// 等等
$t = [
"Configure API URL and token" => "Configura la URL de API y el token",
// ...
];
$header = "msgid \"\"\nmsgstr \"\"\n"
. "\"Project-Id-Version: Mi Plugin 1.0.0\\n\"\n"
. "\"Language-Team: Español (neutro) <>\\n\"\n"
. "\"Language: es\\n\"\n"
. "\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\n";
// 解析 .pot,用 $t 填入 msgstr,寫入 .po
// (完整邏輯:請參閱 wp-plugin-i18n-release/build-es.php)
編譯 .mo:
php $wpCli i18n make-mo languages/
這會自動建立 languages/<text-domain>-es.mo。
7. 同步增加版本號
更新四個檔案:
// 1. my-plugin.php (標頭)
* Version: 1.1.0
// 2. my-plugin.php (常數)
define('PLUGIN_VERSION', '1.1.0');
// 3. readme.txt
Stable tag: 1.1.0
/* + 在 == Changelog == 中加入新條目 */
// 4. CHANGELOG.md
## [1.1.0] - 2026-09-14
### Changed
- ...
8. 重新產生乾淨的建置版本 (排除開發檔案)
外掛的 .distignore 定義了要排除的內容。如果該檔案不存在,請使用這個標準列表:
$exclude = @('.git','.github','.gitignore','.gitattributes','.distignore',
'.idea','.DS_Store','.release-build','bin','node_modules',
'tests','vendor/bin','phpcs.xml*','phpunit.xml*',
'.phpunit.result.cache','*.lock','*.log','*.zip',
'README.md','RELEASING.md','Makefile','.pr-body-*.md')
# 清空目標目錄,複製所有檔案,然後刪除排除的檔案
$src = '路徑/鏡像/plugin'
$dist = '路徑/build/plugin'
Copy-Item -Path "$src\*" -Destination $dist -Recurse -Force
foreach ($pat in $exclude) {
Get-ChildItem -Path $dist -Recurse -Force -Filter $pat -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force
}
9. 提交至 trunk + 建立標籤
$env:Path = "$env:ProgramFiles\TortoiseSVN\bin;$env:Path"
# 用乾淨的建置版本替換 trunk
$trunk = "C:\Temp\opencode\$slug-svn\trunk"
Get-ChildItem -Path $trunk -Force | Where-Object { $_.Name -ne '.svn' } | Remove-Item -Recurse -Force
Copy-Item -Path "$dist\*" -Destination $trunk -Recurse -Force
# svn add + 提交
Set-Location $trunk
svn add --force --depth infinity .
svn commit --non-interactive --trust-server-cert `
--username $svnUser --password $svnPass `
-m "Release 1.1.0: English source strings, full i18n rewrite, initial Spanish (es) translation"
# 建立標籤 (遠端 svn copy)
svn copy --non-interactive --trust-server-cert `
--username $svnUser --password $svnPass `
-m "Tagging 1.1.0" `
"https://plugins.svn.wordpress.org/$slug/trunk" `
"https://plugins.svn.wordpress.org/$slug/tags/1.1.0"
在遠端標籤建立後,強制更新本機工作副本 (新的標籤不會完全在工作副本中實體化):
svn cleanup
svn update --depth infinity --force --non-interactive --trust-server-cert `
--username $svnUser --password $svnPass "$trunk/../"
10. (可選) 同步本機目錄
保留不在 SVN 匯出中的檔案 (.git/、.gitignore、tests/、bin/ 等):
$local = 'C:\Users\...\wp-content\plugins\mi-plugin'
$exportDir = 'C:\Users\...\export-1.1.0'
# 匯出剛建立的標籤
svn export "https://plugins.svn.wordpress.org/$slug/tags/1.1.0" $exportDir
# 只刪除本機目錄中與版本相關的檔案,保留其他檔案
$releaseNames = (Get-ChildItem $exportDir -Force).Name
foreach ($name in $releaseNames) {
$localPath = Join-Path $local $name
if (Test-Path $localPath) { Remove-Item $localPath -Recurse -Force }
}
# 將匯出的檔案複製回本機目錄
Copy-Item -Path "$exportDir\*" -Destination $local -Recurse -Force
11. 最終驗證
# 本機版本
Write-Host "header: " (Get-Content "$local\mi-plugin.php" | Select-String -Pattern 'Version:' | Select-Object -First 1).ToString().Trim()
Write-Host "readme: " (Get-Content "$local\readme.txt" | Select-String -Pattern 'Stable tag:' | Select-Object -First 1).ToString().Trim()
# 驗證遠端 SVN
svn ls --recursive "https://plugins.svn.wordpress.org/$slug/tags/1.1.0/"
WordPress.org 的關鍵約定
| 概念 | 規則 |
|---|---|
| Text Domain | 必須與外掛的 slug 完全相同 (例如:mi-plugin)。未經重新核准外掛,切勿更改 slug。 |
| Domain Path | /languages/ 以便 WP.org 找到 .pot 檔案 |
| Plugin Name 標頭 | 開頭不要包含第三方商標 (WP.org 審查會拒絕) |
| readme.txt | WP.org 解析這個檔案 (而不是 README.md)。Stable tag 必須與版本號相符。 |
| SVN 目錄結構 | /trunk/ (開發用)、/tags/X.Y.Z/ (版本)、/assets/ (橫幅、圖示、截圖) |
| ** SVN 作為發佈系統** | 「只推送準備好上線的版本」。不要在 trunk 頻繁提交開發中內容。 |
| 翻譯後備機制 | WP 會串接尋找:text-domain-locale.mo → text-domain.mo → text-domain-en_US.mo (核心)。根代碼 es 適用於所有 es_XX 變體。 |
常見問題與錯誤
PowerShell 與 UNC 路徑
如果當前工作目錄 (CWD) 是 UNC 路徑 (\\server\share),PHP 和許多二進位檔會失敗。在執行命令之前:
Set-Location C:\Users\<使用者> # 或任何本機目錄
php $wpCli ... # 現在可以正常運作
PHP (Windows) 中帶重音符號字元的編碼問題
Windows 上的 PHP 在處理腳本中的 UTF-8 字面值時有問題。解決方案:對每個帶重音符號的字元使用 mb_chr(0x00f3):
"Obtener pa" . mb_chr(0x00ed) . "s" // "Obtener país"
PowerShell 在執行 Get-Content 時也可能顯示 ? 而非帶重音符號的字元。請驗證檔案的位元組,而不是渲染結果:
$bytes = [System.IO.File]::ReadAllBytes('archivo.po')
# 查找 0xC3 0xB3 ('ó' 的 UTF-8 編碼)
建立新標籤後,SVN 工作副本似乎是空的
SVN 會在 trunk 和 tag 之間對檔案進行去重 (相同的「blob」)。遠端 svn copy 之後,檔案會出現在伺服器上,但本機工作副本則包含佔位符。解決方案:svn cleanup + svn update --depth infinity --force。
svn.exe 不在 PATH 中
TortoiseSVN 安裝在 C:\Program Files\TortoiseSVN\bin\,但預設不會將 svn.exe 加入 PATH。臨時解決方案:
$env:Path = "$env:ProgramFiles\TortoiseSVN\bin;$env:Path"
或者永久加入使用者 PATH。
外掛在標頭中使用西班牙文字串
外掛標頭的字串 (Plugin Name、Description 等) 是不可翻譯的。但如果 wp i18n 將它們包含在 .pot 中,請保留 msgstr "" (空字串)。WP.org 和 GlotPress 知道它們不會被翻譯。
不要包含 GlotPress 管理的語系的 .po/.mo
一旦 GlotPress 有你的外掛翻譯 (來自社群),WP.org 會自動提供 .mo 檔案。你初始的 .po/.mo 只是初始的種子。不要與 GlotPress 對抗。
版本發佈所修改的檔案 (核對清單)
-
my-plugin.php— 標頭 Version + PLUGIN_VERSION -
readme.txt— Stable tag + Changelog 中的條目 -
CHANGELOG.md— 新條目 -
languages/<text-domain>.pot— 重新產生 -
languages/<text-domain>-es.po— 填入初始翻譯 -
languages/<text-domain>-es.mo— 編譯的二進位檔案 -
.distignore— 如有需要則建立/更新 - 在
plugins.svn.wordpress.org上建立tags/X.Y.Z/