WordPress 插件:完整国际化 + 发布到 SVN
何时使用
- 刚刚在 WP.org 获得批准,需要准备好用于 translate.wordpress.org 的插件。
- 可翻译字符串(
__()、_e()、wp.i18n.__()等)被硬编码为西班牙语,阻碍了其他语言环境(如 fr、de、pt、ja 等)在 GlotPress 中工作。 - 需要首次添加初始翻译或
.pot文件。 - 任何需要更新版本并发布到 plugins.svn.wordpress.org 的发布。
先决条件
| 组件 | 验证方式 | 如果缺失 |
|---|---|---|
| TortoiseSVN (svn.exe) | Test-Path "C:\Program Files\TortoiseSVN\bin\svn.exe" | 从 https://tortoisesvn.net/downloads.html 安装(勾选“命令行工具”) |
| PATH 中的 PHP 8.x | 从非 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(例如中性西班牙语) - 同步更新版本号(header
Version、常量、readme 稳定版标签、CHANGELOG) - 重新生成干净的构建包,排除开发文件(
.distignore) - 提交到 trunk + 在 SVN 中创建标签
- (可选)同步本地目录,同时保留 Git 文件
1. 验证环境和插件
# Windows 上的 SVN(通常不在 PATH 中;使用完整路径或添加)
$env:Path = "$env:ProgramFiles\TortoiseSVN\bin;$env:Path"
svn --version
# PHP
php --version
在修改代码之前,阅读插件:
- 插件头部(名称、版本、文本域、域路径)
readme.txt(稳定版标签、更新日志)CHANGELOG.md(如果存在)includes/和assets/中的 PHP/JS 文件列表
2. 插件的 SVN Checkout
$svnUser = '<wp-org-user>' # WP.org 用户名(区分大小写)
$svnPass = 'svn_XXXXX...' # 来自 profiles.wordpress.org 的“账户与安全”
$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 函数的第一个参数。不要动文本域(第二个参数)。
- 不要动 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 {动词}" | "Could not {动词}" |
| "Debes ingresar X" | "You must enter X" |
| "Cotización API recibida" | "API quote received" |
| "Tarifa por defecto (CLP)" | "Default rate (CLP)" |
对于大文件,将任务委托给一个代理,并提供精确的指令和翻译表。
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 文件:使用一个 PHP 脚本,读取 .pot 并应用 EN→ES 的翻译数组。对于带重音符号的字符,使用 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 (header)
* 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 = 'ruta/espejo/plugin'
$dist = 'ruta/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 + commit
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 的关键约定
| 概念 | 规则 |
|---|---|
| 文本域 | 必须与插件 slug 完全相同(例如 mi-plugin)。未经重新批准,切勿更改 slug。 |
| 域路径 | /languages/,以便 WP.org 找到 .pot 文件 |
| 插件名称头部 | 开头不要包含第三方商标(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 路径
如果当前工作目录是 UNC 路径(\server\share),PHP 和许多二进制文件会失败。在运行命令之前:
Set-Location C:\Users\<usuario> # 或任何本地目录
php $wpCli ... # 现在可以正常工作
Windows 上 PHP 中带重音符号字符的编码问题
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 中。
插件在头部使用了西班牙语字符串
插件头部的字符串(插件名称、描述等)不可翻译。但是,如果 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— 已编译(可选) - 所有 PHP/JS 字符串均为英文
外部资源
- 插件手册 i18n:https://developer.wordpress.org/plugins/internationalization/
- wp-cli i18n 包:https://github.com/wp-cli/i18n-command
- translate.wordpress.org / GlotPress:https://translate.wordpress.org/
- WP.org SVN 指南:https://developer.wordpress.org/plugins/wordpress-org/how-to-use-subversion/
- readme.txt 验证器:https://wordpress.org/plugins/developer
…
[为翻译而截断]