首先要去创建获取的路由(get)和写入的路由(post)
这个目前针对文章和产品文章分类翻译有效,页面没配置过,页面需要涉及到一个block的翻译的问题
class Polylang_REST_API_Extension
{
private $namespace = 'polylang-api/v1';
public function __construct()
{
add_action('rest_api_init', array($this, 'register_routes'));
}
/**
* Register all REST API routes
*/
public function register_routes()
{
// Get all translatable post types
register_rest_route($this->namespace, '/post-types', array(
'methods' => 'GET',
'callback' => array($this, 'get_post_types'),
'permission_callback' => array($this, 'check_permission'),
));
// Get untranslated posts
register_rest_route($this->namespace, '/untranslated-posts', array(
'methods' => 'GET',
'callback' => array($this, 'get_untranslated_posts'),
'permission_callback' => array($this, 'check_permission'),
'args' => array(
'post_type' => array(
'default' => 'post',
'sanitize_callback' => 'sanitize_text_field',
),
'source_lang' => array(
'default' => 'en',
'sanitize_callback' => 'sanitize_text_field',
),
'target_lang' => array(
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
),
));
// Get post content for translation
register_rest_route($this->namespace, '/post/(?P<id>\d+)', array(
'methods' => 'GET',
'callback' => array($this, 'get_post_content'),
'permission_callback' => array($this, 'check_permission'),
'args' => array(
'id' => array(
'validate_callback' => function ($param) {
return is_numeric($param);
}
),
),
));
// Create translated post
register_rest_route($this->namespace, '/translate-post', array(
'methods' => 'POST',
'callback' => array($this, 'create_translated_post'),
'permission_callback' => array($this, 'check_permission'),
));
// Get languages
register_rest_route($this->namespace, '/languages', array(
'methods' => 'GET',
'callback' => array($this, 'get_languages'),
'permission_callback' => array($this, 'check_permission'),
));
// Get translation status
register_rest_route($this->namespace, '/translation-status/(?P<id>\d+)', array(
'methods' => 'GET',
'callback' => array($this, 'get_translation_status'),
'permission_callback' => array($this, 'check_permission'),
));
}
/**
* Permission callback
*/
public function check_permission()
{
return current_user_can('edit_posts');
}
/**
* Get all public post types that support translation
*/
public function get_post_types()
{
if (!function_exists('pll_languages_list')) {
return new WP_Error('polylang_not_active', 'Polylang is not active', array('status' => 500));
}
$post_types = get_post_types(array(
'public' => true,
), 'objects');
unset($post_types['attachment'], $post_types['video']);
$translatable_types = array();
foreach ($post_types as $post_type) {
if (function_exists('pll_is_translated_post_type') && pll_is_translated_post_type($post_type->name)) {
$translatable_types[] = array(
'name' => $post_type->name,
'label' => $post_type->label,
'singular_name' => $post_type->labels->singular_name,
);
}
}
return rest_ensure_response(array(
'success' => true,
'post_types' => array_column($translatable_types, 'name'),
'post_types_details' => $translatable_types,
));
}
/**
* Get all untranslated posts for a specific language
*/
public function get_untranslated_posts($request)
{
if (!function_exists('pll_languages_list')) {
return new WP_Error('polylang_not_active', 'Polylang is not active', array('status' => 500));
}
$post_type = $request->get_param('post_type');
$source_lang = $request->get_param('source_lang');
$target_lang = $request->get_param('target_lang');
$args = array(
'post_type' => $post_type,
'posts_per_page' => -1,
'lang' => $source_lang,
'post_status' => 'publish',
);
$posts = get_posts($args);
$untranslated = array();
foreach ($posts as $post) {
$translation_id = pll_get_post($post->ID, $target_lang);
if (!$translation_id) {
$untranslated[] = array(
'id' => $post->ID,
'title' => $post->post_title,
'slug' => $post->post_name,
'status' => $post->post_status,
'date' => $post->post_date,
'modified' => $post->post_modified,
'post_type' => $post->post_type,
'language' => $source_lang,
);
}
}
return rest_ensure_response(array(
'success' => true,
'count' => count($untranslated),
'posts' => $untranslated,
));
}
/**
* Get full post content including all meta fields
*/
public function get_post_content($request)
{
$post_id = $request->get_param('id');
$post = get_post($post_id);
if (!$post) {
return new WP_Error('post_not_found', 'Post not found', array('status' => 404));
}
$post_meta = get_post_meta($post_id);
$clean_meta = array();
foreach ($post_meta as $key => $value) {
if (substr($key, 0, 1) === '_')
continue;
$meta_value = is_array($value) ? $value[0] : $value;
if (!is_string($meta_value) || $this->is_serialized($meta_value))
continue;
$clean_meta[$key] = $meta_value;
}
$categories = wp_get_post_categories($post_id, array('fields' => 'all'));
$tags = wp_get_post_tags($post_id, array('fields' => 'all'));
$custom_taxonomies = array();
$taxonomies = get_object_taxonomies($post->post_type, 'objects');
foreach ($taxonomies as $taxonomy) {
if (!in_array($taxonomy->name, array('category', 'post_tag', 'language', 'post_translations'))) {
$terms = wp_get_post_terms($post_id, $taxonomy->name);
if (!is_wp_error($terms) && !empty($terms)) {
$custom_taxonomies[$taxonomy->name] = $terms;
}
}
}
$thumbnail_id = get_post_thumbnail_id($post_id);
$thumbnail_url = $thumbnail_id ? wp_get_attachment_url($thumbnail_id) : null;
$acf_fields = array();
if (function_exists('get_fields')) {
$acf_fields = get_fields($post_id) ?: array();
}
$current_lang = pll_get_post_language($post_id);
return rest_ensure_response(array(
'success' => true,
'post' => array(
'id' => $post->ID,
'title' => $post->post_title,
'content' => $post->post_content,
'excerpt' => $post->post_excerpt,
'slug' => $post->post_name,
'status' => $post->post_status,
'type' => $post->post_type,
'language' => $current_lang,
'featured_image' => $thumbnail_url,
'thumbnail_id' => $thumbnail_id,
'categories' => $categories,
'tags' => $tags,
'custom_taxonomies' => $custom_taxonomies,
'meta' => $clean_meta,
'acf_fields' => $acf_fields,
),
));
}
/**
* Create or update translated post - FIXED VERSION
* Now properly preserves all translation relationships
*/
public function create_translated_post($request)
{
$params = $request->get_json_params();
if (!isset($params['source_post_id']) || !isset($params['target_lang'])) {
return new WP_Error('missing_params', 'Missing required parameters', array('status' => 400));
}
$source_post_id = intval($params['source_post_id']);
$target_lang = sanitize_text_field($params['target_lang']);
$translated_title = sanitize_text_field($params['title']);
$translated_content = wp_kses_post($params['content']);
$translated_excerpt = isset($params['excerpt']) ? sanitize_textarea_field($params['excerpt']) : '';
$source_post = get_post($source_post_id);
if (!$source_post) {
return new WP_Error('post_not_found', 'Source post not found', array('status' => 404));
}
// Check if translation exists
$existing_translation = pll_get_post($source_post_id, $target_lang);
if ($existing_translation) {
// Update existing translation
$updated_post = array(
'ID' => $existing_translation,
'post_title' => $translated_title,
'post_content' => $translated_content,
'post_excerpt' => $translated_excerpt,
);
$result = wp_update_post($updated_post, true);
if (is_wp_error($result)) {
return $result;
}
$translated_post_id = $existing_translation;
$action = 'updated';
} else {
// Create new translation
$new_post = array(
'post_title' => $translated_title,
'post_content' => $translated_content,
'post_excerpt' => $translated_excerpt,
'post_status' => $source_post->post_status,
'post_type' => $source_post->post_type,
'post_author' => $source_post->post_author,
);
$translated_post_id = wp_insert_post($new_post, true);
if (is_wp_error($translated_post_id)) {
return $translated_post_id;
}
// Set language for new post
pll_set_post_language($translated_post_id, $target_lang);
// ⭐ CRITICAL FIX: Get ALL existing translations first
$existing_translations = pll_get_post_translations($source_post_id);
// Add the new translation to the existing group
$existing_translations[$target_lang] = $translated_post_id;
// Save the complete translation group
pll_save_post_translations($existing_translations);
$action = 'created';
}
// Copy or set featured image
if (isset($params['thumbnail_id']) && $params['thumbnail_id']) {
set_post_thumbnail($translated_post_id, intval($params['thumbnail_id']));
} else {
$thumbnail_id = get_post_thumbnail_id($source_post_id);
if ($thumbnail_id) {
set_post_thumbnail($translated_post_id, $thumbnail_id);
}
}
// Handle categories
if (isset($params['categories']) && is_array($params['categories'])) {
$translated_category_ids = array();
foreach ($params['categories'] as $category_data) {
$cat_id = $this->translate_term($category_data, 'category', $target_lang);
if ($cat_id) {
$translated_category_ids[] = $cat_id;
}
}
wp_set_post_categories($translated_post_id, $translated_category_ids);
}
// Handle tags
if (isset($params['tags']) && is_array($params['tags'])) {
$translated_tag_ids = array();
foreach ($params['tags'] as $tag_data) {
$tag_id = $this->translate_term($tag_data, 'post_tag', $target_lang);
if ($tag_id) {
$translated_tag_ids[] = $tag_id;
}
}
wp_set_post_tags($translated_post_id, $translated_tag_ids);
}
// Handle ACF fields
if (isset($params['acf_fields']) && function_exists('update_field')) {
foreach ($params['acf_fields'] as $field_key => $field_value) {
update_field($field_key, $field_value, $translated_post_id);
}
}
// Handle custom meta fields
if (isset($params['meta_fields']) && is_array($params['meta_fields'])) {
foreach ($params['meta_fields'] as $meta_key => $meta_value) {
if (substr($meta_key, 0, 1) === '_')
continue;
update_post_meta($translated_post_id, $meta_key, sanitize_text_field($meta_value));
}
}
// Get final translation status for verification
$final_translations = pll_get_post_translations($source_post_id);
return rest_ensure_response(array(
'success' => true,
'action' => $action,
'translated_post_id' => $translated_post_id,
'source_post_id' => $source_post_id,
'post_type' => $source_post->post_type,
'target_language' => $target_lang,
'url' => get_permalink($translated_post_id),
'all_translations' => $final_translations, // Shows all connected translations
));
}
/**
* Helper to translate taxonomy terms
*/
private function translate_term($term_data, $taxonomy, $target_lang)
{
$source_term_id = isset($term_data['term_id']) ? intval($term_data['term_id']) : 0;
$translated_name = isset($term_data['translated_name']) ? sanitize_text_field($term_data['translated_name']) : '';
if ($source_term_id && function_exists('pll_get_term')) {
$existing_translation = pll_get_term($source_term_id, $target_lang);
if ($existing_translation) {
return $existing_translation;
}
}
if ($translated_name) {
$new_term = wp_insert_term($translated_name, $taxonomy);
if (!is_wp_error($new_term)) {
$new_term_id = $new_term['term_id'];
if (function_exists('pll_set_term_language')) {
pll_set_term_language($new_term_id, $target_lang);
if ($source_term_id) {
// ⭐ SAME FIX for terms: preserve existing translations
$existing_term_translations = pll_get_term_translations($source_term_id);
$existing_term_translations[$target_lang] = $new_term_id;
pll_save_term_translations($existing_term_translations);
}
}
return $new_term_id;
}
}
return null;
}
/**
* Get all configured languages
*/
public function get_languages()
{
if (!function_exists('pll_languages_list')) {
return new WP_Error('polylang_not_active', 'Polylang is not active', array('status' => 500));
}
$languages = pll_languages_list(array('fields' => 'all'));
$lang_data = array();
foreach ($languages as $lang) {
$lang_data[] = array(
'slug' => $lang->slug,
'name' => $lang->name,
'locale' => $lang->locale,
'is_rtl' => $lang->is_rtl,
'flag' => $lang->flag,
);
}
return rest_ensure_response(array(
'success' => true,
'languages' => $lang_data,
));
}
/**
* Get translation status for a specific post
*/
public function get_translation_status($request)
{
$post_id = $request->get_param('id');
if (!function_exists('pll_get_post_translations')) {
return new WP_Error('polylang_not_active', 'Polylang is not active', array('status' => 500));
}
$translations = pll_get_post_translations($post_id);
$languages = pll_languages_list();
$status = array();
foreach ($languages as $lang) {
$status[$lang] = isset($translations[$lang]) ? array(
'exists' => true,
'post_id' => $translations[$lang],
'url' => get_permalink($translations[$lang]),
) : array(
'exists' => false,
'post_id' => null,
'url' => null,
);
}
return rest_ensure_response(array(
'success' => true,
'post_id' => $post_id,
'translations' => $status,
));
}
/**
* Check if a string is serialized
*/
private function is_serialized($data)
{
if (!is_string($data)) {
return false;
}
$data = trim($data);
if ($data === 'N;') {
return true;
}
if (strlen($data) < 4 || $data[1] !== ':') {
return false;
}
return @unserialize($data) !== false;
}
}
// Initialize the plugin
new Polylang_REST_API_Extension();
翻译文章时候需要在rest api创建文章时候同步更新一样的文章缩略图和字段方法
/**
* Sync featured image and gallery meta when post is created/updated via REST API
*/
function sync_media_on_rest_insert($post, $request, $creating)
{
// Check if Polylang is active
if (!function_exists('pll_get_post_translations')) {
return;
}
// Get the post ID
$post_id = $post->ID;
// Check if this post has translations
$translations = pll_get_post_translations($post_id);
if (empty($translations) || count($translations) <= 1) {
return; // No translations to sync
}
// Sync Featured Image
$thumbnail_id = get_post_thumbnail_id($post_id);
if ($thumbnail_id) {
foreach ($translations as $lang_code => $translated_post_id) {
if ($translated_post_id == $post_id) {
continue;
}
set_post_thumbnail($translated_post_id, $thumbnail_id);
}
}
// Sync Gallery Meta Field (adjust meta key as needed)
$gallery_meta_keys = array('product_gallery'); // Add your meta keys
foreach ($gallery_meta_keys as $meta_key) {
$gallery_value = get_post_meta($post_id, $meta_key, true);
if (!empty($gallery_value)) {
foreach ($translations as $lang_code => $translated_post_id) {
if ($translated_post_id == $post_id) {
continue;
}
update_post_meta($translated_post_id, $meta_key, $gallery_value);
}
}
}
}
// Hook into REST API post insert/update for all post types
add_action('rest_after_insert_post', 'sync_media_on_rest_insert', 10, 3);
add_action('rest_after_insert_page', 'sync_media_on_rest_insert', 10, 3);
add_action('rest_after_insert_product', 'sync_media_on_rest_insert', 10, 3);
// For custom post types, add specific hooks
// add_action('rest_after_insert_YOUR_POST_TYPE', 'sync_media_on_rest_insert', 10, 3);
另外一种注册路由的方法
add_action('rest_api_init', function () {
// Your existing endpoint
register_rest_route('polylang-api/v1', '/translate-post', array(
'methods' => 'POST',
'callback' => 'create_polylang_translation',
'permission_callback' => function() {
return current_user_can('edit_posts');
}
));
register_rest_route('polylang-api/v1', '/check-translation/(?P<id>\d+)', array(
'methods' => 'GET',
'callback' => function($request) {
$post_id = intval($request['id']);
return array(
'post_id' => $post_id,
'language' => pll_get_post_language($post_id),
'translations' => pll_get_post_translations($post_id),
'all_languages' => pll_languages_list()
);
},
'permission_callback' => '__return_true'
));
});
function create_polylang_translation($request) {
$params = $request->get_json_params();
$source_post_id = intval($params['source_post_id']);
$target_lang = sanitize_text_field($params['target_lang']);
$title = $params['title'];
$content = $params['content'];
$excerpt = $params['excerpt'] ?? '';
// Validate source post exists
$source_post = get_post($source_post_id);
if (!$source_post) {
return new WP_Error('invalid_post', 'Source post not found', array('status' => 404));
}
// Get source language
$source_lang = pll_get_post_language($source_post_id);
// Check if translation already exists
$existing_translation_id = pll_get_post($source_post_id, $target_lang);
$post_data = array(
'post_title' => $title,
'post_content' => $content,
'post_excerpt' => $excerpt,
'post_status' => $source_post->post_status, // Match source status
'post_type' => $source_post->post_type,
'post_parent' => 0, // Will link via Polylang
'menu_order' => $source_post->menu_order,
);
if ($existing_translation_id) {
// Update existing translation
$post_data['ID'] = $existing_translation_id;
$translated_post_id = wp_update_post($post_data, true);
if (is_wp_error($translated_post_id)) {
return $translated_post_id;
}
$message = 'Translation updated';
} else {
// Create new translation
$translated_post_id = wp_insert_post($post_data, true);
if (is_wp_error($translated_post_id)) {
return $translated_post_id;
}
// CRITICAL: Set the language for the new post
pll_set_post_language($translated_post_id, $target_lang);
// CRITICAL: Link the translation to the source post
$translations = pll_get_post_translations($source_post_id);
$translations[$target_lang] = $translated_post_id;
pll_save_post_translations($translations);
$message = 'Translation created and linked';
}
// Copy thumbnail
if (!empty($params['thumbnail_id'])) {
$thumbnail_id = intval($params['thumbnail_id']);
set_post_thumbnail($translated_post_id, $thumbnail_id);
}
// Handle categories
if (!empty($params['categories']) && is_array($params['categories'])) {
$category_ids = array();
foreach ($params['categories'] as $cat) {
$term_id = intval($cat['term_id']);
// Get translated category if exists
$translated_cat = pll_get_term($term_id, $target_lang);
if ($translated_cat) {
$category_ids[] = $translated_cat;
} else {
// If no translation exists, create one or use original
$category_ids[] = $term_id;
}
}
wp_set_post_categories($translated_post_id, $category_ids);
}
// Handle tags
if (!empty($params['tags']) && is_array($params['tags'])) {
$tag_ids = array();
foreach ($params['tags'] as $tag) {
$term_id = intval($tag['term_id']);
$translated_tag = pll_get_term($term_id, $target_lang);
if ($translated_tag) {
$tag_ids[] = $translated_tag;
} else {
$tag_ids[] = $term_id;
}
}
wp_set_post_tags($translated_post_id, $tag_ids);
}
// Handle ACF fields
if (!empty($params['acf_fields']) && is_array($params['acf_fields'])) {
foreach ($params['acf_fields'] as $field_key => $field_value) {
update_field($field_key, $field_value, $translated_post_id);
}
}
// Handle meta fields
if (!empty($params['meta_fields']) && is_array($params['meta_fields'])) {
foreach ($params['meta_fields'] as $meta_key => $meta_value) {
update_post_meta($translated_post_id, $meta_key, $meta_value);
}
}
return array(
'success' => true,
'post_id' => $translated_post_id,
'source_post_id' => $source_post_id,
'target_lang' => $target_lang,
'source_lang' => $source_lang,
'message' => $message,
'translations' => pll_get_post_translations($source_post_id) // Return all linked translations
);
}
翻译的时候需要考虑,第一,要翻译全部文章,自定义文章类型,页面,第二,要翻译页面里的block, 第三,要翻译分类,自定义分类,并进行链接文章
{
"name": "wp-auto-translate-all-post-types",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "weeks"
}
]
}
},
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [
400,
560
],
"id": "59704616-352e-41de-a588-bf594674d6cb"
},
{
"parameters": {
"url": "=https://banwork.cn/wp-json/polylang-api/v1/post-types",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"options": {}
},
"name": "Get All Post Types",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
592,
560
],
"id": "1f9c6d8a-eb18-4d43-bfff-d7b10704032c",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"jsCode": "// Get post types from previous node\nconst postTypesData = $input.first().json;\nconst postTypes = postTypesData.post_types || [];\n\n// Define language configuration\nconst config = {\n post_types: postTypes,\n target_languages: ['zh'], // Add more languages as needed: ['zh', 'es', 'fr']\n source_lang: 'en'\n};\n\nreturn {\n json: config\n};"
},
"name": "Set Language Config",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
800,
560
],
"id": "c8cf8dab-f866-41d4-814b-0ffdd70b6f83"
},
{
"parameters": {
"fieldToSplitOut": "post_types",
"options": {}
},
"name": "Split Post Types",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
992,
560
],
"id": "5c047ae5-e3fc-4413-9f50-90ebf907e169"
},
{
"parameters": {
"fieldToSplitOut": "target_languages",
"options": {}
},
"name": "Split Target Languages",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
1200,
560
],
"id": "7529fb8e-b056-4b0f-98d2-831428deb1a4"
},
{
"parameters": {
"url": "=https://fuckthe.work/wp-json/polylang-api/v1/untranslated-posts",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "post_type",
"value": "={{ $('Split Post Types').item.json.post_types }}"
},
{
"name": "source_lang",
"value": "={{ $('Set Language Config').item.json.source_lang }}"
},
{
"name": "target_lang",
"value": "={{ $json.target_languages }}"
}
]
},
"options": {}
},
"name": "Get Untranslated Posts",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
1408,
560
],
"id": "46f1a4b8-b8de-4cca-bb40-95a563fd7b0a",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "has-posts",
"leftValue": "={{ $json.count }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
],
"combinator": "and"
},
"options": {}
},
"name": "Has Untranslated Posts?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1616,
560
],
"id": "fe7587ca-35dd-47ed-ace0-c554e90d6d24"
},
{
"parameters": {
"fieldToSplitOut": "posts",
"options": {}
},
"name": "Split Posts",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
1824,
448
],
"id": "8d2e78b5-ea1f-4e64-a8a5-dfb217797868"
},
{
"parameters": {
"url": "=https://fuckthe.work/wp-json/polylang-api/v1/post/{{ $json.id }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"options": {}
},
"name": "Get Post Content",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
2032,
448
],
"id": "28e411ff-2159-4cb2-ba0d-ff523a0cd407",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"jsCode": "function stripHtml(html) {\n if (!html) return '';\n \n let text = html\n .replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, '')\n .replace(/<style\\b[^<]*(?:(?!<\\/style>)<[^<]*)*<\\/style>/gi, '');\n \n text = text.replace(/<[^>]+>/g, ' ');\n text = text.replace(/\\s+/g, ' ').trim();\n \n return text;\n}\n\n// Process translatable meta fields\nfunction extractTranslatableMeta(metaFields, postType) {\n const translatable = {};\n \n if (!metaFields || typeof metaFields !== 'object') return translatable;\n \n // Extract text-based meta fields (skip internal WordPress fields)\n for (const [key, value] of Object.entries(metaFields)) {\n // Skip internal WP fields and serialized data\n if (key.startsWith('_') || !value) continue;\n \n // Handle arrays (take first value)\n const metaValue = Array.isArray(value) ? value[0] : value;\n \n // Only include string values\n if (typeof metaValue === 'string' && metaValue.length > 0) {\n translatable[key] = stripHtml(metaValue);\n }\n }\n \n return translatable;\n}\n\nconst items = $input.all();\n\nreturn items.map((item, index) => {\n const postData = item.json.post || item.json;\n \n return {\n json: {\n id: item.json.id || postData.id,\n post: {\n id: postData.id,\n post_type: postData.type,\n title: stripHtml(postData.title),\n content: stripHtml(postData.content),\n excerpt: stripHtml(postData.excerpt || ''),\n categories: postData.categories || [],\n tags: postData.tags || [],\n acf_fields: postData.acf_fields || {},\n meta_fields: extractTranslatableMeta(postData.meta, postData.type),\n thumbnail_id: postData.thumbnail_id || null\n }\n },\n pairedItem: index\n };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2240,
448
],
"id": "61eeb731-e6d1-48b1-af60-1827f80f9e33",
"name": "Process Post Data"
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst sourceLang = $('Set Language Config').item.json.source_lang;\nconst targetLang = $('Split Target Languages').item.json.target_languages;\n\nreturn items.map((item, index) => {\n const postData = item.json.post;\n \n // Build content for translation including meta fields\n let translationContent = `Title: ${postData.title}\\n\\nContent: ${postData.content}`;\n \n if (postData.excerpt) {\n translationContent += `\\n\\nExcerpt: ${postData.excerpt}`;\n }\n \n // Add meta fields to translation\n const metaFieldsArray = [];\n if (postData.meta_fields && Object.keys(postData.meta_fields).length > 0) {\n translationContent += '\\n\\nMeta Fields:';\n for (const [key, value] of Object.entries(postData.meta_fields)) {\n translationContent += `\\n${key}: ${value}`;\n metaFieldsArray.push({ key, value });\n }\n }\n \n // Build expected JSON structure\n let expectedStructure = '\"title\": \"translated title\", \"content\": \"translated content\"';\n if (postData.excerpt) {\n expectedStructure += ', \"excerpt\": \"translated excerpt\"';\n }\n if (metaFieldsArray.length > 0) {\n expectedStructure += ', \"meta_fields\": {\"field_key\": \"translated value\"}';\n }\n \n const requestBody = {\n model: \"deepseek-chat\",\n messages: [\n {\n role: \"system\",\n content: `You are a professional translator. Translate the following WordPress ${postData.post_type} content from ${sourceLang} to ${targetLang}. Return ONLY a valid JSON object with these exact keys: {${expectedStructure}}. For meta_fields, use the same keys but translate the values.`\n },\n {\n role: \"user\",\n content: translationContent\n }\n ],\n temperature: 0.3,\n response_format: { type: \"json_object\" }\n };\n \n return {\n json: {\n requestBody: requestBody,\n post: postData,\n metaFieldsKeys: metaFieldsArray.map(m => m.key)\n },\n pairedItem: index\n };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2448,
448
],
"id": "8b218b25-ced3-465d-a9bd-1f7d00eb7247",
"name": "Build DeepSeek Request"
},
{
"parameters": {
"method": "POST",
"url": "https://api.deepseek.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.requestBody }}",
"options": {}
},
"name": "DeepSeek Translate",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
2656,
448
],
"id": "545d1efa-23f4-499b-a091-e04d79ae3037",
"credentials": {
"httpHeaderAuth": {
"id": "5X9OcogB9LTTIp0J",
"name": "Header Auth account"
}
}
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst postContentItems = $('Get Post Content').all();\nconst targetLang = $('Split Target Languages').item.json.target_languages;\n\nreturn items.map((item, index) => {\n const response = item.json;\n const translatedData = JSON.parse(response.choices[0].message.content);\n \n const originalPostItem = postContentItems[index].json;\n const originalPost = originalPostItem.post || originalPostItem;\n \n const categories = (originalPost.categories || []).map(cat => ({\n term_id: cat.term_id,\n translated_name: cat.name\n }));\n \n const tags = (originalPost.tags || []).map(tag => ({\n term_id: tag.term_id,\n translated_name: tag.name\n }));\n \n let acfFields = {};\n if (originalPost.acf_fields && typeof originalPost.acf_fields === 'object') {\n acfFields = originalPost.acf_fields;\n }\n \n // Extract translated meta fields\n let translatedMeta = {};\n if (translatedData.meta_fields && typeof translatedData.meta_fields === 'object') {\n translatedMeta = translatedData.meta_fields;\n }\n \n return {\n json: {\n source_post_id: originalPost.id,\n post_type: originalPost.type,\n target_lang: targetLang,\n title: translatedData.title,\n content: translatedData.content,\n excerpt: translatedData.excerpt || '',\n categories: categories,\n tags: tags,\n acf_fields: acfFields,\n meta_fields: translatedMeta,\n thumbnail_id: originalPost.thumbnail_id\n },\n pairedItem: index\n };\n});"
},
"name": "Format Translation Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2864,
448
],
"id": "192db91b-5c62-4400-a727-640bced0c904"
},
{
"parameters": {
"method": "POST",
"url": "=https://fuckthe.work/wp-json/polylang-api/v1/translate-post",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"source_post_id\": {{ $json.source_post_id }},\n \"target_lang\": \"{{ $json.target_lang }}\",\n \"title\": {{ JSON.stringify($json.title) }},\n \"content\": {{ JSON.stringify($json.content) }},\n \"excerpt\": {{ JSON.stringify($json.excerpt) }},\n \"categories\": {{ JSON.stringify($json.categories) }},\n \"tags\": {{ JSON.stringify($json.tags) }},\n \"acf_fields\": {{ JSON.stringify($json.acf_fields) }},\n \"meta_fields\": {{ JSON.stringify($json.meta_fields) }},\n \"thumbnail_id\": {{ $json.thumbnail_id }}\n}",
"options": {}
},
"name": "Create Translation",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
3072,
448
],
"id": "88d88b3e-1255-4764-a1be-67c7ab3f4c02",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"amount": 2
},
"name": "Rate Limit Delay",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
3280,
448
],
"id": "28000c82-342e-4cf5-b9c9-64756756bb40",
"webhookId": "rate-limit-delay"
},
{
"parameters": {
"options": {}
},
"name": "Log Success",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
3472,
448
],
"id": "616ccc77-6d2f-4b44-af82-27b2778db74f"
},
{
"parameters": {
"options": {}
},
"name": "Workflow Complete",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
3680,
560
],
"id": "f46d3075-dea5-4036-8176-98fe6de463f4"
}
],
"pinData": {},
"connections": {
"Schedule Trigger": {
"main": [
[
{
"node": "Get All Post Types",
"type": "main",
"index": 0
}
]
]
},
"Get All Post Types": {
"main": [
[
{
"node": "Set Language Config",
"type": "main",
"index": 0
}
]
]
},
"Set Language Config": {
"main": [
[
{
"node": "Split Post Types",
"type": "main",
"index": 0
}
]
]
},
"Split Post Types": {
"main": [
[
{
"node": "Split Target Languages",
"type": "main",
"index": 0
}
]
]
},
"Split Target Languages": {
"main": [
[
{
"node": "Get Untranslated Posts",
"type": "main",
"index": 0
}
]
]
},
"Get Untranslated Posts": {
"main": [
[
{
"node": "Has Untranslated Posts?",
"type": "main",
"index": 0
}
]
]
},
"Has Untranslated Posts?": {
"main": [
[
{
"node": "Split Posts",
"type": "main",
"index": 0
}
],
[
{
"node": "Workflow Complete",
"type": "main",
"index": 0
}
]
]
},
"Split Posts": {
"main": [
[
{
"node": "Get Post Content",
"type": "main",
"index": 0
}
]
]
},
"Get Post Content": {
"main": [
[
{
"node": "Process Post Data",
"type": "main",
"index": 0
}
]
]
},
"Process Post Data": {
"main": [
[
{
"node": "Build DeepSeek Request",
"type": "main",
"index": 0
}
]
]
},
"Build DeepSeek Request": {
"main": [
[
{
"node": "DeepSeek Translate",
"type": "main",
"index": 0
}
]
]
},
"DeepSeek Translate": {
"main": [
[
{
"node": "Format Translation Data",
"type": "main",
"index": 0
}
]
]
},
"Format Translation Data": {
"main": [
[
{
"node": "Create Translation",
"type": "main",
"index": 0
}
]
]
},
"Create Translation": {
"main": [
[
{
"node": "Rate Limit Delay",
"type": "main",
"index": 0
}
]
]
},
"Rate Limit Delay": {
"main": [
[
{
"node": "Log Success",
"type": "main",
"index": 0
}
]
]
},
"Log Success": {
"main": [
[
{
"node": "Workflow Complete",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"availableInMCP": false
},
"versionId": "1c21db38-c2cd-4127-9419-9e9cdbca9b5b",
"meta": {
"instanceId": "e116f0cdc2af85ea765f95e160256c7af2464258f43bb537f78b1a25e7dab4f3"
},
"id": "PH4nhu4nqMun1_KCNC23O",
"tags": []
}
{
"name": "如果文章超长版这个适用但是多语言没测试过只测试翻译一种wp-auto-translate-all-post-types",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 6
}
]
}
},
"name": "Schedule Trigger1",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [
672,
448
],
"id": "0e1b246e-44f2-4513-ae03-fb6368c24496"
},
{
"parameters": {
"url": "=https://banwork.cn/wp-json/polylang-api/v1/post-types",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"options": {}
},
"name": "Get All Post Types1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
864,
448
],
"id": "27b8f772-e94a-4441-a3e8-dd33ed8a38b2",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"fieldToSplitOut": "post_types",
"include": "selectedOtherFields",
"fieldsToInclude": "target_languages",
"options": {}
},
"name": "Split Post Types1",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
1264,
448
],
"id": "5a162a60-5a37-4b66-be6f-8e64559bf429"
},
{
"parameters": {
"fieldToSplitOut": "target_languages",
"options": {}
},
"name": "Split Target Languages1",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
1472,
448
],
"id": "ce33536c-0199-417a-9a84-0e631c285fb1"
},
{
"parameters": {
"url": "=https://banwork.cn/wp-json/polylang-api/v1/untranslated-posts",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "post_type",
"value": "={{ $('Split Post Types1').item.json.post_types }}"
},
{
"name": "source_lang",
"value": "={{ $('Set Language Config1').item.json.source_lang }}"
},
{
"name": "target_lang",
"value": "={{ $json.target_languages }}"
}
]
},
"options": {}
},
"name": "Get Untranslated Posts1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
1680,
448
],
"id": "91a76fe3-9915-40fd-9142-c655f5a7c7c7",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "has-posts",
"leftValue": "={{ $json.count }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
],
"combinator": "and"
},
"options": {}
},
"name": "Has Untranslated Posts?1",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1872,
464
],
"id": "25ac9872-9f6b-4c97-9db9-66054b9d797b"
},
{
"parameters": {
"fieldToSplitOut": "posts",
"options": {}
},
"name": "Split Posts1",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
2096,
336
],
"id": "6e7ad1b8-1cd8-4cda-8d66-ad30f4a2a3a1"
},
{
"parameters": {
"url": "=https://banwork.cn/wp-json/polylang-api/v1/post/{{ $json.id }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"options": {}
},
"name": "Get Post Content1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
2304,
336
],
"id": "bb12daca-9142-4dfb-aa88-ea52f09fa093",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"jsCode": "function stripHtml(html) {\n if (!html) return '';\n \n let text = html\n .replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, '')\n .replace(/<style\\b[^<]*(?:(?!<\\/style>)<[^<]*)*<\\/style>/gi, '');\n \n text = text.replace(/<[^>]+>/g, ' ');\n text = text.replace(/\\s+/g, ' ').trim();\n \n return text;\n}\n\n// Process translatable meta fields\nfunction extractTranslatableMeta(metaFields, postType) {\n const translatable = {};\n \n if (!metaFields || typeof metaFields !== 'object') return translatable;\n \n // Extract text-based meta fields (skip internal WordPress fields)\n for (const [key, value] of Object.entries(metaFields)) {\n // Skip internal WP fields and serialized data\n if (key.startsWith('_') || !value) continue;\n \n // Handle arrays (take first value)\n const metaValue = Array.isArray(value) ? value[0] : value;\n \n // Only include string values\n if (typeof metaValue === 'string' && metaValue.length > 0) {\n translatable[key] = stripHtml(metaValue);\n }\n }\n \n return translatable;\n}\n\nconst items = $input.all();\n\nreturn items.map((item, index) => {\n const postData = item.json.post || item.json;\n \n return {\n json: {\n id: item.json.id || postData.id,\n post: {\n id: postData.id,\n post_type: postData.type,\n title: stripHtml(postData.title),\n content: stripHtml(postData.content),\n excerpt: stripHtml(postData.excerpt || ''),\n categories: postData.categories || [],\n tags: postData.tags || [],\n acf_fields: postData.acf_fields || {},\n meta_fields: extractTranslatableMeta(postData.meta, postData.type),\n thumbnail_id: postData.thumbnail_id || null\n }\n },\n pairedItem: index\n };\n});\n\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2512,
336
],
"id": "079ca47d-f7b4-4ba2-9878-78133e5053de",
"name": "Process Post Data1"
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst sourceLang = $('Set Language Config1').item.json.source_lang;\nconst targetLang = $('Split Target Languages1').item.json.target_languages;\n\n// Helper function to truncate text\nfunction truncateText(text, maxLength = 8000) {\n if (text.length <= maxLength) return text;\n return text.substring(0, maxLength) + '... [truncated]';\n}\n\nreturn items.map((item, index) => {\n const postData = item.json.post;\n \n // Truncate content if too long\n const title = truncateText(postData.title, 500);\n const content = truncateText(postData.content, 10000);\n const excerpt = postData.excerpt ? truncateText(postData.excerpt, 1000) : '';\n \n // Build content for translation including meta fields\n let translationContent = `Title: ${title}\\n\\nContent: ${content}`;\n \n if (excerpt) {\n translationContent += `\\n\\nExcerpt: ${excerpt}`;\n }\n \n // Add meta fields to translation\n const metaFieldsArray = [];\n if (postData.meta_fields && Object.keys(postData.meta_fields).length > 0) {\n translationContent += '\\n\\nMeta Fields:';\n for (const [key, value] of Object.entries(postData.meta_fields)) {\n const truncatedValue = truncateText(String(value), 500);\n translationContent += `\\n${key}: ${truncatedValue}`;\n metaFieldsArray.push({ key, value: truncatedValue });\n }\n }\n \n // Build expected JSON structure\n let expectedStructure = '\"title\": \"translated title\", \"content\": \"translated content\"';\n if (postData.excerpt) {\n expectedStructure += ', \"excerpt\": \"translated excerpt\"';\n }\n if (metaFieldsArray.length > 0) {\n expectedStructure += ', \"meta_fields\": {\"field_key\": \"translated value\"}';\n }\n \n const requestBody = {\n model: \"deepseek-chat\",\n messages: [\n {\n role: \"system\",\n content: `You are a professional translator. Translate the following WordPress ${postData.post_type} content from ${sourceLang} to ${targetLang}. Return ONLY a valid JSON object with these exact keys: {${expectedStructure}}. For meta_fields, use the same keys but translate the values. IMPORTANT: Ensure all strings in the JSON are properly escaped and the response is valid JSON.`\n },\n {\n role: \"user\",\n content: translationContent\n }\n ],\n temperature: 0.4,\n max_tokens: 8192, // Add max_tokens limit\n response_format: { type: \"json_object\" }\n };\n \n return {\n json: {\n requestBody: requestBody,\n post: postData,\n metaFieldsKeys: metaFieldsArray.map(m => m.key)\n },\n pairedItem: index\n };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2720,
336
],
"id": "6e998ed2-f3e8-45a5-9df2-e127624bad03",
"name": "Build DeepSeek Request1"
},
{
"parameters": {
"method": "POST",
"url": "https://api.deepseek.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.requestBody }}",
"options": {}
},
"name": "DeepSeek Translate1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
2928,
336
],
"id": "765f8a4e-5b45-4995-9c9c-170c62b0fc8e",
"credentials": {
"httpHeaderAuth": {
"id": "5X9OcogB9LTTIp0J",
"name": "Header Auth account"
}
}
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst postContentItems = $('Get Post Content1').all();\nconst targetLang = $('Split Target Languages1').item.json.target_languages;\n\nreturn items.map((item, index) => {\n const response = item.json;\n \n let translatedData;\n try {\n // Get the raw content\n const rawContent = response.choices[0].message.content;\n \n // Try to parse the JSON\n translatedData = JSON.parse(rawContent);\n } catch (parseError) {\n // If JSON parsing fails, try to extract what we can\n const rawContent = response.choices[0].message.content;\n \n // Log the error for debugging\n console.error('Failed to parse translation response:', parseError.message);\n console.error('Raw content:', rawContent.substring(0, 500)); // First 500 chars\n \n // Try to salvage partial data\n translatedData = {\n title: 'Translation failed - JSON parse error',\n content: 'Error: Could not parse translation response',\n excerpt: ''\n };\n \n // Try to extract title if possible\n const titleMatch = rawContent.match(/\"title\"\\s*:\\s*\"([^\"]*)\"/);\n if (titleMatch) {\n translatedData.title = titleMatch[1];\n }\n }\n \n const originalPostItem = postContentItems[index].json;\n const originalPost = originalPostItem.post || originalPostItem;\n \n const categories = (originalPost.categories || []).map(cat => ({\n term_id: cat.term_id,\n translated_name: cat.name\n }));\n \n const tags = (originalPost.tags || []).map(tag => ({\n term_id: tag.term_id,\n translated_name: tag.name\n }));\n \n let acfFields = {};\n if (originalPost.acf_fields && typeof originalPost.acf_fields === 'object') {\n acfFields = originalPost.acf_fields;\n }\n \n // Extract translated meta fields\n let translatedMeta = {};\n if (translatedData.meta_fields && typeof translatedData.meta_fields === 'object') {\n translatedMeta = translatedData.meta_fields;\n }\n \n return {\n json: {\n source_post_id: originalPost.id,\n post_type: originalPost.type,\n target_lang: targetLang,\n title: translatedData.title || originalPost.title,\n content: translatedData.content || originalPost.content,\n excerpt: translatedData.excerpt || '',\n categories: categories,\n tags: tags,\n acf_fields: acfFields,\n meta_fields: translatedMeta,\n thumbnail_id: originalPost.thumbnail_id\n },\n pairedItem: index\n };\n});"
},
"name": "Format Translation Data1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3136,
336
],
"id": "7eb7a33c-060e-4e2f-9d68-7d8173070ab7"
},
{
"parameters": {
"method": "POST",
"url": "=https://banwork.cn/wp-json/polylang-api/v1/translate-post",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"source_post_id\": {{ $json.source_post_id }},\n \"target_lang\": \"{{ $json.target_lang }}\",\n \"title\": {{ JSON.stringify($json.title) }},\n \"content\": {{ JSON.stringify($json.content) }},\n \"excerpt\": {{ JSON.stringify($json.excerpt) }},\n \"categories\": {{ JSON.stringify($json.categories) }},\n \"tags\": {{ JSON.stringify($json.tags) }},\n \"acf_fields\": {{ JSON.stringify($json.acf_fields) }},\n \"meta_fields\": {{ JSON.stringify($json.meta_fields) }},\n \"thumbnail_id\": {{ $json.thumbnail_id }}\n}",
"options": {}
},
"name": "Create Translation1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
3344,
336
],
"id": "a4960f8d-d5ed-4a61-be0a-a2659dc9da79",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"amount": 2
},
"name": "Rate Limit Delay1",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
3552,
336
],
"id": "cd0b2e9f-852d-4eb2-aafc-8473ef02f382",
"webhookId": "rate-limit-delay"
},
{
"parameters": {
"options": {}
},
"name": "Log Success1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
3744,
336
],
"id": "b9a47f7a-cbd4-4db4-87a0-decd1b8396ed"
},
{
"parameters": {
"options": {}
},
"name": "Workflow Complete1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
3952,
448
],
"id": "33ab1d69-8a88-4a58-84fb-0c8b12d82b0a"
},
{
"parameters": {
"jsCode": "const inputData = $input.first().json;\n\nreturn [\n {\n json: {\n post_types: inputData.post_types,\n // Add all your target languages here\n target_languages: [\"fr\"],\n source_lang: \"en\"\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1072,
448
],
"id": "c45d4f72-852f-42a5-ab2b-7e52d8f10c3b",
"name": "Set Language Config1"
}
],
"pinData": {},
"connections": {
"Schedule Trigger1": {
"main": [
[
{
"node": "Get All Post Types1",
"type": "main",
"index": 0
}
]
]
},
"Get All Post Types1": {
"main": [
[
{
"node": "Set Language Config1",
"type": "main",
"index": 0
}
]
]
},
"Split Post Types1": {
"main": [
[
{
"node": "Split Target Languages1",
"type": "main",
"index": 0
}
]
]
},
"Split Target Languages1": {
"main": [
[
{
"node": "Get Untranslated Posts1",
"type": "main",
"index": 0
}
]
]
},
"Get Untranslated Posts1": {
"main": [
[
{
"node": "Has Untranslated Posts?1",
"type": "main",
"index": 0
}
]
]
},
"Has Untranslated Posts?1": {
"main": [
[
{
"node": "Split Posts1",
"type": "main",
"index": 0
}
],
[
{
"node": "Workflow Complete1",
"type": "main",
"index": 0
}
]
]
},
"Split Posts1": {
"main": [
[
{
"node": "Get Post Content1",
"type": "main",
"index": 0
}
]
]
},
"Get Post Content1": {
"main": [
[
{
"node": "Process Post Data1",
"type": "main",
"index": 0
}
]
]
},
"Process Post Data1": {
"main": [
[
{
"node": "Build DeepSeek Request1",
"type": "main",
"index": 0
}
]
]
},
"Build DeepSeek Request1": {
"main": [
[
{
"node": "DeepSeek Translate1",
"type": "main",
"index": 0
}
]
]
},
"DeepSeek Translate1": {
"main": [
[
{
"node": "Format Translation Data1",
"type": "main",
"index": 0
}
]
]
},
"Format Translation Data1": {
"main": [
[
{
"node": "Create Translation1",
"type": "main",
"index": 0
}
]
]
},
"Create Translation1": {
"main": [
[
{
"node": "Rate Limit Delay1",
"type": "main",
"index": 0
}
]
]
},
"Rate Limit Delay1": {
"main": [
[
{
"node": "Log Success1",
"type": "main",
"index": 0
}
]
]
},
"Log Success1": {
"main": [
[
{
"node": "Workflow Complete1",
"type": "main",
"index": 0
}
]
]
},
"Set Language Config1": {
"main": [
[
{
"node": "Split Post Types1",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"availableInMCP": false
},
"versionId": "80268b84-3537-4eec-829f-a030c34d3e04",
"meta": {
"instanceId": "e116f0cdc2af85ea765f95e160256c7af2464258f43bb537f78b1a25e7dab4f3"
},
"id": "O0FMfybFZu--afOm6Pv9S",
"tags": []
}
{
"name": "这个版本保留Post conten所有html",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 6
}
]
}
},
"name": "Schedule Trigger1",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [
672,
448
],
"id": "1080d094-efb9-421f-9c19-efb00894b52f"
},
{
"parameters": {
"url": "=https://banwork.cn/wp-json/polylang-api/v1/post-types",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"options": {}
},
"name": "Get All Post Types1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
864,
448
],
"id": "3278e59f-712f-4b8d-912e-5365201a051e",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"fieldToSplitOut": "post_types",
"include": "selectedOtherFields",
"fieldsToInclude": "target_languages",
"options": {}
},
"name": "Split Post Types1",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
1264,
448
],
"id": "e9166d45-c3c2-4848-b8e0-b5c9067ab3fe"
},
{
"parameters": {
"fieldToSplitOut": "target_languages",
"options": {}
},
"name": "Split Target Languages1",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
1472,
448
],
"id": "7a05a4c4-89e9-40fa-829e-32bdfd568f3a"
},
{
"parameters": {
"url": "=https://banwork.cn/wp-json/polylang-api/v1/untranslated-posts",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "post_type",
"value": "={{ $('Split Post Types1').item.json.post_types }}"
},
{
"name": "source_lang",
"value": "={{ $('Set Language Config1').item.json.source_lang }}"
},
{
"name": "target_lang",
"value": "={{ $json.target_languages }}"
}
]
},
"options": {}
},
"name": "Get Untranslated Posts1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
1680,
448
],
"id": "06185347-be6a-4d38-a1c1-0f261548ab5b",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "has-posts",
"leftValue": "={{ $json.count }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
],
"combinator": "and"
},
"options": {}
},
"name": "Has Untranslated Posts?1",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1872,
464
],
"id": "2efcb82a-3a05-4f70-824f-7ac5ec9a0946"
},
{
"parameters": {
"fieldToSplitOut": "posts",
"options": {}
},
"name": "Split Posts1",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
2096,
336
],
"id": "412f4054-b3e4-4384-bba8-901b98925420"
},
{
"parameters": {
"url": "=https://banwork.cn/wp-json/polylang-api/v1/post/{{ $json.id }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"options": {}
},
"name": "Get Post Content1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
2304,
336
],
"id": "9673e104-fe30-4d27-9baa-ec25a24f9a27",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"jsCode": "function stripHtml(html) {\n if (!html) return '';\n \n let text = html\n .replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, '')\n .replace(/<style\\b[^<]*(?:(?!<\\/style>)<[^<]*)*<\\/style>/gi, '');\n \n text = text.replace(/<[^>]+>/g, ' ');\n text = text.replace(/\\s+/g, ' ').trim();\n \n return text;\n}\n\n// Process translatable meta fields\nfunction extractTranslatableMeta(metaFields, postType) {\n const translatable = {};\n \n if (!metaFields || typeof metaFields !== 'object') return translatable;\n \n // Extract text-based meta fields (skip internal WordPress fields)\n for (const [key, value] of Object.entries(metaFields)) {\n // Skip internal WP fields and serialized data\n if (key.startsWith('_') || !value) continue;\n \n // Handle arrays (take first value)\n const metaValue = Array.isArray(value) ? value[0] : value;\n \n // Only include string values\n if (typeof metaValue === 'string' && metaValue.length > 0) {\n translatable[key] = stripHtml(metaValue);\n }\n }\n \n return translatable;\n}\n\nconst items = $input.all();\n\nreturn items.map((item, index) => {\n const postData = item.json.post || item.json;\n \n return {\n json: {\n id: item.json.id || postData.id,\n post: {\n id: postData.id,\n post_type: postData.type,\n title: stripHtml(postData.title),\n content: postData.content,\n excerpt: stripHtml(postData.excerpt || ''),\n categories: postData.categories || [],\n tags: postData.tags || [],\n acf_fields: postData.acf_fields || {},\n meta_fields: extractTranslatableMeta(postData.meta, postData.type),\n thumbnail_id: postData.thumbnail_id || null\n }\n },\n pairedItem: index\n };\n});\n\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2512,
336
],
"id": "959d8a1e-3eca-4088-b723-e09bb38d068b",
"name": "Process Post Data1"
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst sourceLang = $('Set Language Config1').item.json.source_lang;\nconst targetLang = $('Split Target Languages1').item.json.target_languages;\n\n// Helper function to truncate text\nfunction truncateText(text, maxLength = 8000) {\n if (text.length <= maxLength) return text;\n return text.substring(0, maxLength) + '... [truncated]';\n}\n\nreturn items.map((item, index) => {\n const postData = item.json.post;\n \n // Truncate content if too long\n const title = truncateText(postData.title, 500);\n const content = truncateText(postData.content, 10000);\n const excerpt = postData.excerpt ? truncateText(postData.excerpt, 1000) : '';\n \n // Build content for translation including meta fields\n let translationContent = `Title: ${title}\\n\\nContent: ${content}`;\n \n if (excerpt) {\n translationContent += `\\n\\nExcerpt: ${excerpt}`;\n }\n \n // Add meta fields to translation\n const metaFieldsArray = [];\n if (postData.meta_fields && Object.keys(postData.meta_fields).length > 0) {\n translationContent += '\\n\\nMeta Fields:';\n for (const [key, value] of Object.entries(postData.meta_fields)) {\n const truncatedValue = truncateText(String(value), 500);\n translationContent += `\\n${key}: ${truncatedValue}`;\n metaFieldsArray.push({ key, value: truncatedValue });\n }\n }\n \n // Build expected JSON structure\n let expectedStructure = '\"title\": \"translated title\", \"content\": \"translated content\"';\n if (postData.excerpt) {\n expectedStructure += ', \"excerpt\": \"translated excerpt\"';\n }\n if (metaFieldsArray.length > 0) {\n expectedStructure += ', \"meta_fields\": {\"field_key\": \"translated value\"}';\n }\n \n const requestBody = {\n model: \"deepseek-chat\",\n messages: [\n {\n role: \"system\",\n content: `You are a professional translator. Translate the following WordPress ${postData.post_type} content from ${sourceLang} to ${targetLang}. CRITICAL RULES:\n- The input content contains HTML.\n- DO NOT remove, rewrite, reorder, escape, or normalize any HTML tags.\n- DO NOT add or remove <p>, <br>, <strong>, <a>, <ul>, <li>, or any attributes.\n- ONLY translate the visible text between HTML tags.\n- Preserve whitespace and line breaks.\n- Return VALID JSON ONLY.\n- Keep HTML exactly as-is. Return ONLY a valid JSON object with these exact keys: {${expectedStructure}}. For meta_fields, use the same keys but translate the values. IMPORTANT: Ensure all strings in the JSON are properly escaped and the response is valid JSON.`\n },\n {\n role: \"user\",\n content: translationContent\n }\n ],\n temperature: 0.4,\n max_tokens: 8192, // Add max_tokens limit\n response_format: { type: \"json_object\" }\n };\n \n return {\n json: {\n requestBody: requestBody,\n post: postData,\n metaFieldsKeys: metaFieldsArray.map(m => m.key)\n },\n pairedItem: index\n };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2720,
336
],
"id": "61db9cd5-d503-49d0-893c-c3603e514a48",
"name": "Build DeepSeek Request1"
},
{
"parameters": {
"method": "POST",
"url": "https://api.deepseek.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.requestBody }}",
"options": {}
},
"name": "DeepSeek Translate1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
2928,
336
],
"id": "438fbcb2-37a4-45a1-9531-61c1378cfdfb",
"credentials": {
"httpHeaderAuth": {
"id": "5X9OcogB9LTTIp0J",
"name": "Header Auth account"
}
}
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst postContentItems = $('Get Post Content1').all();\nconst targetLang = $('Split Target Languages1').item.json.target_languages;\n\nreturn items.map((item, index) => {\n const response = item.json;\n \n let translatedData;\n try {\n // Get the raw content\n const rawContent = response.choices[0].message.content;\n \n // Try to parse the JSON\n translatedData = JSON.parse(rawContent);\n } catch (parseError) {\n // If JSON parsing fails, try to extract what we can\n const rawContent = response.choices[0].message.content;\n \n // Log the error for debugging\n console.error('Failed to parse translation response:', parseError.message);\n console.error('Raw content:', rawContent.substring(0, 500)); // First 500 chars\n \n // Try to salvage partial data\n translatedData = {\n title: 'Translation failed - JSON parse error',\n content: 'Error: Could not parse translation response',\n excerpt: ''\n };\n \n // Try to extract title if possible\n const titleMatch = rawContent.match(/\"title\"\\s*:\\s*\"([^\"]*)\"/);\n if (titleMatch) {\n translatedData.title = titleMatch[1];\n }\n }\n \n const originalPostItem = postContentItems[index].json;\n const originalPost = originalPostItem.post || originalPostItem;\n \n const categories = (originalPost.categories || []).map(cat => ({\n term_id: cat.term_id,\n translated_name: cat.name\n }));\n \n const tags = (originalPost.tags || []).map(tag => ({\n term_id: tag.term_id,\n translated_name: tag.name\n }));\n \n let acfFields = {};\n if (originalPost.acf_fields && typeof originalPost.acf_fields === 'object') {\n acfFields = originalPost.acf_fields;\n }\n \n // Extract translated meta fields\n let translatedMeta = {};\n if (translatedData.meta_fields && typeof translatedData.meta_fields === 'object') {\n translatedMeta = translatedData.meta_fields;\n }\n \n return {\n json: {\n source_post_id: originalPost.id,\n post_type: originalPost.type,\n target_lang: targetLang,\n title: translatedData.title || originalPost.title,\n content: translatedData.content || originalPost.content,\n excerpt: translatedData.excerpt || '',\n categories: categories,\n tags: tags,\n acf_fields: acfFields,\n meta_fields: translatedMeta,\n thumbnail_id: originalPost.thumbnail_id\n },\n pairedItem: index\n };\n});"
},
"name": "Format Translation Data1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3136,
336
],
"id": "80234c05-0a20-465f-afc4-a42cb52ae6db"
},
{
"parameters": {
"method": "POST",
"url": "=https://banwork.cn/wp-json/polylang-api/v1/translate-post",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"source_post_id\": {{ $json.source_post_id }},\n \"target_lang\": \"{{ $json.target_lang }}\",\n \"title\": {{ JSON.stringify($json.title) }},\n \"content\": {{ JSON.stringify($json.content) }},\n \"excerpt\": {{ JSON.stringify($json.excerpt) }},\n \"categories\": {{ JSON.stringify($json.categories) }},\n \"tags\": {{ JSON.stringify($json.tags) }},\n \"acf_fields\": {{ JSON.stringify($json.acf_fields) }},\n \"meta_fields\": {{ JSON.stringify($json.meta_fields) }},\n \"thumbnail_id\": {{ $json.thumbnail_id }}\n}",
"options": {}
},
"name": "Create Translation1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
3344,
336
],
"id": "9bcdb731-7348-4608-8469-f49d5b38082e",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"amount": 2
},
"name": "Rate Limit Delay1",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
3552,
336
],
"id": "8d744c8a-f962-4d61-b6bc-47f9a311e21e",
"webhookId": "0f33c2d9-789c-43d0-9186-25a2b0a33997"
},
{
"parameters": {
"options": {}
},
"name": "Log Success1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
3744,
336
],
"id": "f22d9d72-d053-4da3-a6ce-552241e8cd2d"
},
{
"parameters": {
"options": {}
},
"name": "Workflow Complete1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
3952,
448
],
"id": "02017e05-b91c-4617-a819-0b15794e21c9"
},
{
"parameters": {
"jsCode": "const inputData = $input.first().json;\n\nreturn [\n {\n json: {\n post_types: inputData.post_types,\n // Add all your target languages here\n target_languages: [\"de\"],\n source_lang: \"en\"\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1072,
448
],
"id": "95253ccb-037a-45b8-952d-2fbf22aaface",
"name": "Set Language Config1"
}
],
"pinData": {},
"connections": {
"Schedule Trigger1": {
"main": [
[
{
"node": "Get All Post Types1",
"type": "main",
"index": 0
}
]
]
},
"Get All Post Types1": {
"main": [
[
{
"node": "Set Language Config1",
"type": "main",
"index": 0
}
]
]
},
"Split Post Types1": {
"main": [
[
{
"node": "Split Target Languages1",
"type": "main",
"index": 0
}
]
]
},
"Split Target Languages1": {
"main": [
[
{
"node": "Get Untranslated Posts1",
"type": "main",
"index": 0
}
]
]
},
"Get Untranslated Posts1": {
"main": [
[
{
"node": "Has Untranslated Posts?1",
"type": "main",
"index": 0
}
]
]
},
"Has Untranslated Posts?1": {
"main": [
[
{
"node": "Split Posts1",
"type": "main",
"index": 0
}
],
[
{
"node": "Workflow Complete1",
"type": "main",
"index": 0
}
]
]
},
"Split Posts1": {
"main": [
[
{
"node": "Get Post Content1",
"type": "main",
"index": 0
}
]
]
},
"Get Post Content1": {
"main": [
[
{
"node": "Process Post Data1",
"type": "main",
"index": 0
}
]
]
},
"Process Post Data1": {
"main": [
[
{
"node": "Build DeepSeek Request1",
"type": "main",
"index": 0
}
]
]
},
"Build DeepSeek Request1": {
"main": [
[
{
"node": "DeepSeek Translate1",
"type": "main",
"index": 0
}
]
]
},
"DeepSeek Translate1": {
"main": [
[
{
"node": "Format Translation Data1",
"type": "main",
"index": 0
}
]
]
},
"Format Translation Data1": {
"main": [
[
{
"node": "Create Translation1",
"type": "main",
"index": 0
}
]
]
},
"Create Translation1": {
"main": [
[
{
"node": "Rate Limit Delay1",
"type": "main",
"index": 0
}
]
]
},
"Rate Limit Delay1": {
"main": [
[
{
"node": "Log Success1",
"type": "main",
"index": 0
}
]
]
},
"Log Success1": {
"main": [
[
{
"node": "Workflow Complete1",
"type": "main",
"index": 0
}
]
]
},
"Set Language Config1": {
"main": [
[
{
"node": "Split Post Types1",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"availableInMCP": false
},
"versionId": "4726220f-8b5e-42ab-bff5-0a389cf970b9",
"meta": {
"instanceId": "e116f0cdc2af85ea765f95e160256c7af2464258f43bb537f78b1a25e7dab4f3"
},
"id": "FlCxo05rYNW5KAdv",
"tags": []
}
{
"name": "能用2-保留block翻译同时保留Post conten所有html copy",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 6
}
]
}
},
"name": "Schedule Trigger1",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [
672,
448
],
"id": "cb01752b-62c8-4da9-9c42-fb7269216fff"
},
{
"parameters": {
"url": "=https://banwork.cn/wp-json/polylang-api/v1/post-types",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"options": {}
},
"name": "Get All Post Types1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
864,
448
],
"id": "9ac5d1e3-0a47-4ba7-ac72-35cbf04b37ce",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"fieldToSplitOut": "post_types",
"include": "selectedOtherFields",
"fieldsToInclude": "target_languages",
"options": {}
},
"name": "Split Post Types1",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
1264,
448
],
"id": "75e5ea49-dc0d-48b2-8b98-168f20975e0e"
},
{
"parameters": {
"fieldToSplitOut": "target_languages",
"options": {}
},
"name": "Split Target Languages1",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
1472,
448
],
"id": "152f703c-60c4-4a1c-8fa8-60b57d613645"
},
{
"parameters": {
"url": "=https://banwork.cn/wp-json/polylang-api/v1/untranslated-posts",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "post_type",
"value": "={{ $('Split Post Types1').item.json.post_types }}"
},
{
"name": "source_lang",
"value": "={{ $('Set Language Config1').item.json.source_lang }}"
},
{
"name": "target_lang",
"value": "={{ $json.target_languages }}"
}
]
},
"options": {}
},
"name": "Get Untranslated Posts1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
1680,
448
],
"id": "c8171ee1-9eb9-4220-ad26-d9e6c0c92b8a",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "has-posts",
"leftValue": "={{ $json.count }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
],
"combinator": "and"
},
"options": {}
},
"name": "Has Untranslated Posts?1",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1872,
464
],
"id": "948b3989-9e4e-4a33-a7d3-a3119982bf4f"
},
{
"parameters": {
"fieldToSplitOut": "posts",
"options": {}
},
"name": "Split Posts1",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
2096,
336
],
"id": "de2dcb1c-3dd3-4e40-a727-6b509b59d2a3"
},
{
"parameters": {
"url": "=https://banwork.cn/wp-json/polylang-api/v1/post/{{ $json.id }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"options": {}
},
"name": "Get Post Content1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
2304,
336
],
"id": "f4b25d59-f0a3-4bb2-be25-42a6e04ec42a",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"jsCode": "function stripHtml(html) {\n if (!html) return '';\n \n let text = html\n .replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, '')\n .replace(/<style\\b[^<]*(?:(?!<\\/style>)<[^<]*)*<\\/style>/gi, '');\n \n text = text.replace(/<[^>]+>/g, ' ');\n text = text.replace(/\\s+/g, ' ').trim();\n \n return text;\n}\n\n// NEW: Parse WordPress blocks\nfunction parseBlocks(content) {\n if (!content) return { blocks: [], plainContent: '' };\n \n const blockPattern = /<!--\\s*wp:([a-z0-9-]+\\/[a-z0-9-]+)\\s+({[^}]+})\\s*(\\/)?-->/gi;\n const blocks = [];\n let match;\n \n while ((match = blockPattern.exec(content)) !== null) {\n try {\n const blockName = match[1];\n const attributes = JSON.parse(match[2]);\n const isSelfClosing = match[3] === '/';\n \n blocks.push({\n blockName,\n attributes,\n isSelfClosing,\n originalMatch: match[0],\n index: match.index\n });\n } catch (e) {\n console.error('Failed to parse block:', match[0]);\n }\n }\n \n // Remove blocks from content for separate translation\n const plainContent = content.replace(blockPattern, '');\n \n return { blocks, plainContent };\n}\n\nfunction extractTranslatableMeta(metaFields, postType) {\n const translatable = {};\n \n if (!metaFields || typeof metaFields !== 'object') return translatable;\n \n for (const [key, value] of Object.entries(metaFields)) {\n if (key.startsWith('_') || !value) continue;\n \n const metaValue = Array.isArray(value) ? value[0] : value;\n \n if (typeof metaValue === 'string' && metaValue.length > 0) {\n translatable[key] = stripHtml(metaValue);\n }\n }\n \n return translatable;\n}\n\nconst items = $input.all();\n\nreturn items.map((item, index) => {\n const postData = item.json.post || item.json;\n \n // Parse blocks from content\n const { blocks, plainContent } = parseBlocks(postData.content);\n \n return {\n json: {\n id: item.json.id || postData.id,\n post: {\n id: postData.id,\n post_type: postData.type,\n title: stripHtml(postData.title),\n content: plainContent, // Content without block comments\n blocks: blocks, // Separate blocks for translation\n excerpt: stripHtml(postData.excerpt || ''),\n categories: postData.categories || [],\n tags: postData.tags || [],\n acf_fields: postData.acf_fields || {},\n meta_fields: extractTranslatableMeta(postData.meta, postData.type),\n thumbnail_id: postData.thumbnail_id || null\n }\n },\n pairedItem: index\n };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2512,
336
],
"id": "7ce1aa13-7eb8-4a29-b779-38f3e972728f",
"name": "Process Post Data1"
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst sourceLang = $('Set Language Config1').item.json.source_lang;\nconst targetLang = $('Split Target Languages1').item.json.target_languages;\n\nfunction truncateText(text, maxLength = 8000) {\n if (text.length <= maxLength) return text;\n return text.substring(0, maxLength) + '... [truncated]';\n}\n\n// Extract translatable content from blocks\nfunction extractBlockTranslatables(content) {\n const blockPattern = /<!--\\s*wp:([^\\s]+)\\s+(\\{[^}]+\\})\\s*(?:\\/)?-->/g;\n const translatables = [];\n let match;\n \n while ((match = blockPattern.exec(content)) !== null) {\n try {\n const blockName = match[1];\n const attrs = JSON.parse(match[2]);\n \n // Extract translatable attributes\n const translatable = {};\n for (const [key, value] of Object.entries(attrs)) {\n if (typeof value === 'string' && !key.includes('Id') && !key.includes('Url')) {\n translatable[key] = value;\n } else if (Array.isArray(value)) {\n // Handle arrays like stats\n translatable[key] = value.map(item => {\n if (typeof item === 'object') {\n const trans = {};\n for (const [k, v] of Object.entries(item)) {\n if (typeof v === 'string' && !k.includes('Id')) {\n trans[k] = v;\n }\n }\n return trans;\n }\n return item;\n });\n }\n }\n \n if (Object.keys(translatable).length > 0) {\n translatables.push({\n blockName,\n attributes: translatable\n });\n }\n } catch (e) {\n console.error('Block parse error:', e);\n }\n }\n \n return translatables;\n}\n\nreturn items.map((item, index) => {\n const postData = item.json.post;\n \n const title = truncateText(postData.title, 500);\n const excerpt = postData.excerpt ? truncateText(postData.excerpt, 1000) : '';\n \n // Extract block translatables\n const blockTranslatables = extractBlockTranslatables(postData.content);\n \n let translationContent = `Title: ${title}`;\n \n if (excerpt) {\n translationContent += `\\n\\nExcerpt: ${excerpt}`;\n }\n \n if (blockTranslatables.length > 0) {\n translationContent += `\\n\\nBlocks to translate:\\n${JSON.stringify(blockTranslatables, null, 2)}`;\n }\n \n const requestBody = {\n model: \"deepseek-chat\",\n messages: [\n {\n role: \"system\",\n content: `Translate from ${sourceLang} to ${targetLang}. Return JSON with:\n- \"title\": translated title\n- \"excerpt\": translated excerpt \n- \"blocks\": array of {blockName, attributes} with ONLY text values translated\n\nDO NOT translate: IDs, URLs, numbers (unless they're labels).`\n },\n {\n role: \"user\",\n content: translationContent\n }\n ],\n temperature: 0.4,\n max_tokens: 8192,\n response_format: { type: \"json_object\" }\n };\n \n return {\n json: {\n requestBody,\n post: postData\n },\n pairedItem: index\n };\n});\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2720,
336
],
"id": "1a3f9225-c35e-4848-b281-758a8402bd1f",
"name": "Build DeepSeek Request1"
},
{
"parameters": {
"method": "POST",
"url": "https://api.deepseek.com/v1/chat/completions",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.requestBody }}",
"options": {}
},
"name": "DeepSeek Translate1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
2928,
336
],
"id": "d02dc43f-ae3c-4025-bf12-ee0dbdd8961b",
"credentials": {
"httpHeaderAuth": {
"id": "5X9OcogB9LTTIp0J",
"name": "Header Auth account"
}
}
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst postContentItems = $('Get Post Content1').all();\nconst targetLang = $('Split Target Languages1').item.json.target_languages;\n\nreturn items.map((item, index) => {\n const response = item.json;\n \n let translatedData;\n try {\n // Get the raw content\n const rawContent = response.choices[0].message.content;\n \n // Try to parse the JSON\n translatedData = JSON.parse(rawContent);\n } catch (parseError) {\n // If JSON parsing fails, try to extract what we can\n const rawContent = response.choices[0].message.content;\n \n // Log the error for debugging\n console.error('Failed to parse translation response:', parseError.message);\n console.error('Raw content:', rawContent.substring(0, 500)); // First 500 chars\n \n // Try to salvage partial data\n translatedData = {\n title: 'Translation failed - JSON parse error',\n content: 'Error: Could not parse translation response',\n excerpt: ''\n };\n \n // Try to extract title if possible\n const titleMatch = rawContent.match(/\"title\"\\s*:\\s*\"([^\"]*)\"/);\n if (titleMatch) {\n translatedData.title = titleMatch[1];\n }\n }\n \n const originalPostItem = postContentItems[index].json;\n const originalPost = originalPostItem.post || originalPostItem;\n \n const categories = (originalPost.categories || []).map(cat => ({\n term_id: cat.term_id,\n translated_name: cat.name\n }));\n \n const tags = (originalPost.tags || []).map(tag => ({\n term_id: tag.term_id,\n translated_name: tag.name\n }));\n \n let acfFields = {};\n if (originalPost.acf_fields && typeof originalPost.acf_fields === 'object') {\n acfFields = originalPost.acf_fields;\n }\n \n // Extract translated meta fields\n let translatedMeta = {};\n if (translatedData.meta_fields && typeof translatedData.meta_fields === 'object') {\n translatedMeta = translatedData.meta_fields;\n }\n \n return {\n json: {\n source_post_id: originalPost.id,\n post_type: originalPost.type,\n target_lang: targetLang,\n title: translatedData.title || originalPost.title,\n content: translatedData.content || originalPost.content,\n excerpt: translatedData.excerpt || '',\n categories: categories,\n tags: tags,\n acf_fields: acfFields,\n meta_fields: translatedMeta,\n thumbnail_id: originalPost.thumbnail_id\n },\n pairedItem: index\n };\n});"
},
"name": "Format Translation Data1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3136,
336
],
"id": "b9943686-75d1-4889-8628-8f51d7ed023d"
},
{
"parameters": {
"method": "POST",
"url": "=https://banwork.cn/wp-json/polylang-api/v1/translate-post",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"source_post_id\": {{ $json.source_post_id }},\n \"target_lang\": \"{{ $json.target_lang }}\",\n \"title\": {{ JSON.stringify($json.title) }},\n \"content\": {{ JSON.stringify($json.content) }},\n \"excerpt\": {{ JSON.stringify($json.excerpt) }},\n \"categories\": {{ JSON.stringify($json.categories) }},\n \"tags\": {{ JSON.stringify($json.tags) }},\n \"acf_fields\": {{ JSON.stringify($json.acf_fields) }},\n \"meta_fields\": {{ JSON.stringify($json.meta_fields) }},\n \"thumbnail_id\": {{ $json.thumbnail_id }}\n}",
"options": {}
},
"name": "Create Translation1",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
3552,
336
],
"id": "da6a142d-3436-43ac-978c-07e1dd9c1274",
"credentials": {
"httpBasicAuth": {
"id": "nRdVHRimMjQuSFHB",
"name": "Unnamed credential"
}
}
},
{
"parameters": {
"amount": 2
},
"name": "Rate Limit Delay1",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
3760,
336
],
"id": "6a04a6e8-9c84-4337-9176-4e96b5bbd11d",
"webhookId": "58627df9-d46d-44dc-98ac-10f6b76a3d17"
},
{
"parameters": {
"options": {}
},
"name": "Log Success1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
3952,
336
],
"id": "38301203-d5c0-4905-94de-a19dcc7d4209"
},
{
"parameters": {
"options": {}
},
"name": "Workflow Complete1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
4160,
448
],
"id": "4e486234-0d01-4144-8357-1cacc32e03ae"
},
{
"parameters": {
"jsCode": "const inputData = $input.first().json;\n\nreturn [\n {\n json: {\n post_types: inputData.post_types,\n // Add all your target languages here\n target_languages: [\"es\"],\n source_lang: \"en\"\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1072,
448
],
"id": "ac5434f8-e518-456d-930f-d67770a7d2c0",
"name": "Set Language Config1"
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst buildRequestItems = $('Build DeepSeek Request1').all();\n\nreturn items.map((item, index) => {\n const translatedData = item.json;\n const originalBlocks = buildRequestItems[index].json.originalBlocks;\n \n // Reconstruct blocks with translated attributes\n let reconstructedContent = translatedData.content || '';\n \n if (translatedData.blocks && originalBlocks.length > 0) {\n translatedData.blocks.forEach(translatedBlock => {\n const originalBlock = originalBlocks[translatedBlock.blockIndex];\n if (!originalBlock) return;\n \n // Merge translated attributes with original structure\n const newAttributes = JSON.stringify({\n ...originalBlock.attributes,\n ...translatedBlock.attributes\n });\n \n // Reconstruct block comment\n const blockComment = originalBlock.isSelfClosing \n ? `<!-- wp:${originalBlock.blockName} ${newAttributes} /-->`\n : `<!-- wp:${originalBlock.blockName} ${newAttributes} --><!-- /wp:${originalBlock.blockName} -->`;\n \n reconstructedContent += '\\n' + blockComment;\n });\n }\n \n return {\n json: {\n ...translatedData,\n content: reconstructedContent\n },\n pairedItem: index\n };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3344,
336
],
"id": "bfc5ac85-d614-4b15-a3c7-ed764a474d95",
"name": "Reconstruct Blocks"
}
],
"pinData": {
"Split Post Types1": [
{
"json": {
"post_types": "page",
"target_languages": [
"es"
]
}
}
]
},
"connections": {
"Schedule Trigger1": {
"main": [
[
{
"node": "Get All Post Types1",
"type": "main",
"index": 0
}
]
]
},
"Get All Post Types1": {
"main": [
[
{
"node": "Set Language Config1",
"type": "main",
"index": 0
}
]
]
},
"Split Post Types1": {
"main": [
[
{
"node": "Split Target Languages1",
"type": "main",
"index": 0
}
]
]
},
"Split Target Languages1": {
"main": [
[
{
"node": "Get Untranslated Posts1",
"type": "main",
"index": 0
}
]
]
},
"Get Untranslated Posts1": {
"main": [
[
{
"node": "Has Untranslated Posts?1",
"type": "main",
"index": 0
}
]
]
},
"Has Untranslated Posts?1": {
"main": [
[
{
"node": "Split Posts1",
"type": "main",
"index": 0
}
],
[
{
"node": "Workflow Complete1",
"type": "main",
"index": 0
}
]
]
},
"Split Posts1": {
"main": [
[
{
"node": "Get Post Content1",
"type": "main",
"index": 0
}
]
]
},
"Get Post Content1": {
"main": [
[
{
"node": "Process Post Data1",
"type": "main",
"index": 0
}
]
]
},
"Process Post Data1": {
"main": [
[
{
"node": "Build DeepSeek Request1",
"type": "main",
"index": 0
}
]
]
},
"Build DeepSeek Request1": {
"main": [
[
{
"node": "DeepSeek Translate1",
"type": "main",
"index": 0
}
]
]
},
"DeepSeek Translate1": {
"main": [
[
{
"node": "Format Translation Data1",
"type": "main",
"index": 0
}
]
]
},
"Format Translation Data1": {
"main": [
[
{
"node": "Reconstruct Blocks",
"type": "main",
"index": 0
}
]
]
},
"Create Translation1": {
"main": [
[
{
"node": "Rate Limit Delay1",
"type": "main",
"index": 0
}
]
]
},
"Rate Limit Delay1": {
"main": [
[
{
"node": "Log Success1",
"type": "main",
"index": 0
}
]
]
},
"Log Success1": {
"main": [
[
{
"node": "Workflow Complete1",
"type": "main",
"index": 0
}
]
]
},
"Set Language Config1": {
"main": [
[
{
"node": "Split Post Types1",
"type": "main",
"index": 0
}
]
]
},
"Reconstruct Blocks": {
"main": [
[
{
"node": "Create Translation1",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"availableInMCP": false
},
"versionId": "ca79bb5e-4a9b-44c8-a749-eed77d978773",
"meta": {
"instanceId": "e116f0cdc2af85ea765f95e160256c7af2464258f43bb537f78b1a25e7dab4f3"
},
"id": "eF1lHl9gFO6PkAx6",
"tags": []
}
WP开发教程 WordPress导师在线教程