星期日 , 8 11 月 2026

使用n8n+deepseek进行翻译最强教程(文章很长)

第一步

Complete N8N Workflow for Automated Polylang Translation with DeepSeek

I’ll guide you through building a complete automated translation workflow for Polylang using N8N and DeepSeek API.

Overview of the Workflow

The workflow will:

  1. Fetch untranslated posts from WordPress
  2. Send content to DeepSeek for translation
  3. Create translated posts in Polylang’s required format
  4. Link translations properly in Polylang

Prerequisites

Before starting, ensure you have:

  • N8N instance running (self-hosted or cloud)
  • WordPress site with Polylang installed and configured
  • DeepSeek API key from platform.deepseek.com
  • WordPress REST API enabled (default in modern WordPress)
  • Application Password or JWT token for WordPress authentication

Step 1: Configure WordPress for API Access

1.1 Create Application Password

  1. Go to WordPress Admin → Users → Profile
  2. Scroll to “Application Passwords”
  3. Enter name: “N8N Automation”
  4. Click “Add New Application Password”
  5. Save the generated password (format: xxxx xxxx xxxx xxxx xxxx xxxx)

1.2 Verify Polylang API Support

Polylang uses standard WordPress post metadata for translations. Key meta fields:

  • _icl_lang_duplicate_of – ID of the original post
  • Polylang stores language info in taxonomies

Step 2: Get DeepSeek API Credentials

  1. Sign up at platform.deepseek.com
  2. Navigate to API Keys section
  3. Create new API key
  4. Save your API key securely

Step 3: Build the N8N Workflow

3.1 Complete Workflow Structure

Here’s the detailed node-by-node breakdown:

[Schedule Trigger] 
    ↓
[Get Untranslated Posts] (HTTP Request)
    ↓
[Split In Batches] (optional, for large datasets)
    ↓
[Extract Post Data] (Set/Function)
    ↓
[Translate with DeepSeek] (HTTP Request)
    ↓
[Parse Translation Response] (Set/Function)
    ↓
[Create Translated Post] (HTTP Request)
    ↓
[Link Polylang Translation] (HTTP Request)
    ↓
[Update Translation Meta] (HTTP Request)

Step 4: Detailed Node Configuration

Node 1: Schedule Trigger

Purpose: Run workflow automatically (e.g., daily)

Configuration:

  • Node Type: Schedule Trigger
  • Trigger Interval: Every day at 2:00 AM (or your preference)
  • Alternative: Use Webhook node for manual triggers

Node 2: Get Untranslated Posts

Purpose: Fetch all posts in source language that need translation

Configuration:

Node Type: HTTP Request
Method: GET
URL: https://yoursite.com/wp-json/wp/v2/posts
Authentication: Basic Auth
  - User: your-wordpress-username
  - Password: [Application Password from Step 1.1]

Query Parameters:
  - lang: en (your source language code)
  - per_page: 10
  - status: publish
  - _fields: id,title,content,excerpt,slug,date,categories,tags

Headers:
  - Content-Type: application/json

Important: Polylang adds lang parameter support to WordPress REST API automatically.


Node 3: Filter Untranslated Posts

Purpose: Check if translation already exists

Configuration:

Node Type: Function
JavaScript Code:

const items = $input.all();
const output = [];

for (let item of items) {
  const postId = item.json.id;
  
  // Check if translation exists for target language
  // This will be done via another HTTP request
  output.push({
    json: {
      source_post_id: postId,
      source_title: item.json.title.rendered,
      source_content: item.json.content.rendered,
      source_excerpt: item.json.excerpt.rendered,
      source_slug: item.json.slug,
      source_categories: item.json.categories,
      source_tags: item.json.tags,
      source_date: item.json.date
    }
  });
}

return output;

Node 4: Check Existing Translations

Purpose: Query Polylang to see if translation exists

Configuration:

Node Type: HTTP Request
Method: GET
URL: https://yoursite.com/wp-json/wp/v2/posts
Authentication: Basic Auth

Query Parameters:
  - lang: zh (target language, e.g., Chinese)
  - meta_key: _icl_lang_duplicate_of
  - meta_value: {{$json.source_post_id}}
  - per_page: 1

Response Format: JSON

Add IF Node after this:

Node Type: IF
Condition: {{$json.length}} equals 0
- TRUE: Proceed to translation
- FALSE: Skip (translation already exists)

Node 5: Prepare Content for Translation

Purpose: Clean HTML and prepare text for DeepSeek

Configuration:

Node Type: Function
JavaScript Code:

function stripHtml(html) {
  // Basic HTML stripping - preserve structure markers
  return html
    .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
    .replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
    .replace(/<[^>]+>/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

const item = $input.item.json;

return [{
  json: {
    ...item,
    clean_title: stripHtml(item.source_title),
    clean_content: stripHtml(item.source_content),
    clean_excerpt: stripHtml(item.source_excerpt)
  }
}];

Node 6: Translate with DeepSeek

Purpose: Send content to DeepSeek API for translation

Configuration:

Node Type: HTTP Request
Method: POST
URL: https://api.deepseek.com/v1/chat/completions
Authentication: Header Auth
  - Name: Authorization
  - Value: Bearer YOUR_DEEPSEEK_API_KEY

Headers:
  - Content-Type: application/json

Body (JSON):
{
  "model": "deepseek-chat",
  "messages": [
    {
      "role": "system",
      "content": "You are a professional translator. Translate the following WordPress post content from English to Traditional Chinese (zh-TW). Maintain the tone, style, and formatting. Return ONLY a JSON object with these keys: title, content, excerpt. Do not include any markdown formatting or code blocks in your response."
    },
    {
      "role": "user",
      "content": "Translate this post:\n\nTitle: {{$json.clean_title}}\n\nContent: {{$json.clean_content}}\n\nExcerpt: {{$json.clean_excerpt}}"
    }
  ],
  "temperature": 0.3,
  "max_tokens": 4000
}

Response Format: JSON

Important Notes:

  • Adjust temperature (0.1-0.5) for more consistent translations
  • Set max_tokens based on your content length
  • For long content, you may need to split into chunks

Node 7: Parse DeepSeek Response

Purpose: Extract translated content from API response

Configuration:

Node Type: Function
JavaScript Code:

const response = $input.item.json;
const sourceData = $node["Prepare Content for Translation"].json;

// DeepSeek returns content in choices[0].message.content
let translatedText = response.choices[0].message.content;

// Try to parse as JSON (if DeepSeek followed instructions)
let translation;
try {
  // Remove markdown code blocks if present
  translatedText = translatedText.replace(/```json\n?/g, '').replace(/```\n?/g, '');
  translation = JSON.parse(translatedText);
} catch (error) {
  // Fallback: use the entire response as content
  console.error('Failed to parse translation JSON:', error);
  translation = {
    title: sourceData.clean_title + ' (翻譯)',
    content: translatedText,
    excerpt: translatedText.substring(0, 150)
  };
}

return [{
  json: {
    source_post_id: sourceData.source_post_id,
    source_slug: sourceData.source_slug,
    source_categories: sourceData.source_categories,
    source_tags: sourceData.source_tags,
    source_date: sourceData.source_date,
    translated_title: translation.title,
    translated_content: translation.content,
    translated_excerpt: translation.excerpt,
    target_language: 'zh' // Your target language code
  }
}];

Node 8: Create Translated Post in WordPress

Purpose: Create new post in target language

Configuration:

Node Type: HTTP Request
Method: POST
URL: https://yoursite.com/wp-json/wp/v2/posts
Authentication: Basic Auth

Headers:
  - Content-Type: application/json

Body (JSON):
{
  "title": "{{$json.translated_title}}",
  "content": "{{$json.translated_content}}",
  "excerpt": "{{$json.translated_excerpt}}",
  "status": "draft",
  "slug": "{{$json.source_slug}}-zh",
  "date": "{{$json.source_date}}",
  "categories": {{$json.source_categories}},
  "tags": {{$json.source_tags}},
  "meta": {
    "_icl_lang_duplicate_of": "{{$json.source_post_id}}"
  }
}

Response Format: JSON

Important:

  • Status set to “draft” for review before publishing
  • Slug appended with language code
  • Categories/tags copied from source

Node 9: Set Polylang Language for New Post

Purpose: Assign language to the translated post

Configuration:

Node Type: HTTP Request
Method: POST
URL: https://yoursite.com/wp-json/wp/v2/posts/{{$node["Create Translated Post"].json.id}}
Authentication: Basic Auth

Query Parameters:
  - lang: zh

Headers:
  - Content-Type: application/json

Body (JSON):
{
  "lang": "zh"
}

Response Format: JSON

Note: This sets the post language in Polylang’s taxonomy.


Node 10: Link Translation in Polylang

Purpose: Create bidirectional translation link

Configuration:

This requires using Polylang’s custom API or direct database manipulation. Since Polylang doesn’t expose translation linking via REST API, we have two options:

Option A: Use Polylang Pro API (if available)

If you have Polylang Pro with REST API enabled:

Node Type: HTTP Request
Method: POST
URL: https://yoursite.com/wp-json/pll/v1/translations
Authentication: Basic Auth

Body (JSON):
{
  "source_id": {{$json.source_post_id}},
  "target_id": {{$node["Create Translated Post"].json.id}},
  "source_lang": "en",
  "target_lang": "zh"
}

Option B: Custom WordPress Plugin/Endpoint (Recommended)

Create a custom REST API endpoint in your WordPress theme’s functions.php or a custom plugin:

php

// Add to functions.php or custom plugin
add_action('rest_api_init', function () {
    register_rest_route('polylang-automation/v1', '/link-translation', array(
        'methods' => 'POST',
        'callback' => 'link_polylang_translation',
        'permission_callback' => function() {
            return current_user_can('edit_posts');
        }
    ));
});

function link_polylang_translation($request) {
    $source_id = $request->get_param('source_id');
    $target_id = $request->get_param('target_id');
    $source_lang = $request->get_param('source_lang');
    $target_lang = $request->get_param('target_lang');
    
    if (!function_exists('pll_save_post_translations')) {
        return new WP_Error('polylang_missing', 'Polylang is not active', array('status' => 500));
    }
    
    // Get existing translations
    $translations = pll_get_post_translations($source_id);
    
    // Add new translation
    $translations[$target_lang] = $target_id;
    
    // Save translation links
    pll_save_post_translations($translations);
    
    return array(
        'success' => true,
        'translations' => $translations
    );
}
```

**Then in N8N:**
```
Node Type: HTTP Request
Method: POST
URL: https://yoursite.com/wp-json/polylang-automation/v1/link-translation
Authentication: Basic Auth

Headers:
  - Content-Type: application/json

Body (JSON):
{
  "source_id": {{$json.source_post_id}},
  "target_id": {{$node["Create Translated Post"].json.id}},
  "source_lang": "en",
  "target_lang": "zh"
}

Response Format: JSON
```

---

## Step 5: Handle Multiple Target Languages

To translate to multiple languages (e.g., Chinese, Japanese, Spanish):

### Add a Loop Node

**After "Filter Untranslated Posts":**
```
Node Type: Function
Name: Define Target Languages

JavaScript Code:

const sourceItem = $input.item.json;
const targetLanguages = [
  { code: 'zh', name: 'Chinese', deepseek_instruction: 'Traditional Chinese (zh-TW)' },
  { code: 'ja', name: 'Japanese', deepseek_instruction: 'Japanese (ja-JP)' },
  { code: 'es', name: 'Spanish', deepseek_instruction: 'Spanish (es-ES)' }
];

return targetLanguages.map(lang => ({
  json: {
    ...sourceItem,
    target_lang_code: lang.code,
    target_lang_name: lang.name,
    translation_instruction: lang.deepseek_instruction
  }
}));

Update DeepSeek Translation Node:

Change the system message to use the dynamic instruction:

json

{
  "role": "system",
  "content": "You are a professional translator. Translate the following WordPress post content from English to {{$json.translation_instruction}}. Maintain the tone, style, and formatting. Return ONLY a JSON object with these keys: title, content, excerpt."
}
```

---

## Step 6: Error Handling & Logging

### Add Error Handling Nodes

**After each HTTP Request, add an IF node:**
```
Node Type: IF
Condition: {{$json.statusCode}} equals 200

TRUE path: Continue workflow
FALSE path: Send error notification
```

**Create Error Notification:**
```
Node Type: Send Email / Slack / Discord
Subject: Translation Workflow Error
Message:
Failed to translate post ID: {{$json.source_post_id}}
Error: {{$json.error}}
Status Code: {{$json.statusCode}}
```

### Add Success Logging
```
Node Type: HTTP Request (or Google Sheets)
Purpose: Log successful translations

If using Google Sheets:
- Sheet columns: Date, Source Post ID, Target Language, Translated Post ID, Status
- Append row with each successful translation
```

---

## Step 7: Optimize for Large Content

### Handle Long Posts (>4000 tokens)

**Add Content Chunking:**
```
Node Type: Function
Name: Split Long Content

JavaScript Code:

const item = $input.item.json;
const maxChunkLength = 3000; // characters

function chunkText(text, maxLength) {
  const chunks = [];
  let currentChunk = '';
  const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
  
  for (let sentence of sentences) {
    if ((currentChunk + sentence).length > maxLength) {
      if (currentChunk) chunks.push(currentChunk.trim());
      currentChunk = sentence;
    } else {
      currentChunk += sentence;
    }
  }
  
  if (currentChunk) chunks.push(currentChunk.trim());
  return chunks;
}

const contentChunks = chunkText(item.clean_content, maxChunkLength);

return contentChunks.map((chunk, index) => ({
  json: {
    ...item,
    content_chunk: chunk,
    chunk_index: index,
    total_chunks: contentChunks.length
  }
}));
```

**After translation, merge chunks:**
```
Node Type: Function
Name: Merge Translated Chunks

JavaScript Code:

const items = $input.all();
const grouped = {};

// Group by source post ID
for (let item of items) {
  const postId = item.json.source_post_id;
  if (!grouped[postId]) {
    grouped[postId] = [];
  }
  grouped[postId].push(item.json);
}

const output = [];

for (let postId in grouped) {
  const chunks = grouped[postId].sort((a, b) => a.chunk_index - b.chunk_index);
  const mergedContent = chunks.map(c => c.translated_content).join('\n\n');
  
  output.push({
    json: {
      ...chunks[0],
      translated_content: mergedContent
    }
  });
}

return output;
```

---

## Step 8: Testing the Workflow

### Manual Testing Steps:

1. **Test with Single Post:**
   - Create a test post in English
   - Manually trigger workflow
   - Check translated post is created in draft status
   - Verify Polylang language switcher shows the link

2. **Verify Translation Quality:**
   - Review DeepSeek output
   - Adjust temperature/prompt if needed
   - Test with posts containing HTML, shortcodes, etc.

3. **Test Error Scenarios:**
   - Invalid API credentials
   - Network timeout
   - Malformed content
   - Very long posts

4. **Test Multiple Languages:**
   - Enable multiple target languages
   - Verify each creates separate post
   - Check all are linked correctly in Polylang

---

## Step 9: Production Deployment

### Before Going Live:

1. **Set up monitoring:**
   - Email notifications for failures
   - Daily summary of translated posts
   - API usage tracking (DeepSeek quotas)

2. **Implement rate limiting:**
   - Add delays between API calls
   - Batch processing (10 posts at a time)

3. **Create backup:**
   - Database backup before first run
   - Export all posts

4. **Schedule wisely:**
   - Run during low-traffic hours
   - Start with weekly, then daily if stable

### Workflow Settings:
```
Execution Order: Linear
Error Workflow: Send notification and continue
Timeout: 5 minutes per node
Save Execution Data: Last 100 executions

Complete N8N Workflow JSON

I can provide you with a ready-to-import N8N workflow JSON, but since it’s lengthy, here’s how to structure it:

Workflow Summary:

  1. Schedule Trigger (Cron: 0 2 * * *)
  2. HTTP Request: Get Posts (lang=en, per_page=10)
  3. Function: Filter & Prepare
  4. HTTP Request: Check Existing Translations
  5. IF: Translation exists? (No → continue)
  6. Function: Define Target Languages (loop)
  7. Function: Prepare for DeepSeek
  8. HTTP Request: DeepSeek Translate
  9. Function: Parse Translation
  10. HTTP Request: Create WP Post
  11. HTTP Request: Link Polylang Translation
  12. HTTP Request/Email: Success Notification

Advanced: Handling Edge Cases

Special Content Types:

1. ACF Fields (Advanced Custom Fields):

php

// Add to custom endpoint
$acf_fields = get_fields($source_id);
update_field('field_name', $translated_value, $target_id);

2. Yoast SEO Meta:

json

// In Create Post body
"yoast_wpseo_title": "{{$json.translated_seo_title}}",
"yoast_wpseo_metadesc": "{{$json.translated_meta_desc}}"
```

**3. Featured Images:**
```
Copy featured image ID from source to translated post
```

---

## Troubleshooting Common Issues

### Issue 1: Translations Not Linking

**Solution:** Verify Polylang language codes match exactly
```
Check: Settings → Languages → Language code
Use exact code in workflow (e.g., 'zh-tw' vs 'zh')
```

### Issue 2: DeepSeek Returns Malformed JSON

**Solution:** Improve prompt engineering
```
Add to system prompt:
"Critical: Your response must be valid JSON only. 
Start with { and end with }. No markdown, no explanations."

Issue 3: HTML Stripped Too Aggressively

Solution: Preserve important HTML

javascript

// Modified cleaning function
function preserveStructure(html) {
  // Keep paragraphs, headings, lists
  return html
    .replace(/<(p|h[1-6]|ul|ol|li)([^>]*)>/g, '<$1>')
    .replace(/<\/(p|h[1-6]|ul|ol|li)>/g, '</$1>');
}
```

### Issue 4: Rate Limiting

**Solution:** Add delays
```
Node Type: Wait
Time: 2 seconds (between DeepSeek calls)

Cost Estimation

DeepSeek API Pricing (as of January 2025):

  • Input: ~$0.14 per million tokens
  • Output: ~$0.28 per million tokens

Example:

  • 1000-word post ≈ 1,500 tokens
  • Translation to 3 languages
  • Cost: ~$0.001 per post
  • Monthly (100 posts): ~$0.10

Much cheaper than human translation!


Next Steps & Optimization

  1. Add translation memory (store previous translations)
  2. Implement glossary (consistent terminology)
  3. A/B testing (compare DeepSeek vs other AI models)
  4. Auto-publish high-confidence translations
  5. SEO optimization (translate slugs, meta descriptions)

Summary Checklist

  • WordPress Application Password created
  • DeepSeek API key obtained
  • Custom REST endpoint added to WordPress
  • N8N workflow imported and configured
  • Credentials added to N8N
  • Test run with single post successful
  • Translation linking verified in Polylang
  • Error handling tested
  • Production schedule configured
  • Monitoring/notifications set up

This complete workflow will automate your Polylang translations efficiently. Start with a small batch test, monitor the quality, and scale up gradually. The workflow is modular, so you can enhance specific parts (like adding translation memory or improving prompts) without rebuilding everything.

Would you like me to create the actual N8N workflow JSON file that you can import directly, or help you with any specific part of the implementation?

when I query like: https://yoursite.com/wp-json/wp/v2/posts?lang=zh it does not return the zh language posts instead it output all the posts I think the lang=zh query parameter does not work , though I did install the polylang on the site

9:43 AM

You’re absolutely right! The lang parameter doesn’t work by default with Polylang’s free version in the WordPress REST API. This is a common issue. Let me show you several solutions:

Solution 1: Use Polylang’s Language Taxonomy (Recommended)

Polylang stores language information as a custom taxonomy called language. Here’s how to query it properly:

Get Posts by Language Using Taxonomy

GET https://yoursite.com/wp-json/wp/v2/posts?filter[language]=zh

Or more reliably:

GET https://yoursite.com/wp-json/wp/v2/posts?language=zh

If that doesn’t work, use the term slug:

First, find your language term ID:

GET https://yoursite.com/wp-json/wp/v2/language

This returns all language terms. Find the Chinese one and note its id (e.g., 123).

Then query:

GET https://yoursite.com/wp-json/wp/v2/posts?language=123

Solution 2: Create a Custom REST API Endpoint (Most Reliable)

Add this to your theme’s functions.php or create a custom plugin:

php

<?php
/**
 * Custom REST API endpoint for Polylang posts
 */

add_action('rest_api_init', function () {
    register_rest_route('polylang/v1', '/posts', array(
        'methods' => 'GET',
        'callback' => 'get_posts_by_language',
        'permission_callback' => '__return_true',
        'args' => array(
            'lang' => array(
                'required' => true,
                'type' => 'string',
                'description' => 'Language code (e.g., en, zh, ja)',
            ),
            'per_page' => array(
                'default' => 10,
                'type' => 'integer',
            ),
            'page' => array(
                'default' => 1,
                'type' => 'integer',
            ),
            'status' => array(
                'default' => 'publish',
                'type' => 'string',
            ),
        ),
    ));
});

function get_posts_by_language($request) {
    $lang = $request->get_param('lang');
    $per_page = $request->get_param('per_page');
    $page = $request->get_param('page');
    $status = $request->get_param('status');
    
    // Check if Polylang is active
    if (!function_exists('pll_get_post_language')) {
        return new WP_Error('polylang_missing', 'Polylang is not active', array('status' => 500));
    }
    
    // Get all posts
    $args = array(
        'post_type' => 'post',
        'post_status' => $status,
        'posts_per_page' => $per_page,
        'paged' => $page,
        'lang' => $lang, // Polylang handles this in WP_Query
    );
    
    $query = new WP_Query($args);
    $posts = array();
    
    if ($query->have_posts()) {
        while ($query->have_posts()) {
            $query->the_post();
            $post_id = get_the_ID();
            
            // Get post language
            $post_lang = pll_get_post_language($post_id);
            
            // Only include posts in requested language
            if ($post_lang === $lang) {
                $posts[] = array(
                    'id' => $post_id,
                    'title' => array('rendered' => get_the_title()),
                    'content' => array('rendered' => get_the_content()),
                    'excerpt' => array('rendered' => get_the_excerpt()),
                    'slug' => get_post_field('post_name', $post_id),
                    'date' => get_the_date('c'),
                    'modified' => get_the_modified_date('c'),
                    'status' => get_post_status(),
                    'link' => get_permalink(),
                    'language' => $post_lang,
                    'categories' => wp_get_post_categories($post_id),
                    'tags' => wp_get_post_tags($post_id, array('fields' => 'ids')),
                    // Get translation IDs
                    'translations' => pll_get_post_translations($post_id),
                );
            }
        }
    }
    
    wp_reset_postdata();
    
    return rest_ensure_response($posts);
}
```

### Usage in N8N:
```
GET https://yoursite.com/wp-json/polylang/v1/posts?lang=zh&per_page=10

Solution 3: Get Posts Without Translation

This endpoint returns posts in the source language that DON’T have a translation yet:

php

<?php
/**
 * Get untranslated posts endpoint
 */

add_action('rest_api_init', function () {
    register_rest_route('polylang/v1', '/untranslated', array(
        'methods' => 'GET',
        'callback' => 'get_untranslated_posts',
        'permission_callback' => '__return_true',
        'args' => array(
            'source_lang' => array(
                'required' => true,
                'type' => 'string',
                'description' => 'Source language code',
            ),
            'target_lang' => array(
                'required' => true,
                'type' => 'string',
                'description' => 'Target language code',
            ),
            'per_page' => array(
                'default' => 10,
                'type' => 'integer',
            ),
        ),
    ));
});

function get_untranslated_posts($request) {
    $source_lang = $request->get_param('source_lang');
    $target_lang = $request->get_param('target_lang');
    $per_page = $request->get_param('per_page');
    
    if (!function_exists('pll_get_post_language')) {
        return new WP_Error('polylang_missing', 'Polylang is not active', array('status' => 500));
    }
    
    // Get all posts in source language
    $args = array(
        'post_type' => 'post',
        'post_status' => 'publish',
        'posts_per_page' => $per_page,
        'lang' => $source_lang,
    );
    
    $query = new WP_Query($args);
    $untranslated = array();
    
    if ($query->have_posts()) {
        while ($query->have_posts()) {
            $query->the_post();
            $post_id = get_the_ID();
            
            // Get translations for this post
            $translations = pll_get_post_translations($post_id);
            
            // Check if target language translation exists
            if (!isset($translations[$target_lang]) || empty($translations[$target_lang])) {
                $untranslated[] = array(
                    'id' => $post_id,
                    'title' => array('rendered' => get_the_title()),
                    'content' => array('rendered' => get_the_content()),
                    'excerpt' => array('rendered' => get_the_excerpt()),
                    'slug' => get_post_field('post_name', $post_id),
                    'date' => get_the_date('c'),
                    'status' => get_post_status(),
                    'link' => get_permalink(),
                    'language' => $source_lang,
                    'categories' => wp_get_post_categories($post_id),
                    'tags' => wp_get_post_tags($post_id, array('fields' => 'ids')),
                    'featured_media' => get_post_thumbnail_id($post_id),
                    'existing_translations' => array_keys($translations),
                    'missing_translation' => $target_lang,
                );
            }
        }
    }
    
    wp_reset_postdata();
    
    return rest_ensure_response(array(
        'total' => count($untranslated),
        'source_lang' => $source_lang,
        'target_lang' => $target_lang,
        'posts' => $untranslated,
    ));
}
```

### Usage in N8N:
```
GET https://yoursite.com/wp-json/polylang/v1/untranslated?source_lang=en&target_lang=zh&per_page=10

This is THE BEST option for your workflow because it only returns posts that actually need translation!


Solution 4: Complete Custom Endpoints Package

Here’s a complete plugin with all endpoints you need:

Create file: wp-content/plugins/polylang-automation/polylang-automation.php

php

<?php
/**
 * Plugin Name: Polylang Automation API
 * Description: REST API endpoints for automated Polylang translation workflow
 * Version: 1.0
 * Author: Your Name
 */

if (!defined('ABSPATH')) exit;

class Polylang_Automation_API {
    
    public function __construct() {
        add_action('rest_api_init', array($this, 'register_routes'));
    }
    
    public function register_routes() {
        // Get posts by language
        register_rest_route('polylang/v1', '/posts', array(
            'methods' => 'GET',
            'callback' => array($this, 'get_posts_by_language'),
            'permission_callback' => '__return_true',
        ));
        
        // Get untranslated posts
        register_rest_route('polylang/v1', '/untranslated', array(
            'methods' => 'GET',
            'callback' => array($this, 'get_untranslated_posts'),
            'permission_callback' => '__return_true',
        ));
        
        // Link translations
        register_rest_route('polylang/v1', '/link-translation', array(
            'methods' => 'POST',
            'callback' => array($this, 'link_translation'),
            'permission_callback' => array($this, 'check_permission'),
        ));
        
        // Get language info
        register_rest_route('polylang/v1', '/languages', array(
            'methods' => 'GET',
            'callback' => array($this, 'get_languages'),
            'permission_callback' => '__return_true',
        ));
        
        // Create translated post
        register_rest_route('polylang/v1', '/create-translation', array(
            'methods' => 'POST',
            'callback' => array($this, 'create_translated_post'),
            'permission_callback' => array($this, 'check_permission'),
        ));
    }
    
    public function check_permission() {
        return current_user_can('edit_posts');
    }
    
    public function get_posts_by_language($request) {
        if (!function_exists('pll_get_post_language')) {
            return new WP_Error('polylang_missing', 'Polylang is not active', array('status' => 500));
        }
        
        $lang = $request->get_param('lang');
        $per_page = $request->get_param('per_page') ?: 10;
        $page = $request->get_param('page') ?: 1;
        
        $args = array(
            'post_type' => 'post',
            'post_status' => 'publish',
            'posts_per_page' => $per_page,
            'paged' => $page,
            'lang' => $lang,
        );
        
        return $this->format_posts_response($args, $lang);
    }
    
    public function get_untranslated_posts($request) {
        if (!function_exists('pll_get_post_translations')) {
            return new WP_Error('polylang_missing', 'Polylang is not active', array('status' => 500));
        }
        
        $source_lang = $request->get_param('source_lang');
        $target_lang = $request->get_param('target_lang');
        $per_page = $request->get_param('per_page') ?: 10;
        
        $args = array(
            'post_type' => 'post',
            'post_status' => 'publish',
            'posts_per_page' => $per_page,
            'lang' => $source_lang,
        );
        
        $query = new WP_Query($args);
        $untranslated = array();
        
        if ($query->have_posts()) {
            while ($query->have_posts()) {
                $query->the_post();
                $post_id = get_the_ID();
                $translations = pll_get_post_translations($post_id);
                
                if (!isset($translations[$target_lang])) {
                    $untranslated[] = $this->format_post_data($post_id, $source_lang);
                }
            }
        }
        
        wp_reset_postdata();
        
        return rest_ensure_response($untranslated);
    }
    
    public function link_translation($request) {
        if (!function_exists('pll_save_post_translations')) {
            return new WP_Error('polylang_missing', 'Polylang is not active', array('status' => 500));
        }
        
        $source_id = $request->get_param('source_id');
        $target_id = $request->get_param('target_id');
        $source_lang = $request->get_param('source_lang');
        $target_lang = $request->get_param('target_lang');
        
        // Get existing translations
        $translations = pll_get_post_translations($source_id);
        
        // Add new translation
        $translations[$source_lang] = $source_id;
        $translations[$target_lang] = $target_id;
        
        // Save the translation group
        pll_save_post_translations($translations);
        
        return rest_ensure_response(array(
            'success' => true,
            'translations' => pll_get_post_translations($source_id),
        ));
    }
    
    public function get_languages($request) {
        if (!function_exists('pll_languages_list')) {
            return new WP_Error('polylang_missing', 'Polylang is not active', array('status' => 500));
        }
        
        $languages = pll_languages_list(array('fields' => array()));
        $formatted = array();
        
        foreach ($languages as $lang) {
            $formatted[] = array(
                'code' => $lang->slug,
                'name' => $lang->name,
                'locale' => $lang->locale,
                'is_rtl' => $lang->is_rtl,
                'flag' => $lang->flag_url,
            );
        }
        
        return rest_ensure_response($formatted);
    }
    
    public function create_translated_post($request) {
        if (!function_exists('pll_set_post_language')) {
            return new WP_Error('polylang_missing', 'Polylang is not active', array('status' => 500));
        }
        
        $source_id = $request->get_param('source_id');
        $target_lang = $request->get_param('target_lang');
        $title = $request->get_param('title');
        $content = $request->get_param('content');
        $excerpt = $request->get_param('excerpt');
        $status = $request->get_param('status') ?: 'draft';
        
        // Get source post data
        $source_post = get_post($source_id);
        if (!$source_post) {
            return new WP_Error('invalid_post', 'Source post not found', array('status' => 404));
        }
        
        // Create new post
        $new_post_data = array(
            'post_title' => $title,
            'post_content' => $content,
            'post_excerpt' => $excerpt,
            'post_status' => $status,
            'post_type' => 'post',
            'post_date' => $source_post->post_date,
        );
        
        $new_post_id = wp_insert_post($new_post_data);
        
        if (is_wp_error($new_post_id)) {
            return $new_post_id;
        }
        
        // Set language
        pll_set_post_language($new_post_id, $target_lang);
        
        // Copy categories and tags
        $categories = wp_get_post_categories($source_id);
        $tags = wp_get_post_tags($source_id, array('fields' => 'ids'));
        
        if (!empty($categories)) {
            wp_set_post_categories($new_post_id, $categories);
        }
        
        if (!empty($tags)) {
            wp_set_post_tags($new_post_id, $tags);
        }
        
        // Copy featured image
        $thumbnail_id = get_post_thumbnail_id($source_id);
        if ($thumbnail_id) {
            set_post_thumbnail($new_post_id, $thumbnail_id);
        }
        
        // Link translations
        $translations = pll_get_post_translations($source_id);
        $source_lang = pll_get_post_language($source_id);
        $translations[$source_lang] = $source_id;
        $translations[$target_lang] = $new_post_id;
        pll_save_post_translations($translations);
        
        return rest_ensure_response(array(
            'success' => true,
            'post_id' => $new_post_id,
            'post_link' => get_permalink($new_post_id),
            'translations' => pll_get_post_translations($new_post_id),
        ));
    }
    
    private function format_post_data($post_id, $lang) {
        return array(
            'id' => $post_id,
            'title' => array('rendered' => get_the_title($post_id)),
            'content' => array('rendered' => get_post_field('post_content', $post_id)),
            'excerpt' => array('rendered' => get_the_excerpt($post_id)),
            'slug' => get_post_field('post_name', $post_id),
            'date' => get_the_date('c', $post_id),
            'modified' => get_the_modified_date('c', $post_id),
            'status' => get_post_status($post_id),
            'link' => get_permalink($post_id),
            'language' => $lang,
            'categories' => wp_get_post_categories($post_id),
            'tags' => wp_get_post_tags($post_id, array('fields' => 'ids')),
            'featured_media' => get_post_thumbnail_id($post_id),
            'translations' => pll_get_post_translations($post_id),
        );
    }
    
    private function format_posts_response($args, $lang) {
        $query = new WP_Query($args);
        $posts = array();
        
        if ($query->have_posts()) {
            while ($query->have_posts()) {
                $query->the_post();
                $posts[] = $this->format_post_data(get_the_ID(), $lang);
            }
        }
        
        wp_reset_postdata();
        return rest_ensure_response($posts);
    }
}

// Initialize the plugin
new Polylang_Automation_API();
```

---

## Updated N8N Workflow Configuration

### Node 1: Get Untranslated Posts (Updated)
```
Node Type: HTTP Request
Method: GET
URL: https://yoursite.com/wp-json/polylang/v1/untranslated
Authentication: Basic Auth

Query Parameters:
  - source_lang: en
  - target_lang: zh
  - per_page: 10

Response Format: JSON
```

### Node 8: Create Translated Post (Updated - Use All-in-One Endpoint)
```
Node Type: HTTP Request
Method: POST
URL: https://yoursite.com/wp-json/polylang/v1/create-translation
Authentication: Basic Auth

Headers:
  - Content-Type: application/json

Body (JSON):
{
  "source_id": {{$json.source_post_id}},
  "target_lang": "zh",
  "title": "{{$json.translated_title}}",
  "content": "{{$json.translated_content}}",
  "excerpt": "{{$json.translated_excerpt}}",
  "status": "draft"
}

Response Format: JSON

This single endpoint now:

  • Creates the post
  • Sets the language
  • Copies categories/tags/featured image
  • Links the translation automatically

You can remove Node 9, 10, and 11 – they’re all handled in this one call!


Testing the New Endpoints

Test 1: Get Available Languages

bash

curl https://yoursite.com/wp-json/polylang/v1/languages

Test 2: Get Untranslated Posts

bash

curl "https://yoursite.com/wp-json/polylang/v1/untranslated?source_lang=en&target_lang=zh&per_page=5"

Test 3: Create Translation

bash

curl -X POST https://yoursite.com/wp-json/polylang/v1/create-translation \
  -u "username:application_password" \
  -H "Content-Type: application/json" \
  -d '{
    "source_id": 123,
    "target_lang": "zh",
    "title": "測試標題",
    "content": "測試內容",
    "excerpt": "測試摘要",
    "status": "draft"
  }'

Summary

The issue was that Polylang’s free version doesn’t add lang parameter support to the default WordPress REST API.

Best solution: Install the custom plugin above, which gives you:

  1. ✅ /untranslated – Get only posts that need translation
  2. ✅ /create-translation – One endpoint to create and link translations
  3. ✅ /languages – Get all configured languages
  4. ✅ Automatic category/tag/image copying
  5. ✅ Automatic translation linking

This dramatically simplifies your N8N workflow from 11 nodes to about 6 nodes!

Would you like me to provide the updated simplified N8N workflow JSON using these new endpoints?

use the Solution 3: Get Posts Without Translation rewite all the work flow

9:49 AM

Polylang Automation Plugin – Complete Solution

Code 

<?php /** * Plugin Name: Polylang Automation API * Description: REST API endpoints for automated Polylang translation workflow with N8N * Version: 1.0.0 * Author: Your Name * Requires at least: 5.0 * Requires PHP: 7.4 */ if (!defined(‘ABSPATH

Complete Polylang + N8N + DeepSeek Automation Workflow (Rewritten)

Using Solution 3: Get Posts Without Translation – This is the cleanest, most efficient approach.


Part 1: WordPress Setup

Step 1.1: Install the Custom Plugin

  1. Create folder: wp-content/plugins/polylang-automation/
  2. Save the artifact code above as polylang-automation.php in that folder
  3. Go to WordPress Admin → Plugins
  4. Activate “Polylang Automation API“

Step 1.2: Create WordPress Application Password

  1. Go to Users → Profile
  2. Scroll to “Application Passwords“
  3. Name: N8N Automation
  4. Click “Add New Application Password“
  5. Copy and save the generated password (format: xxxx xxxx xxxx xxxx xxxx xxxx)

Step 1.3: Verify Polylang Configuration

  1. Go to Settings → Languages
  2. Note your language codes:
    • Source language (e.g., en for English)
    • Target language(s) (e.g., zh for Chinese, ja for Japanese)

Step 1.4: Test the API Endpoints

Open your browser or use curl:

bash

# Test 1: Get available languages
https://yoursite.com/wp-json/polylang/v1/languages

# Test 2: Get untranslated posts
https://yoursite.com/wp-json/polylang/v1/untranslated?source_lang=en&target_lang=zh&per_page=5

You should see JSON responses. If you get 404 errors, check that:

  • Polylang is activated
  • The plugin is activated
  • Permalinks are set to “Post name” (Settings → Permalinks)

Part 2: DeepSeek Setup

Step 2.1: Get API Key

  1. Go to platform.deepseek.com
  2. Sign up or log in
  3. Navigate to API Keys
  4. Click “Create API Key“
  5. Copy and save your API key (format: sk-...)

Step 2.2: Test DeepSeek API

Test with curl to verify it works:

bash

curl https://api.deepseek.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "model": "deepseek-chat",
    "messages": [
      {"role": "user", "content": "Translate to Chinese: Hello World"}
    ]
  }'
```

You should receive a JSON response with the translation.

---

## Part 3: N8N Workflow - Complete Configuration

### Simplified Workflow Structure (6 Nodes)
```
[1. Schedule Trigger]
    ↓
[2. Get Untranslated Posts]
    ↓
[3. Loop Through Posts] (Split In Batches)
    ↓
[4. Translate with DeepSeek]
    ↓
[5. Parse & Create Translation]
    ↓
[6. Log Results]
```

---

### Node 1: Schedule Trigger

**Purpose:** Automatically run the workflow daily

**Node Configuration:**
```
Node Type: Schedule Trigger
Trigger Rule: Cron Expression
Cron: 0 2 * * *
(Runs every day at 2:00 AM)
```

**Alternative for Manual Testing:**
```
Node Type: Manual Trigger
(Click "Execute Workflow" to run)
```

---

### Node 2: Get Untranslated Posts

**Purpose:** Fetch all posts that need translation

**Node Configuration:**
```
Node Type: HTTP Request
Method: GET
URL: https://yoursite.com/wp-json/polylang/v1/untranslated

Authentication:
  Type: Generic Credential Type
  Generic Auth Type: Basic Auth
  User: your-wordpress-username
  Password: [Your Application Password from Step 1.2]

Query Parameters:
  source_lang: en
  target_lang: zh
  per_page: 10

Headers:
  Content-Type: application/json

Options:
  Response Format: JSON
  Full Response: OFF
  Ignore SSL Issues: OFF (unless testing locally)

Expected Response:

json

{
  "total": 5,
  "source_lang": "en",
  "target_lang": "zh",
  "posts": [
    {
      "id": 123,
      "title": {"rendered": "My Post Title", "raw": "My Post Title"},
      "content": {"rendered": "<p>Content...</p>", "raw": "Content..."},
      "excerpt": {"rendered": "Excerpt...", "raw": "Excerpt..."},
      ...
    }
  ]
}
```

---

### Node 3: Split Posts (Extract from Response)

**Purpose:** Extract individual posts from the response array

**Node Configuration:**
```
Node Type: Code
Mode: Run Once for All Items
Language: JavaScript

Code:
const response = $input.first().json;

// Check if we have posts to process
if (!response.posts || response.posts.length === 0) {
  console.log('No untranslated posts found');
  return [];
}

// Return each post as a separate item
return response.posts.map(post => ({
  json: {
    source_post_id: post.id,
    source_title_raw: post.title.raw,
    source_content_raw: post.content.raw,
    source_excerpt_raw: post.excerpt.raw || '',
    source_slug: post.slug,
    source_link: post.link,
    target_language: response.target_lang,
    source_language: response.source_lang
  }
}));
```

**What this does:**
- Extracts the `posts` array
- Creates one output item per post
- Prepares clean data for translation

---

### Node 4: Translate with DeepSeek

**Purpose:** Send content to DeepSeek for translation

**Node Configuration:**
```
Node Type: HTTP Request
Method: POST
URL: https://api.deepseek.com/v1/chat/completions

Authentication:
  Type: Generic Credential Type
  Generic Auth Type: Header Auth
  Name: Authorization
  Value: Bearer YOUR_DEEPSEEK_API_KEY

Headers:
  Content-Type: application/json

Body:
  Content-Type: JSON

Body JSON:
{
  "model": "deepseek-chat",
  "messages": [
    {
      "role": "system",
      "content": "You are a professional translator. Translate WordPress post content from English to Traditional Chinese (zh-TW). Maintain the original tone, style, and formatting. Return ONLY a valid JSON object with exactly these keys: title, content, excerpt. Do not include markdown code blocks, explanations, or any other text outside the JSON object."
    },
    {
      "role": "user",
      "content": "Translate this WordPress post:\n\nTitle: {{ $json.source_title_raw }}\n\nContent: {{ $json.source_content_raw }}\n\nExcerpt: {{ $json.source_excerpt_raw }}"
    }
  ],
  "temperature": 0.3,
  "max_tokens": 4000,
  "stream": false
}

Options:
  Response Format: JSON
```

**Important Notes:**
- `temperature: 0.3` = More consistent/literal translations
- `temperature: 0.7` = More creative/natural translations
- Adjust `max_tokens` based on your content length (4000 handles ~3000 words)

---

### Node 5: Parse Translation & Create Post

**Purpose:** Extract translation and create WordPress post in one step

**Node Configuration:**
```
Node Type: Code
Mode: Run Once for Each Item
Language: JavaScript

Code:
// Get the DeepSeek response
const deepseekResponse = $input.item.json;
const sourceData = $node["Split Posts"].json;

// Extract the translated text from DeepSeek
let translatedText = deepseekResponse.choices[0].message.content;

// Remove markdown code blocks if DeepSeek added them
translatedText = translatedText.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();

// Parse the translation JSON
let translation;
try {
  translation = JSON.parse(translatedText);
} catch (error) {
  console.error('Failed to parse DeepSeek response:', error);
  console.error('Raw response:', translatedText);
  
  // Fallback: use entire response as content
  translation = {
    title: sourceData.source_title_raw + ' (翻譯)',
    content: translatedText,
    excerpt: translatedText.substring(0, 200) + '...'
  };
}

// Return formatted data for WordPress API
return {
  json: {
    source_id: sourceData.source_post_id,
    target_lang: sourceData.target_language,
    title: translation.title,
    content: translation.content,
    excerpt: translation.excerpt || '',
    status: 'draft' // Change to 'publish' if you want auto-publish
  }
};
```

---

### Node 6: Create WordPress Translation

**Purpose:** Create the translated post and link it in Polylang

**Node Configuration:**
```
Node Type: HTTP Request
Method: POST
URL: https://yoursite.com/wp-json/polylang/v1/create-translation

Authentication:
  Type: Generic Credential Type
  Generic Auth Type: Basic Auth
  User: your-wordpress-username
  Password: [Your Application Password]

Headers:
  Content-Type: application/json

Body:
  Content-Type: JSON

Body JSON:
{
  "source_id": {{ $json.source_id }},
  "target_lang": "{{ $json.target_lang }}",
  "title": "{{ $json.title }}",
  "content": "{{ $json.content }}",
  "excerpt": "{{ $json.excerpt }}",
  "status": "{{ $json.status }}"
}

Options:
  Response Format: JSON

This single endpoint handles:

  • ✅ Creating the new post
  • ✅ Setting the correct language
  • ✅ Copying categories and tags
  • ✅ Copying featured image
  • ✅ Linking the translation in Polylang

Expected Response:

json

{
  "success": true,
  "post_id": 456,
  "post_link": "https://yoursite.com/zh/translated-post/",
  "edit_link": "https://yoursite.com/wp-admin/post.php?post=456&action=edit",
  "source_id": 123,
  "target_lang": "zh",
  "status": "draft",
  "translations": {
    "en": 123,
    "zh": 456
  }
}
```

---

### Node 7: Log Success (Optional but Recommended)

**Purpose:** Track successful translations

**Option A: Google Sheets Logging**
```
Node Type: Google Sheets
Operation: Append
Sheet: Translation Log
Columns:
  - Timestamp: {{ $now }}
  - Source ID: {{ $json.source_id }}
  - New Post ID: {{ $json.post_id }}
  - Language: {{ $json.target_lang }}
  - Status: {{ $json.status }}
  - Link: {{ $json.post_link }}
```

**Option B: Email Notification**
```
Node Type: Send Email
To: [email protected]
Subject: Translation Complete - {{ $json.post_id }}
Message:
Successfully translated post!

Source: {{ $('Node 6').item.json.source_id }}
New Post: {{ $json.post_id }}
Language: {{ $json.target_lang }}
Status: {{ $json.status }}

View: {{ $json.edit_link }}
```

**Option C: Simple HTTP Log**
```
Node Type: Code
Code:
const result = $input.item.json;
console.log('✅ Translation created:', {
  source: result.source_id,
  new_id: result.post_id,
  lang: result.target_lang,
  link: result.post_link
});

return { json: result };
```

---

## Part 4: Error Handling

### Add Error Workflow

Create a separate workflow for error notifications:

**Error Workflow:**
```
Node 1: Error Trigger
Node 2: Send Email/Slack Notification
```

**In your main workflow settings:**
```
Settings → Error Workflow → [Select your error workflow]
```

### Add Conditional Error Handling After Node 6

**Node 6a: Check Success**
```
Node Type: IF
Condition:
  {{ $json.success }} is equal to Boolean true

TRUE branch: Continue to Node 7 (Logging)
FALSE branch: Send error notification
```

**Node 6b: Error Notification (on FALSE branch)**
```
Node Type: Send Email
To: [email protected]
Subject: ⚠️ Translation Failed
Message:
Failed to create translation

Source Post: {{ $('Node 5').item.json.source_id }}
Target Language: {{ $('Node 5').item.json.target_lang }}
Error: {{ $json.error || 'Unknown error' }}

Please check the WordPress logs.
```

---

## Part 5: Handle Multiple Target Languages

### Modify Node 2 to Loop Multiple Languages

Replace **Node 2** with this enhanced version:

**Node 2a: Define Target Languages**
```
Node Type: Code
Code:
// Define which languages to translate to
const targetLanguages = ['zh', 'ja', 'es']; // Chinese, Japanese, Spanish
const sourceLang = 'en';

return targetLanguages.map(lang => ({
  json: {
    source_lang: sourceLang,
    target_lang: lang
  }
}));
```

**Node 2b: Get Untranslated for Each Language**
```
Node Type: HTTP Request
URL: https://yoursite.com/wp-json/polylang/v1/untranslated
Query Parameters:
  source_lang: {{ $json.source_lang }}
  target_lang: {{ $json.target_lang }}
  per_page: 10

Update Node 4 (DeepSeek) system message:

json

{
  "role": "system",
  "content": "You are a professional translator. Translate from English to {{ $json.target_language_name }}. Language code: {{ $json.target_language }}. Return only JSON with keys: title, content, excerpt."
}
```

**Add language mapping before Node 4:**
```
Node Type: Code
Code:
const langMap = {
  'zh': 'Traditional Chinese (zh-TW)',
  'ja': 'Japanese (ja-JP)',
  'es': 'Spanish (es-ES)',
  'fr': 'French (fr-FR)',
  'de': 'German (de-DE)'
};

return {
  json: {
    ...$input.item.json,
    target_language_name: langMap[$input.item.json.target_language] || $input.item.json.target_language
  }
};
```

---

## Part 6: Handle Long Content (Advanced)

For posts longer than 3000 words, add content chunking:

**Node 4a: Check Content Length**
```
Node Type: IF
Condition: {{ $json.source_content_raw.length }} greater than 10000

TRUE: Go to chunking logic
FALSE: Continue normal translation
```

**Node 4b: Chunk Long Content**
```
Node Type: Code
Code:
const content = $input.item.json.source_content_raw;
const maxLength = 8000;

function chunkBySentences(text, maxLen) {
  const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
  const chunks = [];
  let current = '';
  
  for (const sentence of sentences) {
    if ((current + sentence).length > maxLen) {
      if (current) chunks.push(current.trim());
      current = sentence;
    } else {
      current += sentence;
    }
  }
  
  if (current) chunks.push(current.trim());
  return chunks;
}

const chunks = chunkBySentences(content, maxLength);

return chunks.map((chunk, i) => ({
  json: {
    ...$input.item.json,
    content_chunk: chunk,
    chunk_number: i + 1,
    total_chunks: chunks.length
  }
}));
```

**Node 4c: Merge Translated Chunks**
```
Node Type: Code
Mode: Run Once for All Items
Code:
// Group chunks by source post ID
const grouped = {};

$input.all().forEach(item => {
  const id = item.json.source_post_id;
  if (!grouped[id]) grouped[id] = [];
  grouped[id].push(item.json);
});

// Merge chunks for each post
return Object.values(grouped).map(chunks => {
  chunks.sort((a, b) => a.chunk_number - b.chunk_number);
  
  const merged = chunks[0];
  merged.content = chunks.map(c => c.translated_content).join('\n\n');
  delete merged.content_chunk;
  delete merged.chunk_number;
  delete merged.total_chunks;
  
  return { json: merged };
});

Part 7: Testing the Complete Workflow

Pre-Flight Checklist

  • WordPress plugin activated
  • Application password created
  • DeepSeek API key obtained
  • N8N credentials configured
  • Test post created in source language

Step-by-Step Testing

Test 1: Manual Trigger

  1. In N8N, click “Execute Workflow“
  2. Check execution log for errors
  3. Verify each node shows green checkmark
  4. Look for output data in each node

Test 2: Verify API Endpoints

bash

# Test untranslated endpoint directly
curl "https://yoursite.com/wp-json/polylang/v1/untranslated?source_lang=en&target_lang=zh&per_page=1" \
  -u "username:app_password"
```

**Test 3: Check WordPress**
1. Go to WordPress Admin → Posts
2. Switch to Chinese language (in Polylang dropdown)
3. You should see the translated post in Draft status
4. Open the post and verify:
   - Title is translated
   - Content is translated
   - Categories/tags are copied
   - Featured image is copied
   - Language is set to Chinese

**Test 4: Verify Translation Link**
1. Edit the original English post
2. Look for Polylang translation box (usually in sidebar)
3. You should see a link to the Chinese translation
4. Click it to verify bidirectional linking works

---

## Part 8: Production Deployment

### Recommended Settings

**Workflow Settings in N8N:**
```
Name: Polylang Auto Translation
Timezone: Your timezone
Save Execution Data: Last 50 executions
Error Workflow: [Your error workflow]
Timeout: 10 minutes
```

**Schedule:**
- **Development:** Manual trigger only
- **Staging:** Daily at 2 AM
- **Production:** Daily at 2 AM (or when traffic is lowest)

### Performance Optimization

**1. Batch Size Control**
```
Node 2 query parameter:
per_page: 5  // Start small, increase gradually
```

**2. Add Delays Between API Calls**
```
After Node 4 (DeepSeek), add:
Node Type: Wait
Time: 2 seconds
```

**3. Rate Limiting Protection**
```
Node Type: Code (before Node 4)
Code:
// Check if we've hit rate limits
const callCount = $executionId; // Simple counter
if (callCount > 50) {
  throw new Error('Rate limit protection: Too many calls in this execution');
}

Part 9: Monitoring & Maintenance

Daily Monitoring Checklist

  1. Check N8N execution log (last 24 hours)
  2. Review WordPress drafts (verify quality)
  3. Monitor DeepSeek usage (check API costs)
  4. Verify translation links (random sample)

Weekly Tasks

  1. Publish approved translations
  2. Update translation prompts if quality issues
  3. Review error logs
  4. Check for WordPress/plugin updates

Monthly Tasks

  1. Analyze translation quality trends
  2. Optimize DeepSeek prompts
  3. Review and adjust batch sizes
  4. Update language pairs if needed

Part 10: Troubleshooting Guide

Issue 1: “No untranslated posts found” but posts exist

Diagnosis:

bash

# Check if posts have correct language
curl "https://yoursite.com/wp-json/wp/v2/posts?per_page=5"
```

**Solution:**
- Ensure source posts have language set in Polylang
- Go to each post → Polylang box → Select "English"
- Re-save the post

---

### Issue 2: DeepSeek returns invalid JSON

**Check Node 4 output:**
```
Look for: ```json or other markdown
```

**Solution 1: Improve prompt**
```
Add to system message:
"CRITICAL: Response must be pure JSON. Start with { and end with }. 
No markdown, no code blocks, no explanations."

Solution 2: Enhanced parsing

javascript

// In Node 5
let translatedText = response.choices[0].message.content;

// Remove ALL possible markdown
translatedText = translatedText
  .replace(/^```json\s*/i, '')
  .replace(/^```\s*/i, '')
  .replace(/```\s*$/i, '')
  .trim();

// Extract JSON if surrounded by other text
const jsonMatch = translatedText.match(/\{[\s\S]*\}/);
if (jsonMatch) {
  translatedText = jsonMatch[0];
}

Issue 3: Translation quality is poor

Solutions:

1. Adjust temperature:

json

"temperature": 0.2  // More literal
"temperature": 0.5  // Balanced
"temperature": 0.7  // More creative
```

**2. Add context to prompt:**
```
"Translate this professional blog post from English to Chinese. 
The content is about [YOUR TOPIC]. 
Maintain a [formal/casual/technical] tone. 
Use [simplified/traditional] Chinese characters."
```

**3. Add examples (few-shot learning):**
```
"Here's an example of the style:
English: 'Our product helps you...'
Chinese: '我們的產品幫助您...'

Now translate: ..."
```

---

### Issue 4: Categories/Tags not translating

**Problem:** Polylang requires term translations to be set up

**Solution:**
1. Go to **Taxonomies** in Polylang
2. For each category in English, create corresponding Chinese category
3. Link them using Polylang's interface
4. Re-run the workflow

**Alternative:** Modify the plugin to auto-create term translations

---

### Issue 5: Rate limiting errors

**DeepSeek Error:** `429 Too Many Requests`

**Solution:**
```
Add wait time in Node 4:
Settings → Batch Size: 1
Settings → Time Between Batches: 3000ms (3 seconds)
```

**WordPress Error:** `429 Too Many Requests`

**Solution:**
- Check with your hosting provider about API rate limits
- Add caching plugin (WP Super Cache)
- Increase batch delay

---

## Part 11: Cost Analysis

### DeepSeek Pricing (January 2025)

**Model:** `deepseek-chat`
- Input: ~$0.14 per 1M tokens
- Output: ~$0.28 per 1M tokens

**Example Calculation:**
```
Average blog post: 1,000 words = ~1,500 tokens
Translation output: ~1,500 tokens

Cost per post:
Input: 1,500 tokens × $0.14/1M = $0.00021
Output: 1,500 tokens × $0.28/1M = $0.00042
Total per post: ~$0.0006

Monthly (100 posts × 3 languages):
300 translations × $0.0006 = $0.18/month

Compared to human translation:

  • Human: $0.10-0.20 per word = $100-200 per 1000-word post
  • DeepSeek: $0.0006 per post
  • Savings: 99.9%+

Part 12: Advanced Features (Optional)

Feature 1: Translation Memory

Store previously translated phrases for consistency:

php

// Add to WordPress plugin
add_action('rest_api_init', function() {
    register_rest_route('polylang/v1', '/translation-memory', array(
        'methods' => 'GET',
        'callback' => 'get_translation_memory',
    ));
});

function get_translation_memory($request) {
    $source_lang = $request->get_param('source_lang');
    $target_lang = $request->get_param('target_lang');
    
    // Get all translated posts
    $memory = array();
    $posts = get_posts(array('lang' => $target_lang, 'posts_per_page' => -1));
    
    foreach ($posts as $post) {
        $translations = pll_get_post_translations($post->ID);
        if (isset($translations[$source_lang])) {
            $source_post = get_post($translations[$source_lang]);
            $memory[] = array(
                'source' => $source_post->post_title,
                'translation' => $post->post_title,
            );
        }
    }
    
    return $memory;
}

Use in N8N: Include memory in DeepSeek prompt for consistency


Feature 2: Quality Scoring

Add automatic quality checks:

javascript

// In Node 5, after parsing translation
function checkQuality(original, translated) {
  const issues = [];
  
  // Check if translation is too short
  if (translated.content.length < original.content.length * 0.5) {
    issues.push('Translation seems too short');
  }
  
  // Check if translation is same as original (not translated)
  if (translated.title === original.title) {
    issues.push('Title not translated');
  }
  
  // Check for English words in Chinese translation (basic check)
  if (translated.content.match(/\b[a-zA-Z]{4,}\b/g)?.length > 5) {
    issues.push('Too many English words in translation');
  }
  
  return {
    score: issues.length === 0 ? 100 : 100 - (issues.length * 20),
    issues: issues
  };
}

const quality = checkQuality(sourceData, translation);

// Only create post if quality is good
if (quality.score < 60) {
  throw new Error(`Low quality translation: ${quality.issues.join(', ')}`);
}

Feature 3: Auto-Publish High-Quality Translations

javascript

// In Node 5
const quality = checkQuality(sourceData, translation);

return {
  json: {
    ...data,
    status: quality.score >= 90 ? 'publish' : 'draft',
    quality_score: quality.score
  }
};

Summary: Your Complete Workflow

What You’ve Built

✅ Fully automated translation system

  • Detects untranslated posts
  • Translates with AI (DeepSeek)
  • Creates properly linked Polylang posts
  • Handles multiple languages
  • Costs ~$0.0006 per post

✅ WordPress integration

  • Custom REST API endpoints
  • Automatic category/tag copying
  • Featured image preservation
  • Proper Polylang linking

✅ Error handling

  • Failed translation detection
  • Email notifications
  • Execution logging

✅ Scalable architecture

  • Handles long content via chunking
  • Rate limiting protection
  • Batch processing

Next Steps

  1. Start with test environment
    • Create 5 test posts in English
    • Run workflow manually
    • Review translation quality
  2. Tune for your needs
    • Adjust DeepSeek temperature
    • Customize translation prompts
    • Set appropriate batch sizes
  3. Go production
    • Enable scheduled trigger
    • Monitor for first week
    • Gradually increase batch size
  4. Optimize over time
    • Track translation quality
    • Build translation memory
    • Add more languages

第二步

deepseek翻译页面 提示词:

// Enhanced DeepSeek Translation Prompt
// Use this in the “DeepSeek Translate” node’s messages parameter

const sourceLanguage = $(‘Set Language Config’).item.json.source_lang;
const targetLanguage = $json.target_languages[$(‘Loop Target Languages’).context.currentItem];
const postData = $(‘Get Post Content’).item.json.post;

// Language name mapping for better context
const languageNames = {
‘en’: ‘English’,
‘es’: ‘Spanish’,
‘fr’: ‘French’,
‘de’: ‘German’,
‘it’: ‘Italian’,
‘pt’: ‘Portuguese’,
‘zh’: ‘Chinese’,
‘ja’: ‘Japanese’,
‘ko’: ‘Korean’,
‘ar’: ‘Arabic’,
‘ru’: ‘Russian’,
‘nl’: ‘Dutch’,
‘pl’: ‘Polish’,
‘tr’: ‘Turkish’,
‘vi’: ‘Vietnamese’,
‘th’: ‘Thai’,
‘id’: ‘Indonesian’,
‘sv’: ‘Swedish’,
‘da’: ‘Danish’,
‘no’: ‘Norwegian’,
‘fi’: ‘Finnish’,
};

const sourceLangName = languageNames[sourceLanguage] || sourceLanguage;
const targetLangName = languageNames[targetLanguage] || targetLanguage;

// Determine post type for context
const postType = postData.type;
const postTypeContext = {
‘post’: ‘blog post’,
‘page’: ‘web page’,
‘product’: ‘product description’,
‘event’: ‘event listing’,
};

const contextType = postTypeContext[postType] || ‘content’;

// System prompt with detailed instructions
const systemPrompt = `You are a professional translator specializing in ${targetLangName} translations for web content.

TRANSLATION TASK:

  • Translate from ${sourceLangName} to ${targetLangName}
  • Content type: WordPress ${contextType}
  • Maintain professional, natural-sounding ${targetLangName}

CRITICAL RULES:

  1. PRESERVE ALL HTML TAGS EXACTLY – Do not modify, remove, or add HTML tags
  2. PRESERVE FORMATTING – Keep line breaks, paragraphs, lists, and structure
  3. PRESERVE SPECIAL CHARACTERS – Keep  , —, etc. as-is
  4. PRESERVE SHORTCODES – Keep [shortcode] syntax unchanged
  5. PRESERVE URLs – Do not translate URLs, links, or anchor tags
  6. PRESERVE PLACEHOLDER TEXT – Keep {{placeholders}} or {variables} unchanged
  7. NATURAL TRANSLATION – Use natural, native ${targetLangName} expressions, not word-for-word
  8. CULTURAL ADAPTATION – Adapt idioms and expressions appropriately for ${targetLangName} audience
  9. SEO AWARENESS – Maintain keyword relevance while ensuring natural flow
  10. CONSISTENCY – Use consistent terminology throughout

SPECIAL HANDLING:

  • Brand names: Keep in original language unless official translation exists
  • Technical terms: Translate only if common translation exists in ${targetLangName}
  • Numbers and dates: Format according to ${targetLangName} conventions
  • Currency: Convert symbols appropriately (e.g., $ to € for European languages if contextual)

OUTPUT FORMAT:
Return ONLY a valid JSON object with these exact keys:
{
“title”: “translated title here”,
“content”: “translated content here with all HTML preserved”,
“excerpt”: “translated excerpt here”
}

QUALITY STANDARDS:

  • Read as if written by a native ${targetLangName} speaker
  • Appropriate tone and formality for the content type
  • No awkward literal translations
  • No translation artifacts or notes`;

// User message with content
const userMessage = JSON.stringify({
title: postData.title,
content: postData.content,
excerpt: postData.excerpt || ”
});

// Return formatted messages array
return [
{
role: “system”,
content: systemPrompt
},
{
role: “user”,
content: userMessage
}
];

×
使用n8n+deepseek进行翻译最强教程(文章很长)

使用n8n+deepseek进行翻译最强教程(文章很长)

第一步 Complete N8N Wor...

查看更多

AI agent 如何翻译网站(标签+动态内容)

先让AI把网站进行i18n化, …