前言
這段 PHP 程式碼是為了解決 WordPress 使用 Polylang 多語系外掛時,如何批量將特定文章類型的內容從一種語言複製到另一種語言,並建立正確的翻譯關聯。適合需要管理大量多語系內容的網站管理員或開發者,尤其是想自動化翻譯文章複製流程的技術人員。
Polylang 官方 API 的使用背景
Polylang 提供了多個官方函式來管理文章語言與翻譯關聯,如 pll_set_post_language 設定文章語言,pll_save_post_translations 儲存翻譯群組,這是官方推薦的做法,能確保多語系資料完整且一致。
後台介面設計與操作流程
後台子選單與頁面
程式碼透過 add_submenu_page 在自訂文章類型「investor_relations」的管理頁面下新增子選單,提供單筆測試與批量複製兩種操作介面。使用者可選擇目標語言(目前限制為英文與日文),並執行對應動作。
權限與安全檢查
頁面載入時會檢查使用者是否有 manage_options 權限,並確認 Polylang API 函式存在,避免外掛未啟用或權限不足導致錯誤。
核心功能:複製文章並建立翻譯關聯
1. 確認來源文章及語言設定
函式 ir_clone_post_official 會先取得來源文章,並用 pll_set_post_language 強制設定來源文章語言,確保語言標記正確。
ir_set_lang_official( $source_id, $source_lang );
2. 檢查目標語言翻譯是否已存在
利用 pll_get_post_translations 取得該文章的所有語言版本,若目標語言已存在翻譯文章,則跳過建立,避免重複。
3. 建立新文章(翻譯版本)
使用 WordPress 官方函式 wp_insert_post 複製文章內容與屬性,並設定新文章的語言為目標語言。
$new_id = wp_insert_post([...]);
ir_set_lang_official( $new_id, $target_lang );
4. 儲存翻譯關聯
將來源文章與新文章的語言關聯資料更新至 Polylang,確保前台語言切換功能正常。
$translations[ $target_lang ] = $new_id;
pll_save_post_translations( $translations );
5. 複製分類法與特色圖片
排除語言分類法後,複製其他自訂分類法的關聯,並複製特色圖片,保持內容一致性。
6. 複製 ACF 自訂欄位資料
若有安裝 Advanced Custom Fields 外掛,會將所有欄位資料一併複製,包含上傳檔案,避免資料遺失。
實務應用與優化建議
- 備份資料庫:執行批量複製前務必備份,避免誤操作導致資料錯亂。
- 單筆測試:先用單筆測試功能確保流程正常,再進行批量操作。
- 擴充語言:可依需求新增允許的目標語言陣列。
- 錯誤處理:可加強錯誤回報與日誌紀錄,方便除錯與維護。
常見問題與注意事項
- Polylang API 函式不存在時,需確認外掛是否啟用。
- 複製文章時,若有其他外掛影響文章資料,可能需要額外處理。
- 語言分類法不應被複製,以免覆寫 Polylang 的語言設定。
完整程式碼
<?php /** * Investor Relations 單語言批量翻譯複製工具(最終版) * - 完全走 Polylang 官方流程: * wp_insert_post + pll_set_post_language + pll_save_post_translations * - 自動跳過「已經有目標語言翻譯」的文章 */ define( 'IR_SOURCE_LANG', 'zh' ); // 來源語言:中文 define( 'IR_POST_TYPE', 'investor_relations' ); // Post type:investor_relations /* ========================= * 共用:設定語言(官方 API) * ========================= */ function ir_set_lang_official( $post_id, $lang_code ) { if ( function_exists( 'pll_set_post_language' ) ) { pll_set_post_language( $post_id, $lang_code ); } } /* ========================= * 後台子選單 * ========================= */ add_action( 'admin_menu', function () { add_submenu_page( 'edit.php?post_type=' . IR_POST_TYPE, 'Bulk Copy Translations (Official)', 'Bulk Copy Translations (Official)', 'manage_options', 'ir-bulk-translations-official', 'ir_bulk_translations_official_page' ); }); /* ========================= * 後台頁面 + 控制流程 * ========================= */ function ir_bulk_translations_official_page() { // 允許的目標語言(依照 Polylang 的 code) $allowed_target_langs = [ 'en', 'ja' ]; if ( ! current_user_can( 'manage_options' ) ) { wp_die( '沒有權限。' ); } if ( ! function_exists( 'pll_set_post_language' ) || ! function_exists( 'pll_save_post_translations' ) ) { echo '<div class="notice notice-error"><p>Polylang API 不存在,請確認外掛是否啟用。</p></div>'; return; } // 目前選擇的目標語言(預設 en) $current_target = isset( $_POST['ir_target_lang'] ) ? sanitize_text_field( $_POST['ir_target_lang'] ) : 'en'; if ( ! in_array( $current_target, $allowed_target_langs, true ) ) { $current_target = 'en'; } echo ' <h1>Investor Relations 翻譯複製工具(官方流程版)</h1>'; echo ' <p>來源語言固定為:<code>' . esc_html( IR_SOURCE_LANG ) . ''; echo '⚠️ 執行前務必先備份資料庫!
'; echo ' .ir-lang-select{margin-left:8px;}'; /* ------------ 單筆測試表單 ------------- */ echo '單筆文章測試
'; echo ''; wp_nonce_field( 'ir_single_test_official' ); echo ''; echo '目標語言:'; echo ''; foreach ( $allowed_target_langs as $code ) { printf( '%s', esc_attr( $code ), selected( $current_target, $code, false ), esc_html( strtoupper( $code ) ) ); } echo ' '; submit_button( '測試複製此文章', 'secondary', 'ir_single_test', false ); echo ''; /* ------------ 批次表單 ------------- */ echo '批量複製所有中文文章
'; echo ''; wp_nonce_field( 'ir_bulk_official' ); echo '目標語言:'; echo ''; foreach ( $allowed_target_langs as $code ) { printf( '%s', esc_attr( $code ), selected( $current_target, $code, false ), esc_html( strtoupper( $code ) ) ); } echo ' '; submit_button( '執行批次複製(請先單筆測試)', 'primary', 'ir_bulk_run', false ); echo ''; /* ------------ 執行動作 ------------- */ // 單筆 if ( isset( $_POST['ir_single_test'] ) ) { check_admin_referer( 'ir_single_test_official' ); $post_id = isset( $_POST['ir_test_id'] ) ? intval( $_POST['ir_test_id'] ) : 0; $target = isset( $_POST['ir_target_lang'] ) ? sanitize_text_field( $_POST['ir_target_lang'] ) : 'en'; ir_run_single_copy_official( $post_id, $target ); } // 批次 if ( isset( $_POST['ir_bulk_run'] ) ) { check_admin_referer( 'ir_bulk_official' ); $target = isset( $_POST['ir_target_lang'] ) ? sanitize_text_field( $_POST['ir_target_lang'] ) : 'en'; ir_run_bulk_copy_official( $target ); } }
function ir_run_single_copy_official( $post_id, $target_lang ) {
echo '
<h3>單筆測試結果</h3>';
if ( ! $post_id || ! get_post( $post_id ) ) {
echo '<p style="color:red;">找不到文章 ID:' . esc_html( $post_id ) . '</p>';
return;
}
$report = ir_clone_post_official( $post_id, $target_lang );
echo '<pre style="background:white; padding:10px; border:1px solid #ccc;">';
echo esc_html( implode( "\n", $report ) );
echo '</pre>';
}
/* =========================
* 批量處理所有中文文章(官方流程)
* ========================= */
function ir_run_bulk_copy_official( $target_lang ) {
$posts = get_posts( [
'post_type' => IR_POST_TYPE,
'posts_per_page' => -1,
'post_status' => 'any',
'lang' => IR_SOURCE_LANG, // Polylang 的語言 query var
'fields' => 'ids',
] );
echo '
<h2>批次執行結果(目標語言:' . esc_html( strtoupper( $target_lang ) ) . ')</h2>';
if ( empty( $posts ) ) {
echo '
<p>沒有找到來源語言(' . esc_html( IR_SOURCE_LANG ) . ')的文章。</p>';
return;
}
$output = [];
foreach ( $posts as $post_id ) {
$report = ir_clone_post_official( $post_id, $target_lang );
$output = array_merge( $output, $report );
}
echo '<pre style="background:white; max-height:450px; overflow:auto; padding:10px; border:1px solid #ccc;">';
echo esc_html( implode( "\n", $output ) );
echo '</pre>';
}
/* =========================
* 核心:官方流程複製一篇文章 → 目標語言翻譯
* ========================= */
function ir_clone_post_official( $source_id, $target_lang ) {
$report = [];
$source_post = get_post( $source_id );
if ( ! $source_post ) {
$report[] = "❌ 原文 {$source_id} 不存在";
return $report;
}
$source_lang = IR_SOURCE_LANG;
// Step 1) 先確保原文語言正確
ir_set_lang_official( $source_id, $source_lang );
$report[] = "Step 1) 設定原文語言:{$source_id} → {$source_lang}";
// 取得現有翻譯 group
$translations = function_exists( 'pll_get_post_translations' )
? pll_get_post_translations( $source_id )
: [];
if ( empty( $translations[ $source_lang ] ) ) {
$translations[ $source_lang ] = $source_id;
}
// ✅ 若已存在該語言翻譯 → 自動跳過
if ( ! empty( $translations[ $target_lang ] ) ) {
$existing = $translations[ $target_lang ];
$report[] = "⚠️ 原文 {$source_id} 已有 {$target_lang} 翻譯(ID {$existing}),跳過建立新文章。";
// 做個語言 debug
$lang_debug = [];
foreach ( $translations as $code => $pid ) {
$lang_debug[] = $code . ':' . $pid . '(' . pll_get_post_language( $pid ) . ')';
}
$report[] = '🧪 目前語言狀態:' . implode( ', ', $lang_debug );
return $report;
}
// Step 2) 使用 wp_insert_post 建立新文章(官方建議做法)
$new_id = wp_insert_post( [
'post_type' => $source_post->post_type,
'post_status' => $source_post->post_status,
'post_title' => $source_post->post_title,
'post_content' => $source_post->post_content,
'post_excerpt' => $source_post->post_excerpt,
'post_author' => $source_post->post_author,
'post_date' => $source_post->post_date,
'post_date_gmt' => $source_post->post_date_gmt,
'menu_order' => $source_post->menu_order,
], true );
if ( is_wp_error( $new_id ) ) {
$report[] = "❌ wp_insert_post 錯誤:" . $new_id->get_error_message();
return $report;
}
$report[] = "Step 2) 建立新文章:{$new_id}";
// Step 3) 官方建議:先設定新文章語言
ir_set_lang_official( $new_id, $target_lang );
$report[] = "Step 3) 設定新文章語言:{$new_id} → {$target_lang}";
// Step 4) 官方建議:建立翻譯關聯(保留既有語言)
$translations[ $target_lang ] = $new_id;
pll_save_post_translations( $translations );
$report[] = "Step 4) 儲存翻譯關聯:" . json_encode( $translations );
// Step 5) 複製 taxonomy(排除語言 taxonomy,避免覆寫語言)
$taxes = get_object_taxonomies( IR_POST_TYPE );
$taxes = array_diff( $taxes, [ 'language', 'pll_language' ] );
foreach ( $taxes as $tax ) {
$terms = wp_get_object_terms( $source_id, $tax, [ 'fields' => 'ids' ] );
if ( ! is_wp_error( $terms ) ) {
wp_set_object_terms( $new_id, $terms, $tax, false );
}
}
// Step 6) 複製特色圖片
$thumb = get_post_thumbnail_id( $source_id );
if ( $thumb ) {
set_post_thumbnail( $new_id, $thumb );
}
// Step 7) 複製 ACF 欄位(包含上傳檔案)
if ( function_exists( 'get_field_objects' ) && function_exists( 'update_field' ) ) {
$fields = get_field_objects( $source_id );
if ( $fields ) {
foreach ( $fields as $field ) {
update_field( $field['key'], $field['value'], $new_id );
}
}
}
// 最後:實際語言檢查
$src_lang_real = pll_get_post_language( $source_id );
$new_lang_real = pll_get_post_language( $new_id );
$report[] = "🧪 實際語言檢查:原文 {$source_id} 語言:{$src_lang_real};新文 {$new_id} 語言:{$new_lang_real}";
$report[] = "✅ 完成:原文 {$source_id} → {$target_lang} 翻譯 {$new_id}";
return $report;
}