Wer schon einmal um 23 Uhr einen Absatz manuell in Google Translate eingefügt hat, um schnell eine englische Version zu veröffentlichen, weiß, wo das Problem liegt: Es ist langsam, unzuverlässig und läuft letztendlich auf ein erneutes Kopieren und Einfügen im Editor hinaus. WordPress ohne jegliche Rückverfolgbarkeit.
WordPress Version 6.9.4 (April 2026) bietet bereits gute Werkzeuge (REST-API, Transienten, Hooks), übersetzt aber noch nichts nativ. Die Idee ist einfach: eine KI-Übersetzungs-API anbinden über wp_remote_post()ohne Installation von Plugins und unter Beibehaltung der Kontrolle über Caching, Kosten und Sicherheit.
Der Bedarf / Der Anwendungsfall
Das konkrete Problem: Sie haben Inhalte (Artikel(Seiten, manchmal ACF-Felder) und Sie möchten schnell eine übersetzte Version in akzeptabler Qualität generieren, ohne ein aufwändiges (und oft teures) Übersetzungs-Plugin hinzuzufügen, das Ihre Datenbank und Ihr Backend verändert.
Ich habe dieses Bedürfnis oft bei folgenden Personen beobachtet:
- Französische Technikblogs, die eine für Long-Tail-SEO „ausreichende“ englische Version wünschen,
- Showcase-Websites (Avada/Divi/Elementor), bei denen 10 Seiten in 2 Sprachen vorhanden sein müssen.
- Websites mit sehr stabilem Inhalt (Dokumente, Rechtstexte), bei denen sich die Übersetzung fast nie ändert.
Am Ende werden Sie wissen, wie Sie es umsetzen:
- ein sicherer REST-Endpunkt zum Anfordern einer Übersetzung auf der Administratorseite.
- eine KI-Übersetzungs-Engine (z. B. OpenAI) über
wp_remote_post(), - Ein Cache pro Beitrag + Sprache mit Transienten,
- eine „on-the-fly“-Darstellung (ohne Duplikate von Beiträgen) über einen Filter auf
the_content(optional), - eine saubere Ausweichstrategie, falls die API langsam ist oder nicht erreur.
Kurze Zusammenfassung
- Sie speichern den API-Schlüssel in
wp-config.php(niemals in fester Form). - Sie erstellen ein Mini-Plugin (oder Mu-Plugin), das einen REST-Endpunkt bereitstellt, der nur für Administratoren zugänglich ist.
- Das Plugin ruft die KI-API auf mit
wp_remote_post()+ Timeout + Fehlerbehandlung. - Die Übersetzung wird zwischengespeichert über
set_transient()(pro Beitrag/Sprache + Inhalts-Hash). - Option 1: Anzeige in Echtzeit (ohne Duplikate). Option 2: Generierung und Speicherung in Metadaten.
- Sie fügen Ratenbegrenzung + HTML-Bereinigung hinzu (
wp_kses_post()) um unangenehme Überraschungen zu vermeiden.
Wann sollte man KI dafür einsetzen?
Diese Vorgehensweise ist anzuwenden, wenn:
- Sie wünschen sich eine „gute“ Übersetzung, ohne eine vollständige mehrsprachige Infrastruktur aufzubauen.
- Der Inhalt ist überwiegend textuell (Artikel, Seiten).
- Sie akzeptieren eine unvollkommene, aber zusammenhängende Übersetzung, wenn Sie ein Glossar bereitstellen.
- Sie möchten die Kosten kontrollieren (aggressives Caching, Übersetzung auf Abruf).
- Sie möchten sich nicht auf ein Plugin verlassen, das sein eigenes Datenmodell vorgibt.
Meiner Erfahrung nach funktioniert es sehr gut für redaktionelle Websites, bei denen 80 % der Seiten Artikel sind und die Übersetzung hauptsächlich zur Kundengewinnung (SEO + internationale Leser) und nicht zur strikten rechtlichen Lokalisierung eingesetzt wird.
Wann man KI NICHT einsetzen sollte
Vermeiden Sie KI (oder beschränken Sie deren Einsatz), wenn:
- Sie unterliegen rechtlichen Verpflichtungen (Geschäftsbedingungen, medizinische, finanzielle): Eine unkorrigierte KI-Übersetzung birgt ein Risiko.
- Sie haben bereits WPML/Polylang im Einsatz und eine echte Mehrsprachigkeitsstrategie (URLs pro Sprache, hreflang, Menüs usw.).
- Sie müssen die Interface-Strings (Theme-Strings) übersetzen: Am besten eignet sich hierfür ein spezielles Tool (gettext).
- Sie müssen hochdynamische Inhalte (Kommentare, UGC) übersetzen: Kosten + DSGVO + Moderation.
- Sie haben eine sehr große Website (über 10 Beiträge) und denken, Sie könnten "alles auf einmal übersetzen": Die Rechnung und die Quoten werden Sie beruhigen.
Eine einfachere, „klassische“ Alternative: Wenn Sie lediglich je nach Sprache unterschiedliche Inhalte anzeigen möchten, ist das manuelle Duplizieren von zehn Seiten plus Menü für jede Sprache oft immer noch die beste Lösung. Künstliche Intelligenz ist vor allem dann nützlich, wenn Sie Prozesse automatisieren möchten, ohne Ihre gesamte Website zu überarbeiten.
Voraussetzungen
Versionen
- WordPress 6.9.4+ (April 2026)
- PHP 8.1+ (8.2/8.3 empfohlen, falls Ihr Hosting-Anbieter dies unterstützt)
- HTTPS muss aktiviert sein (ansonsten ist der API-Aufruf keine gute Idee).
API-Schlüssel (OpenAI-Beispiel)
Die API wird über HTTP genutzt. Zunächst sind weder ein SDK noch Composer verfügbar. Offizielle Dokumentation:
- wp_remote_post() (WordPress-Entwicklerressourcen)
- REST-API-Handbuch (WordPress)
- OpenAI API-Referenz
- JSON-Erweiterung (php.net)
- Nonces (WordPress-Sicherheit)
Der Schlüssel ist in wp-config.php gespeichert.
Füge dies hinzu zu wp-config.phpIdealerweise über eine vom Hosting-Anbieter eingefügte Umgebungsvariable (noch besser), andernfalls fest codiert in wp-config.php (Akzeptabel, wenn die Datei gut geschützt ist).
/** Clé API OpenAI - ne jamais commiter ce fichier dans un dépôt public */
define('BPCAB_OPENAI_API_KEY', 'sk-REMPLACEZ-MOI');
/** Modèle de traduction (à ajuster selon votre fournisseur) */
define('BPCAB_TRANSLATION_MODEL', 'gpt-4.1-mini');
Klassische Falle: diese Konstante einfügen in functions.phpIch sehe es immer noch auf Divi/Avada-Websites: Beim ersten Theme-Wechsel „verschwindet“ die Taste. Füge sie ein wp-config.php oder als Umgebungsvariable.
Lösungsarchitektur
Ablauf (textuelles Schema):
WordPress-Admin → REST-API (sicherer Endpunkt) →
wp_remote_post()→ KI-API (Übersetzung) → Validierung + Bereinigung → Cache (transient + Meta-Option) → JSON-Rückgabe → Anzeige (optional über Filter)
Was geschieht hinter den Kulissen?
- Entrée : a
post_id, eine Ausgangssprache, eine Zielsprache und gegebenenfalls ein „Glossar“. - Extrahierung Wir rufen den Inhalt (und auf Wunsch auch den Titel) ab und bereiten ihn anschließend auf (HTML bleibt erhalten).
- Cache Wir berechnen einen Cache-Schlüssel anhand des Inhalts-Hashs und der Zielsprache. Wenn sich dieser nicht geändert hat, fallen keine Kosten für die API an.
- API-Aufruf : JSON-Anfrage, angemessenes Timeout, minimale Wiederholungsversuche (keine Endlosschleife).
- Reinigung Wir vertrauen dem zurückgegebenen HTML-Code nicht. Wir verwenden
wp_kses_post(). - Ausgang : JSON für den Administrator und optional eine Frontend-Darstellung basierend auf einem Sprachparameter.
Der vollständige Code – Schritt für Schritt
Wir werden ein Mini-Plugin erstellen. Ich empfehle ein Mu-Plugin Wenn Sie nicht möchten, dass ein Kunde es „zu Testzwecken“ deaktiviert. Ansonsten ein Standard-Plugin.
Schritt 1 – Erstellen Sie ein MU-Plugin
schaffen wp-content/mu-plugins/bpcab-ai-translate.phpWenn die Datei mu-plugins nicht existiert, erstellen Sie es.
Realistischer Fehler: Viele Leute fügen die Datei ein in wp-content/plugins Dann vergessen sie, es zu aktivieren. Ein MU-Plugin wird automatisch geladen.
Schritt 2 – Einen REST-Endpunkt nur für Administratoren deklarieren
Wir stellen einen Endpunkt bereit, der nur für einen Benutzer mit der entsprechenden Fähigkeit funktioniert. edit_posts (ggf. anpassen) und wofür eine REST-Nonce erforderlich ist.
<?php
/**
* Plugin Name: BPCAB AI Translate (sans plugin de traduction)
* Description: Traduction IA à la demande via REST API + cache Transients.
* Author: Votre Nom
* Version: 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
add_action('rest_api_init', function () {
register_rest_route('bpcab/v1', '/translate', [
'methods' => 'POST',
'callback' => 'bpcab_translate_endpoint',
'permission_callback' => 'bpcab_translate_permission_check',
'args' => [
'post_id' => [
'type' => 'integer',
'required' => true,
'sanitize_callback' => 'absint',
],
'source' => [
'type' => 'string',
'required' => false,
'default' => 'fr',
'sanitize_callback' => 'sanitize_key',
],
'target' => [
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_key',
],
'glossary' => [
'type' => 'string',
'required' => false,
'default' => '',
'sanitize_callback' => 'sanitize_textarea_field',
],
'store_as_meta' => [
'type' => 'boolean',
'required' => false,
'default' => false,
],
],
]);
});
function bpcab_translate_permission_check(WP_REST_Request $request) : bool {
// Vérifie la capacité
if (!current_user_can('edit_posts')) {
return false;
}
// Vérifie le nonce REST (envoyé via X-WP-Nonce)
$nonce = $request->get_header('x_wp_nonce');
if (!$nonce || !wp_verify_nonce($nonce, 'wp_rest')) {
return false;
}
return true;
}
Schritt 3 – Übersetzungsfunktion erstellen (Cache + API)
Wir werden:
- Beitrag abrufen,
- einen stabilen Cache-Schlüssel berechnen,
- Rufen Sie gegebenenfalls die API auf.
- Desinfektionsmittel sind wieder da.
- Optional in den Beitragsmetadaten speichern.
function bpcab_translate_endpoint(WP_REST_Request $request) : WP_REST_Response {
$post_id = (int) $request->get_param('post_id');
$source = (string) $request->get_param('source');
$target = (string) $request->get_param('target');
$glossary = (string) $request->get_param('glossary');
$store_as_meta = (bool) $request->get_param('store_as_meta');
$post = get_post($post_id);
if (!$post || $post->post_status === 'trash') {
return new WP_REST_Response([
'error' => 'Post introuvable.',
], 404);
}
// On limite aux types publics classiques (ajustez si vous traduisez des CPT)
$allowed_types = ['post', 'page'];
if (!in_array($post->post_type, $allowed_types, true)) {
return new WP_REST_Response([
'error' => 'Type de contenu non supporté pour la traduction.',
], 400);
}
// Validation basique des langues (évite des clés de cache bizarres)
if (!preg_match('/^[a-z]{2}(-[A-Z]{2})?$/', $source)) {
$source = 'fr';
}
if (!preg_match('/^[a-z]{2}(-[A-Z]{2})?$/', $target)) {
return new WP_REST_Response([
'error' => 'Langue cible invalide (ex: en, en-US, es).',
], 400);
}
$original_title = (string) get_the_title($post);
$original_content = (string) $post->post_content;
// Si votre contenu contient des shortcodes lourds, c’est souvent mieux de traduire
// le contenu "brut" et de laisser les shortcodes intacts.
// Ici on envoie le HTML/shortcodes tels quels, et on demande explicitement de les préserver.
$payload = [
'title' => $original_title,
'content' => $original_content,
];
$translated = bpcab_translate_with_cache($post_id, $payload, $source, $target, $glossary);
if (is_wp_error($translated)) {
return new WP_REST_Response([
'error' => $translated->get_error_message(),
'details' => $translated->get_error_data(),
], 502);
}
if ($store_as_meta) {
// Stockage simple : un meta par langue
// Attention : si vous faites du SEO multilingue sérieux, vous voudrez un modèle plus propre.
update_post_meta($post_id, '_bpcab_ai_title_' . strtolower($target), $translated['title']);
update_post_meta($post_id, '_bpcab_ai_content_' . strtolower($target), $translated['content']);
}
return new WP_REST_Response([
'post_id' => $post_id,
'source' => $source,
'target' => $target,
'translated_title' => $translated['title'],
'translated_html' => $translated['content'],
'cached' => (bool) $translated['cached'],
], 200);
}
Schritt 4 — Transient Cache + OpenAI-Aufruf über wp_remote_post()
Der entscheidende Punkt: Der Cache-Schlüssel muss sich ändern, wenn sich der Inhalt ändert.Ich verwende einen Hash aus Inhalt, Titel, Glossar und Vorlage. Andernfalls wird tagelang eine veraltete Übersetzung ausgeliefert.
function bpcab_translate_with_cache(int $post_id, array $payload, string $source, string $target, string $glossary) {
if (!defined('BPCAB_OPENAI_API_KEY') || !BPCAB_OPENAI_API_KEY) {
return new WP_Error('bpcab_no_api_key', 'Clé API manquante. Définissez BPCAB_OPENAI_API_KEY dans wp-config.php.');
}
$model = defined('BPCAB_TRANSLATION_MODEL') ? (string) BPCAB_TRANSLATION_MODEL : 'gpt-4.1-mini';
// Hash stable du contenu à traduire
$hash_input = wp_json_encode([
'model' => $model,
'source' => $source,
'target' => $target,
'glossary' => $glossary,
'payload' => $payload,
]);
if (!$hash_input) {
return new WP_Error('bpcab_json_error', 'Impossible d’encoder le payload en JSON.');
}
$content_hash = hash('sha256', $hash_input);
$transient_key = 'bpcab_tr_' . $post_id . '_' . strtolower($target) . '_' . substr($content_hash, 0, 16);
$cached = get_transient($transient_key);
if (is_array($cached) && isset($cached['title'], $cached['content'])) {
$cached['cached'] = true;
return $cached;
}
$result = bpcab_call_openai_translation($payload, $source, $target, $glossary, $model);
if (is_wp_error($result)) {
return $result;
}
// Cache 30 jours (à ajuster)
set_transient($transient_key, [
'title' => $result['title'],
'content' => $result['content'],
], 30 * DAY_IN_SECONDS);
return [
'title' => $result['title'],
'content' => $result['content'],
'cached' => false,
];
}
function bpcab_call_openai_translation(array $payload, string $source, string $target, string $glossary, string $model) {
$endpoint = 'https://api.openai.com/v1/chat/completions';
// Prompt conçu pour préserver HTML + shortcodes.
// J’insiste sur "ne pas traduire les attributs, URLs, shortcodes".
$system = "Vous êtes un moteur de traduction professionnel. Conservez strictement la structure HTML et les shortcodes WordPress (ex:
, ). Ne traduisez pas les URLs, slugs, attributs HTML, classes CSS, ids, noms de fichiers. Ne modifiez pas les entités HTML. Retournez uniquement du JSON strict.";
$glossary_block = '';
if (!empty($glossary)) {
$glossary_block = "Glossaire (à respecter strictement) :n" . $glossary;
}
$user = "Traduisez du {$source} vers {$target}.n"
. $glossary_block . "nn"
. "Retour attendu (JSON strict) :n"
. "{n"
. " "title": "...",n"
. " "content": "..."n"
. "}nn"
. "Texte à traduire :n"
. "TITLE:n" . $payload['title'] . "nn"
. "CONTENT (HTML/shortcodes):n" . $payload['content'];
$body = [
'model' => $model,
// Température basse pour limiter les variations
'temperature' => 0.2,
'messages' => [
['role' => 'system', 'content' => $system],
['role' => 'user', 'content' => $user],
],
];
$args = [
'headers' => [
'Authorization' => 'Bearer ' . BPCAB_OPENAI_API_KEY,
'Content-Type' => 'application/json; charset=utf-8',
],
'body' => wp_json_encode($body),
'timeout' => 25, // timeout réseau (secondes)
'redirection' => 3,
];
$response = wp_remote_post($endpoint, $args);
if (is_wp_error($response)) {
return new WP_Error('bpcab_http_error', 'Erreur HTTP vers l’API : ' . $response->get_error_message(), [
'wp_error' => $response,
]);
}
$code = (int) wp_remote_retrieve_response_code($response);
$raw = (string) wp_remote_retrieve_body($response);
if ($code < 200 || $code >= 300) {
return new WP_Error('bpcab_api_status', 'Réponse API non OK (HTTP ' . $code . ').', [
'status' => $code,
'body' => $raw,
]);
}
$data = json_decode($raw, true);
if (!is_array($data)) {
return new WP_Error('bpcab_bad_json', 'JSON invalide retourné par l’API.', [
'body' => $raw,
]);
}
// Extraction "chat.completions"
$content = $data['choices'][0]['message']['content'] ?? '';
if (!is_string($content) || $content === '') {
return new WP_Error('bpcab_empty_content', 'Réponse vide de l’API.', [
'parsed' => $data,
]);
}
// Le modèle est censé renvoyer du JSON strict, mais je ne lui fais jamais confiance.
$translated = json_decode($content, true);
if (!is_array($translated) || !isset($translated['title'], $translated['content'])) {
return new WP_Error('bpcab_invalid_translation_format', 'Format de traduction invalide (JSON attendu).', [
'model_output' => $content,
]);
}
// Nettoyage : titre en texte, contenu en HTML autorisé WP
$title_clean = sanitize_text_field((string) $translated['title']);
$html_clean = wp_kses_post((string) $translated['content']);
return [
'title' => $title_clean,
'content' => $html_clean,
];
}
Schritt 5 – (Optional) Die Übersetzung wird direkt auf der Vorderseite angezeigt.
Wenn Sie keine "/en/…"-Seiten erstellen möchten, können Sie die Übersetzung über eine Einstellung dynamisch anzeigen lassen. ?lang=enEs eignet sich gut zum Testen, ist aber keine vollständige mehrsprachige SEO-Strategie.
Ich mache das oft während der Validierungsphase: Der Kunde klickt, vergleicht, wir passen das Glossar an, und erst dann entscheiden wir, ob wir es in den Metadaten speichern oder die Seiten duplizieren.
add_filter('the_content', function ($content) {
if (is_admin() || !is_singular()) {
return $content;
}
// Langue demandée via query var simple
$lang = isset($_GET['lang']) ? sanitize_key((string) $_GET['lang']) : '';
if (!$lang || $lang === 'fr') {
return $content;
}
global $post;
if (!$post instanceof WP_Post) {
return $content;
}
$source = 'fr';
$target = $lang;
$payload = [
'title' => (string) get_the_title($post),
'content' => (string) $post->post_content,
];
// Glossaire vide ici, mais vous pouvez le remplir via une option.
$translated = bpcab_translate_with_cache((int) $post->ID, $payload, $source, $target, '');
if (is_wp_error($translated)) {
// Fallback silencieux : on garde le contenu original
return $content;
}
return $translated['content'];
}, 20);
Ein häufiger Fehler ist, diesem Filter eine zu niedrige Priorität (z. B. 1) zuzuweisen, wodurch Elementor-/Divi-/Avada-Shortcodes, die später ausgeführt werden, nicht mehr funktionieren. Priorität 20 ist oft ein guter Kompromiss. Falls Ihr Builder Inhalte über eigene Hooks einbindet, müssen Sie diesen Wert gegebenenfalls anpassen.
Der vollständige kompilierte Code
Kopieren Sie diese Datei unverändert und fügen Sie sie ein in wp-content/mu-plugins/bpcab-ai-translate.phpDer API-Schlüssel bleibt erhalten wp-config.php.
<?php
/**
* Plugin Name: BPCAB AI Translate (sans plugin de traduction)
* Description: Traduction IA à la demande via REST API + cache Transients (WP 6.9.4+, PHP 8.1+).
* Version: 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
add_action('rest_api_init', function () {
register_rest_route('bpcab/v1', '/translate', [
'methods' => 'POST',
'callback' => 'bpcab_translate_endpoint',
'permission_callback' => 'bpcab_translate_permission_check',
'args' => [
'post_id' => [
'type' => 'integer',
'required' => true,
'sanitize_callback' => 'absint',
],
'source' => [
'type' => 'string',
'required' => false,
'default' => 'fr',
'sanitize_callback' => 'sanitize_key',
],
'target' => [
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_key',
],
'glossary' => [
'type' => 'string',
'required' => false,
'default' => '',
'sanitize_callback' => 'sanitize_textarea_field',
],
'store_as_meta' => [
'type' => 'boolean',
'required' => false,
'default' => false,
],
],
]);
});
function bpcab_translate_permission_check(WP_REST_Request $request) : bool {
if (!current_user_can('edit_posts')) {
return false;
}
$nonce = $request->get_header('x_wp_nonce');
if (!$nonce || !wp_verify_nonce($nonce, 'wp_rest')) {
return false;
}
return true;
}
function bpcab_translate_endpoint(WP_REST_Request $request) : WP_REST_Response {
$post_id = (int) $request->get_param('post_id');
$source = (string) $request->get_param('source');
$target = (string) $request->get_param('target');
$glossary = (string) $request->get_param('glossary');
$store_as_meta = (bool) $request->get_param('store_as_meta');
$post = get_post($post_id);
if (!$post || $post->post_status === 'trash') {
return new WP_REST_Response(['error' => 'Post introuvable.'], 404);
}
$allowed_types = ['post', 'page'];
if (!in_array($post->post_type, $allowed_types, true)) {
return new WP_REST_Response(['error' => 'Type de contenu non supporté pour la traduction.'], 400);
}
if (!preg_match('/^[a-z]{2}(-[A-Z]{2})?$/', $source)) {
$source = 'fr';
}
if (!preg_match('/^[a-z]{2}(-[A-Z]{2})?$/', $target)) {
return new WP_REST_Response(['error' => 'Langue cible invalide (ex: en, en-US, es).'], 400);
}
$payload = [
'title' => (string) get_the_title($post),
'content' => (string) $post->post_content,
];
$translated = bpcab_translate_with_cache($post_id, $payload, $source, $target, $glossary);
if (is_wp_error($translated)) {
return new WP_REST_Response([
'error' => $translated->get_error_message(),
'details' => $translated->get_error_data(),
], 502);
}
if ($store_as_meta) {
update_post_meta($post_id, '_bpcab_ai_title_' . strtolower($target), $translated['title']);
update_post_meta($post_id, '_bpcab_ai_content_' . strtolower($target), $translated['content']);
}
return new WP_REST_Response([
'post_id' => $post_id,
'source' => $source,
'target' => $target,
'translated_title' => $translated['title'],
'translated_html' => $translated['content'],
'cached' => (bool) $translated['cached'],
], 200);
}
function bpcab_translate_with_cache(int $post_id, array $payload, string $source, string $target, string $glossary) {
if (!defined('BPCAB_OPENAI_API_KEY') || !BPCAB_OPENAI_API_KEY) {
return new WP_Error('bpcab_no_api_key', 'Clé API manquante. Définissez BPCAB_OPENAI_API_KEY dans wp-config.php.');
}
$model = defined('BPCAB_TRANSLATION_MODEL') ? (string) BPCAB_TRANSLATION_MODEL : 'gpt-4.1-mini';
$hash_input = wp_json_encode([
'model' => $model,
'source' => $source,
'target' => $target,
'glossary' => $glossary,
'payload' => $payload,
]);
if (!$hash_input) {
return new WP_Error('bpcab_json_error', 'Impossible d’encoder le payload en JSON.');
}
$content_hash = hash('sha256', $hash_input);
$transient_key = 'bpcab_tr_' . $post_id . '_' . strtolower($target) . '_' . substr($content_hash, 0, 16);
$cached = get_transient($transient_key);
if (is_array($cached) && isset($cached['title'], $cached['content'])) {
$cached['cached'] = true;
return $cached;
}
$result = bpcab_call_openai_translation($payload, $source, $target, $glossary, $model);
if (is_wp_error($result)) {
return $result;
}
set_transient($transient_key, [
'title' => $result['title'],
'content' => $result['content'],
], 30 * DAY_IN_SECONDS);
return [
'title' => $result['title'],
'content' => $result['content'],
'cached' => false,
];
}
function bpcab_call_openai_translation(array $payload, string $source, string $target, string $glossary, string $model) {
$endpoint = 'https://api.openai.com/v1/chat/completions';
$system = "Vous êtes un moteur de traduction professionnel. Conservez strictement la structure HTML et les shortcodes WordPress (ex:
, ). Ne traduisez pas les URLs, slugs, attributs HTML, classes CSS, ids, noms de fichiers. Ne modifiez pas les entités HTML. Retournez uniquement du JSON strict.";
$glossary_block = '';
if (!empty($glossary)) {
$glossary_block = "Glossaire (à respecter strictement) :n" . $glossary;
}
$user = "Traduisez du {$source} vers {$target}.n"
. $glossary_block . "nn"
. "Retour attendu (JSON strict) :n"
. "{n"
. " "title": "...",n"
. " "content": "..."n"
. "}nn"
. "Texte à traduire :n"
. "TITLE:n" . $payload['title'] . "nn"
. "CONTENT (HTML/shortcodes):n" . $payload['content'];
$body = [
'model' => $model,
'temperature' => 0.2,
'messages' => [
['role' => 'system', 'content' => $system],
['role' => 'user', 'content' => $user],
],
];
$args = [
'headers' => [
'Authorization' => 'Bearer ' . BPCAB_OPENAI_API_KEY,
'Content-Type' => 'application/json; charset=utf-8',
],
'body' => wp_json_encode($body),
'timeout' => 25,
'redirection' => 3,
];
$response = wp_remote_post($endpoint, $args);
if (is_wp_error($response)) {
return new WP_Error('bpcab_http_error', 'Erreur HTTP vers l’API : ' . $response->get_error_message(), [
'wp_error' => $response,
]);
}
$code = (int) wp_remote_retrieve_response_code($response);
$raw = (string) wp_remote_retrieve_body($response);
if ($code < 200 || $code >= 300) {
return new WP_Error('bpcab_api_status', 'Réponse API non OK (HTTP ' . $code . ').', [
'status' => $code,
'body' => $raw,
]);
}
$data = json_decode($raw, true);
if (!is_array($data)) {
return new WP_Error('bpcab_bad_json', 'JSON invalide retourné par l’API.', [
'body' => $raw,
]);
}
$content = $data['choices'][0]['message']['content'] ?? '';
if (!is_string($content) || $content === '') {
return new WP_Error('bpcab_empty_content', 'Réponse vide de l’API.', [
'parsed' => $data,
]);
}
$translated = json_decode($content, true);
if (!is_array($translated) || !isset($translated['title'], $translated['content'])) {
return new WP_Error('bpcab_invalid_translation_format', 'Format de traduction invalide (JSON attendu).', [
'model_output' => $content,
]);
}
$title_clean = sanitize_text_field((string) $translated['title']);
$html_clean = wp_kses_post((string) $translated['content']);
return [
'title' => $title_clean,
'content' => $html_clean,
];
}
// Optionnel : affichage à la volée via ?lang=en (pratique pour valider)
add_filter('the_content', function ($content) {
if (is_admin() || !is_singular()) {
return $content;
}
$lang = isset($_GET['lang']) ? sanitize_key((string) $_GET['lang']) : '';
if (!$lang || $lang === 'fr') {
return $content;
}
global $post;
if (!$post instanceof WP_Post) {
return $content;
}
$payload = [
'title' => (string) get_the_title($post),
'content' => (string) $post->post_content,
];
$translated = bpcab_translate_with_cache((int) $post->ID, $payload, 'fr', $lang, '');
if (is_wp_error($translated)) {
return $content;
}
return $translated['content'];
}, 20);
Code-Erklärung
Warum ein REST-Endpunkt anstelle einer Schaltfläche im Admin-Panel?
Weil der REST-Endpunkt ein stabiler „Einstiegspunkt“ ist. Dann können Sie:
- Die Übersetzung wird über ein kleines JS-Skript im Admin-Panel aufgerufen.
- Stapelübersetzungen über WP-CLI starten (Variante unten),
- Einen redaktionellen Workflow verbinden.
Und vor allem: REST zwingt Sie dazu, Berechtigungen und Nonce ordnungsgemäß zu verwalten.
Warum den Transienten-Cache verwenden und nicht eine Option oder eine Datei?
Der Einschwingvorgang ist deshalb praktisch, weil:
- Es hat ein vordefiniertes Ablaufdatum.
- Es ist kompatibel mit Objektcaches (Redis/Memcached), falls Sie einen solchen verwenden.
- Dadurch wird vermieden, dass der Tisch verschmutzt wird.
postmetafür Schnelltests.
Beim Übergang zu einer professionellen, mehrsprachigen Produktionsumgebung werden Sie Daten wahrscheinlich in Metadaten speichern (oder übersetzte Beiträge erstellen). Hierbei dient der temporäre Speicher als Kostenpuffer.
Warum wp_kses_post() Bezüglich der KI-Antwort?
Weil Sie keine hundertprozentige Kontrolle darüber haben, was das Modell zurückgibt. Selbst wenn Sie es anweisen, „striktes JSON“ zu verwenden, kann ein Modell immer noch Fehlfunktionen aufweisen, Tags einfügen oder Ihren HTML-Code durch Modifikation „korrigieren“.
wp_kses_post() Wendet die Whitelist der im WordPress-Kontext zulässigen Tags an. Offizielle Dokumentation: wp_kses_post().
Warum ist im Cache-Schlüssel ein Hash der Nutzdaten enthalten?
Ohne Hashing wird „Beitrag 123 in EN“ zwischengespeichert und dieselbe Übersetzung angezeigt, selbst wenn der Beitrag bearbeitet wird. Hashing sorgt dafür, dass der Cache auf Änderungen reagiert, ohne dass eine komplizierte Löschlogik erforderlich ist.
API-Kosten und Optimierung
Die Kosten hängen vom Anbieter, dem Modell und insbesondere vom Textumfang ab. Ich gebe Ihnen eine realistische Berechnungsmethode, aber kein Versprechen.
Praktische Schätzung
- Ein „durchschnittlicher“ Blogbeitrag (800–1200 Wörter) besteht nach der Serialisierung (HTML + Shortcodes + Eingabeaufforderung) oft aus einigen tausend Tokens.
- Wenn Sie 100 Artikel pro Monat übersetzen, ohne Zwischenspeicherung, zahlen Sie für mindestens 100 Aufrufe pro Monat.
Ich verwende bewusst die allgemeine Formulierung: Kosten = Eintrittstoken × Eintrittspreis + Austrittstoken × AustrittspreisDie Preise ändern sich häufig; überprüfen Sie die Preisseite Ihres Lieferanten.
Optimierungen mit unmittelbarer Wirkung:
- Langer Cache (30 Tage oder mehr) und Schlüssel basierend auf dem Inhalts-Hash.
- „Mini“-Modell zur Übersetzung (oft ausreichend).
- Reduziere die Eingabeaufforderung : deine
systemkönnte nach Stabilisierung kürzer sein. - Übersetzen Sie nur die endgültige Fassung. : Vermeiden Sie es, denselben Block 10 Mal zu senden (Vorlagengenerator).
Kostenfalle, die ich oft sehe
Die Leute testen die Produktionsumgebung, indem sie eine Seite 30 Mal aktualisieren. ?lang=enund wundern sich, warum die Rechnung steigt. Ohne Cache löst jede Aktualisierung einen Aufruf aus. Mit Transienten- und Hash-Speicher stabilisiert sich die Situation sofort.
Erweiterte Varianten und Anwendungsfälle
Variante 1 – Übersetzen und in Metadaten speichern, dann über Filter anzeigen
Wenn Sie API-Aufrufe im Frontend vermeiden möchten, speichern Sie die Daten in Meta-Tags (store_as_meta=truedann zeige die Metadaten an, wenn ?lang=xx vorhanden ist.
add_filter('the_content', function ($content) {
if (is_admin() || !is_singular()) {
return $content;
}
$lang = isset($_GET['lang']) ? sanitize_key((string) $_GET['lang']) : '';
if (!$lang || $lang === 'fr') {
return $content;
}
global $post;
if (!$post instanceof WP_Post) {
return $content;
}
$stored = get_post_meta((int) $post->ID, '_bpcab_ai_content_' . strtolower($lang), true);
if (is_string($stored) && $stored !== '') {
return wp_kses_post($stored);
}
return $content;
}, 20);
Variante 2 – Stapelverarbeitung über WP-CLI (Idee, kein vollständiger Code)
Um 200 Beiträge gleichzeitig zu übersetzen, ist WP-CLI oft zuverlässiger als HTTP-Anfragen vom Admin-Panel. Sie können einen Befehl erstellen, der die IDs durchläuft und die entsprechenden Aufrufe tätigt. bpcab_translate_with_cache() und speichert in Metadaten.
Um den Artikel übersichtlich zu halten, verzichte ich hier auf den vollständigen WP-CLI-Code, aber die offizielle Dokumentation ist eindeutig: WP-CLI-Befehls-Kochbuch.
Variante 3 – Kompatibilität mit Divi 5 / Elementor / Avada
- Divi 5 Ein Großteil des Inhalts ist in Shortcodes/Strukturen gespeichert. Die Meldung „Shortcodes nicht verändern“ ist unbedingt erforderlich. Testen Sie dies auf einer komplexen Seite, da sonst Module nicht mehr funktionieren.
- Elementor Ein Teil des Inhalts befindet sich in
_elementor_data(JSON). Übersetzen Sie dieses JSON nicht mit diesem Code. Übersetzen Sie stattdessen den „gerenderten“ Inhalt (oder erstellen Sie einen Übersetzer speziell für das Elementor-Schema, was ein separates Projekt ist). - Avada (Fusion Builder) Gleiches Prinzip wie bei Divi, viele Fusion-Shortcodes. Verwenden Sie diese unbedingt selbst, sonst geht das Layout verloren.
Wenn Ihre Website hauptsächlich auf einem Website-Baukasten basiert, ist die sicherste Strategie: Übersetzen Sie nur die Textfelder (Widgets/Module) und nicht die Struktur. Hier geht es um eine spezifische Entwicklung pro Builder.
Sicherheit und bewährte Verfahren
Geben Sie den API-Schlüssel niemals clientseitig preis.
Kein JavaScript, das OpenAI/Anthropic direkt aufruft. Ihr Schlüssel würde im Browser landen. Alle Anfragen müssen über Ihren WordPress-Server laufen.
Mindestrate
Ein REST-Endpunkt kann leicht gehackt werden (selbst von einem ungeschickten Administrator). Fügen Sie eine einfache Sperre pro Benutzer hinzu.
function bpcab_rate_limit_or_fail(int $user_id, int $limit, int $window_seconds) {
$key = 'bpcab_rl_' . $user_id;
$data = get_transient($key);
if (!is_array($data)) {
$data = ['count' => 0, 'start' => time()];
}
$elapsed = time() - (int) $data['start'];
if ($elapsed > $window_seconds) {
$data = ['count' => 0, 'start' => time()];
}
$data['count']++;
set_transient($key, $data, $window_seconds);
if ($data['count'] > $limit) {
return new WP_Error('bpcab_rate_limited', 'Rate limit atteint. Réessayez plus tard.', [
'limit' => $limit,
'window' => $window_seconds,
]);
}
return true;
}
Sie können diese Funktion am Anfang aufrufen bpcab_translate_endpoint() mit get_current_user_id()Es ist nicht perfekt, aber es vermeidet die Situation, in der man 50 Mal klicken muss.
Eingabevalidierung
- Strengstens desinfizieren:
absint,sanitize_key,sanitize_textarea_field. - Whitelist der Beitragsarten.
- Regulärer Ausdruck für Sprachcodes, um exotische Cache-Schlüssel zu vermeiden.
DSGVO / An die API gesendete Daten
Wenn Sie übersetzen:
- Nutzerdaten (Kommentare, Formulare),
- sensible Daten (E-Mails, Adressen),
Sie übermitteln diese Daten an Dritte. Führen Sie eine Prüfung durch: Rechtsgrundlage, Einwilligung zur Datenverarbeitung (DSB), Aufbewahrungsfrist, Anonymisierung. Der obige Code anonymisiert keine Daten.
Wie man testet und Fehler behebt
1) Protokollierung aktivieren
Tanz wp-config.php (in einer Testumgebung):
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
Offizielles Dokument: Debuggen in WordPress.
2) Testen Sie den REST-Endpunkt mit curl.
Rufen Sie eine REST-Nonce aus Ihrer Admin-Sitzung ab (zum Beispiel über wpApiSettings.nonce (falls Sie eine Admin-Seite haben, die dies ermöglicht) oder verwenden Sie ein REST-Tool im Admin-Panel. Beispiel: curl (schematisch):
curl -X POST "https://votre-site.tld/wp-json/bpcab/v1/translate"
-H "Content-Type: application/json"
-H "X-WP-Nonce: VOTRE_NONCE"
-d '{"post_id":123,"source":"fr","target":"en","glossary":"WordPress=WordPressnExtension=plugin","store_as_meta":false}'
3) Überprüfen Sie den Cache.
Einfacher Test: Führen Sie dieselbe Abfrage zweimal aus. Beim zweiten Mal sollte folgendes Ergebnis zurückgegeben werden: "cached": trueFalls das nicht der Fall ist, haben Sie Folgendes:
- sich ändernder Inhalt (Builder, der Zeitstempel einfügt),
- ein anderes Glossar,
- ein anderes Modell
- oder ein Objektcache, der aggressiv gelöscht wird.
4) Überprüfen Sie die Modellausgabe.
Wenn die Fehlermeldung „Ungültiges Übersetzungsformat“ angezeigt wird, melden Sie sich an. model_output (in einer Testumgebung) um zu verstehen, was das Modell tatsächlich zurückgibt.
Wenn es nicht funktioniert
Hier sind die Fehlerursachen, die mir am häufigsten begegnen, zusammen mit einer schnellen Methode zur Fehlerbehebung.
| Symptom | Mögliche Ursache | Überprüfung | Lösung |
|---|---|---|---|
| HTTP 401 / „Nicht autorisiert“ | Ungültiger oder fehlender API-Schlüssel | überprüfen BPCAB_OPENAI_API_KEY + Rohantwort in details.body |
Korrigieren Sie den Schlüssel und prüfen Sie die Projektrechte auf Seiten des Lieferanten. |
| HTTP 429 | Quote überschritten / Lieferantenpreislimit | aussehen details.status und der API-Körper |
Warten Sie, verringern Sie die Lautstärke, aktivieren Sie den Cache, verwenden Sie ein leichteres Modell |
| Timeout | Timeout zu niedrig oder Server zu langsam | PHP-Protokolle + vorübergehende Erhöhung timeout |
Im Batch-Modus auf 40 Sekunden erhöhen oder Off-Front übersetzen (WP-CLI). |
| Fehlerhaftes HTML (Layout-Builder) | Das Modell hat geänderte Shortcodes/Attribute. | Vergleiche Original und Übersetzung | Verbesserung der Eingabeaufforderung, Übersetzung nur von Textbereichen, Speicherung nach Modul |
| Übersetzung niemals „versteckt“ | Instabiler Cache-Schlüssel (unterschiedlicher Inhalt bei jedem Aufruf) | Hash protokollieren (auf der Staging-Umgebung) | Bereinigen Sie den Inhalt vor dem Hashing (entfernen Sie dynamische Blöcke) oder speichern Sie ihn in den Metadaten. |
| Fehler 403 am Endpunkt | Fehlende/ungültige REST-Nonce oder unzureichende Kapazität | Prüfen Sie den Header. X-WP-Nonce + Benutzerrolle |
Nonce korrekt generieren, anpassen permission_callback |
"Dumme", aber häufige Fehler
- Der Code wurde an der falschen Stelle eingefügt. : in einem Plugin-Snippet, das PHP minimiert/bearbeitet und dadurch die Kodierung beschädigt. Verwenden Sie stattdessen eine mu-Plugin-Datei.
- Fehlendes Semikolon Es tritt ein 500-Fehler auf. Schau mal nach.
wp-content/debug.log. - Unangemessener Haken : wenn Sie versuchen, die REST-API aufzurufen, bevor
rest_api_initEs wird nichts deklariert. - Produktionstests Sie lösen Dutzende bezahlter Anrufe aus. Planen Sie Ihre Maßnahmen und migrieren Sie dann.
- PHP ist zu alt : Eingabe + Rückgabe
: WP_REST_ResponseSie könnten unter PHP 7.x Probleme verursachen. Hier zielen wir auf PHP 8.1+ ab.
Ressourcen
- wp_remote_post() — WordPress-Entwicklerressourcen
- REST-API-Handbuch — WordPress
- set_transient() — WordPress-Entwicklerressourcen
- wp_kses_post() — WordPress-Entwicklerressourcen
- OpenAI API-Referenz
- WordPress-Debugging – Entwicklerressourcen
- WordPress (Mirror) auf GitHub – um den Quellcode zu durchsuchen
- WordPress Core Trac – Verlauf und Tickets
- json_decode() — PHP.net
FAQ
Ist es wirklich „pluginfrei“?
Ohne ein Übersetzungs-Plugin eines Drittanbieters, ja. Technisch gesehen fügen Sie dennoch Code in Form eines MU-Plugins (oder eines benutzerdefinierten Plugins) hinzu. Dies ist beabsichtigt: Sie behalten die Kontrolle und vermeiden ein komplexes System.
Werden dadurch übersetzte Seiten mit /en/-URLs erstellt?
Nein, nicht mit diesem Code. Die Übersetzung wird dynamisch angezeigt über ?lang=en Oder Sie speichern es in Meta-Tags. Für eine echte URL-Struktur pro Sprache benötigen Sie eine Routing-Schicht + hreflang + Sitemaps (oder Sie verwenden ein mehrsprachiges Plugin).
Warum nicht direkt übersetzen? post_content und den Beitrag speichern?
Weil Sie riskieren, Ihren Quellcode zu überschreiben. Trennen Sie daher immer Quellcode und Übersetzung (Metadaten, CPT-„Übersetzung“ oder Duplikate). Ich habe schon erlebt, dass Webseiten nach einer fehlerhaften Übersetzungsschleife ihren ursprünglichen Inhalt verloren haben.
Das Modell liefert manchmal Text zurück, der nicht dem JSON-Format entspricht. Was soll ich tun?
Das ist üblich. Der Code schlägt in diesem Fall absichtlich fehl. Auf einer realen Website könnte man einen Reparaturschritt (erneute Eingabeaufforderung) hinzufügen, aber das verdoppelt die Kosten. Ich bevorzuge es, einen sauberen Fehler auszulösen und den Code anschließend zu untersuchen. model_output auf der Bühne.
Wie übersetze ich den Auszug?
hinzufügen excerpt Fordern Sie im Payload ein JSON an mit excerptSpeichern Sie es dann als Metadaten. Beachten Sie dabei dasselbe Prinzip: Bereinigen Sie den Text, nicht das HTML.
Wie verwaltet man ein übersichtliches Glossar?
Speichern Sie es in einer Option (z.B. get_option('bpcab_translation_glossary')) und übergeben Sie es an die Funktion. Ein Glossar von 20–50 Zeilen verbessert die Konsistenz erheblich, insbesondere bei Markenbegriffen.
Funktioniert es mit Elementor, wenn meine Seiten zu 100 % mit Elementor erstellt wurden?
Das kommt darauf an. Wenn Ihr „eigentlicher“ Inhalt in _elementor_data, übersetzen post_content Das reicht nicht aus. Für Elementor müssen Sie entweder die endgültige Ausgabe übersetzen (riskant) oder einen Übersetzer schreiben, der das Elementor-JSON durchsucht und nur die Textfelder übersetzt.
Warum verwenden? chat/completions Und kein dedizierter "Übersetzungs"-Endpunkt?
Da der „Chat“-Ansatz die Einschränkung des Formats (JSON) und die Festlegung strenger Regeln (Beibehaltung von HTML/Shortcodes) ermöglicht, ist ein reiner „Übersetzungs“-Endpunkt zwar mitunter einfacher, bietet aber weniger Kontrolle über das Ausgabeformat.
Wie kann man die Übersetzung unerwünschter Teile (Code, Code-Snippets) vermeiden?
Füge der Eingabeaufforderung folgende Regel hinzu: „Übersetze den Inhalt innerhalb der Tags nicht.“ <code> et <pre>Bei großen Codemengen können Sie den HTML-Code auch vorverarbeiten und diese Blöcke vor dem Senden durch Platzhalter ersetzen und sie anschließend wieder einfügen.
Der temporäre Cache wird auf meinem Hosting nicht dauerhaft gespeichert. Warum?
Manche Hosting-Anbieter leeren Caches sehr schnell, oder Ihre Website verwendet möglicherweise einen Objektcache mit eigenen Regeln. In diesem Fall sollten Sie den Cache in Meta-Tags speichern (dauerhafter) oder einen korrekt konfigurierten persistenten Objektcache (z. B. Redis) hinzufügen.
Kann ich OpenAI durch Mistral/Anthropic/Google ersetzen?
Ja: Die Architektur (Cache + Bereinigung + Fehlerbehandlung) bleibt gleich, nur der Cache wird ersetzt. bpcab_call_openai_translation() durch eine Funktion, die ihren Endpunkt aufruft mit wp_remote_post()Ändern Sie den Rest nicht, solange Ihr Ausgabeformat „strenges JSON“ bleibt.
