第一步
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:
- Fetch untranslated posts from WordPress
- Send content to DeepSeek for translation
- Create translated posts in Polylang’s required format
- 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
- Go to WordPress Admin → Users → Profile
- Scroll to “Application Passwords”
- Enter name: “N8N Automation”
- Click “Add New Application Password”
- 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
- Sign up at platform.deepseek.com
- Navigate to API Keys section
- Create new API key
- 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
Webhooknode 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_tokensbased 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:
- Schedule Trigger (Cron: 0 2 * * *)
- HTTP Request: Get Posts (lang=en, per_page=10)
- Function: Filter & Prepare
- HTTP Request: Check Existing Translations
- IF: Translation exists? (No → continue)
- Function: Define Target Languages (loop)
- Function: Prepare for DeepSeek
- HTTP Request: DeepSeek Translate
- Function: Parse Translation
- HTTP Request: Create WP Post
- HTTP Request: Link Polylang Translation
- 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
- Add translation memory (store previous translations)
- Implement glossary (consistent terminology)
- A/B testing (compare DeepSeek vs other AI models)
- Auto-publish high-confidence translations
- 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:
- ✅
/untranslated– Get only posts that need translation - ✅
/create-translation– One endpoint to create and link translations - ✅
/languages– Get all configured languages - ✅ Automatic category/tag/image copying
- ✅ 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
- Create folder:
wp-content/plugins/polylang-automation/ - Save the artifact code above as
polylang-automation.phpin that folder - Go to WordPress Admin → Plugins
- Activate “Polylang Automation API“
Step 1.2: Create WordPress Application Password
- Go to Users → Profile
- Scroll to “Application Passwords“
- Name:
N8N Automation - Click “Add New Application Password“
- Copy and save the generated password (format:
xxxx xxxx xxxx xxxx xxxx xxxx)
Step 1.3: Verify Polylang Configuration
- Go to Settings → Languages
- Note your language codes:
- Source language (e.g.,
enfor English) - Target language(s) (e.g.,
zhfor Chinese,jafor Japanese)
- Source language (e.g.,
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
- Go to platform.deepseek.com
- Sign up or log in
- Navigate to API Keys
- Click “Create API Key“
- 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
- In N8N, click “Execute Workflow“
- Check execution log for errors
- Verify each node shows green checkmark
- 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
- Check N8N execution log (last 24 hours)
- Review WordPress drafts (verify quality)
- Monitor DeepSeek usage (check API costs)
- Verify translation links (random sample)
Weekly Tasks
- Publish approved translations
- Update translation prompts if quality issues
- Review error logs
- Check for WordPress/plugin updates
Monthly Tasks
- Analyze translation quality trends
- Optimize DeepSeek prompts
- Review and adjust batch sizes
- 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
- Start with test environment
- Create 5 test posts in English
- Run workflow manually
- Review translation quality
- Tune for your needs
- Adjust DeepSeek temperature
- Customize translation prompts
- Set appropriate batch sizes
- Go production
- Enable scheduled trigger
- Monitor for first week
- Gradually increase batch size
- 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:
- PRESERVE ALL HTML TAGS EXACTLY – Do not modify, remove, or add HTML tags
- PRESERVE FORMATTING – Keep line breaks, paragraphs, lists, and structure
- PRESERVE SPECIAL CHARACTERS – Keep , —, etc. as-is
- PRESERVE SHORTCODES – Keep [shortcode] syntax unchanged
- PRESERVE URLs – Do not translate URLs, links, or anchor tags
- PRESERVE PLACEHOLDER TEXT – Keep {{placeholders}} or {variables} unchanged
- NATURAL TRANSLATION – Use natural, native ${targetLangName} expressions, not word-for-word
- CULTURAL ADAPTATION – Adapt idioms and expressions appropriately for ${targetLangName} audience
- SEO AWARENESS – Maintain keyword relevance while ensuring natural flow
- 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
}
];
WP开发教程 WordPress导师在线教程