Ak ste niekedy manuálne vložili odsek do Prekladača Google o 23:00, aby ste rýchlo publikovali anglickú verziu, viete, kde je problém: je to pomalé, nekonzistentné a v editore sa to nakoniec kopíruje a vkladá. WordPress bez akejkoľvek sledovateľnosti.
WordPress Verzia 6.9.4 (apríl 2026) už poskytuje dobré nástroje (REST API, transienty, hooky), ale natívne nič „neprekladá“. Myšlienka je tu jednoduchá: pripojiť API prekladu s umelou inteligenciou cez wp_remote_post()bez inštalácie akýchkoľvek pluginov a s kontrolou nad ukladaním do vyrovnávacej pamäte, nákladmi a zabezpečením.
Potreba / Prípad použitia
Konkrétny problém: máte obsah (tovar(stránky, niekedy polia ACF) a chcete rýchlo vygenerovať preloženú verziu s prijateľnou kvalitou bez pridania náročného (a často drahého) prekladového pluginu, ktorý upravuje vašu databázu a váš back office.
Túto potrebu som často videl na:
- Francúzske technické blogy, ktoré chcú anglickú verziu „postačujúcu“ pre long-tail SEO,
- prezentačné webové stránky (Avada/Divi/Elementor), kde musí existovať 10 stránok v 2 jazykoch,
- stránky s veľmi stabilným obsahom (dokumentácia, právne stránky), kde sa preklad takmer nikdy nemení.
Nakoniec budete vedieť, ako implementovať:
- zabezpečený REST endpoint na vyžiadanie prekladu na strane administrátora,
- AI prekladový nástroj (napr. OpenAI) prostredníctvom
wp_remote_post(), - Jedna vyrovnávacia pamäť na príspevok + jazyk s prechodnými javmi,
- vykresľovanie „za chodu“ (bez duplikovania príspevkov) pomocou filtra na
the_content(voliteľné), - čistá záložná stratégia, ak je API pomalé alebo nefunkčné chyba.
Rýchle zhrnutie
- Kľúč API uložíte do
wp-config.php(nikdy v tvrdej forme). - Vytvoríte mini-plugin (alebo mu-plugin), ktorý sprístupní koncový bod REST iba pre správcu.
- Plugin volá AI API pomocou
wp_remote_post()+ časový limit + spracovanie chýb. - Preklad je uložený do vyrovnávacej pamäte prostredníctvom
set_transient()(za príspevok/jazyk + hash obsahu). - Možnosť 1: Zobrazenie „za behu“ (bez duplikácie). Možnosť 2: Generovanie a ukladanie v metadátach.
- Pridáte obmedzenie rýchlosti + vyčistenie HTML (
wp_kses_post()) aby ste sa vyhli nepríjemným prekvapeniam.
Kedy na to použiť umelú inteligenciu
Tento prístup použite, keď:
- Chcete „dobrý“ preklad bez vybudovania kompletnej viacjazyčnej infraštruktúry,
- Obsah je prevažne textový (články, stránky),
- Nedokonalý, ale súvislý preklad akceptujete, ak poskytnete glosár.
- chcete mať pod kontrolou náklady (agresívne ukladanie do vyrovnávacej pamäte, preklad na požiadanie),
- Nechcete sa spoliehať na plugin, ktorý si vnucuje vlastný dátový model.
Z mojej skúsenosti to funguje veľmi dobre pre redakčné weby, kde 80 % stránok tvoria články a kde sa preklad používa hlavne na akvizíciu (SEO + medzinárodní čitatelia), nie na striktnú právnu lokalizáciu.
Kedy NEPOUŽÍVAŤ AI
Vyhnite sa umelej inteligencii (alebo obmedzte jej používanie, keď:
- Máte právne požiadavky (zmluvné podmienky, zdravotné, finančné): nekorigovaný preklad umelou inteligenciou predstavuje riziko.
- Už máte zavedený WPML/Polylang a skutočnú viacjazyčnú stratégiu (URL adresy podľa jazyka, hreflang, menu atď.),
- Musíte preložiť reťazce rozhrania (reťazce témy): najlepší je na to špecializovaný nástroj (gettext).
- Potrebujete preložiť vysoko dynamický obsah (komentáre, UGC): cena + GDPR + moderovanie.
- Máte veľmi rozsiahlu stránku (viac ako 10 000 príspevkov) a myslíte si, že dokážete „preložiť všetko naraz“: účet a kvóty vás upokoja.
Jednoduchšia, „klasická“ alternatíva: ak potrebujete iba zobraziť rôzny obsah v závislosti od jazyka, manuálne duplikovanie 10 stránok plus menu pre každý jazyk je niekedy stále najlepšou návratnosťou investícií. Umelá inteligencia je užitočná najmä vtedy, keď chcete automatizovať bez toho, aby ste museli prepracovať celú stránku.
predpoklady
verzia
- WordPress 6.9.4+ (apríl 2026)
- PHP 8.1+ (odporúča sa 8.2/8.3, ak ho váš poskytovateľ hostingu podporuje)
- HTTPS musí byť povolený (inak je volanie API zlý nápad)
Kľúč API (príklad OpenAI)
Budete používať API cez HTTP. Žiadne SDK, žiadny Composer, spočiatku. Oficiálna dokumentácia:
- wp_remote_post() (Zdroje pre vývojárov WordPressu)
- Príručka REST API (WordPress)
- Referencia OpenAI API
- Rozšírenie JSON (php.net)
- Jednoduché čísla (zabezpečenie WordPressu)
Kľúč je uložený v súbore wp-config.php
Pridajte toto k wp-config.phpIdeálne prostredníctvom premennej prostredia vloženej poskytovateľom hostingu (ešte lepšie), inak pevne naprogramovanej v wp-config.php (prijateľné, ak je súbor dobre chránený).
/** 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');
Klasická pasca: vloženie tejto konštanty do functions.phpStále to vidím na stránkach Divi/Avada: pri prvom prepnutí témy kláves „zmizne“. Vložte ho tam. wp-config.php alebo ako premenná prostredia.
Architektúra riešenia
Priebeh (textová schéma):
Správca WordPressu → REST API (zabezpečený koncový bod) →
wp_remote_post()→ AI API (preklad) → validácia + čistenie → vyrovnávacia pamäť (prechodná + meta možnosť) → návrat JSON → zobrazenie (voliteľné cez filter)
Čo sa deje v zákulisí
- vstup :
post_id, zdrojový jazyk, cieľový jazyk a prípadne „glosár“. - Ťažba : načítame obsah (a názov, ak chcete) a potom ho pripravíme (so zachovaným HTML kódom).
- Cache Kľúč vyrovnávacej pamäte vypočítavame na základe hashu obsahu a cieľového jazyka. Ak sa to nezmenilo, za API neplatíme.
- Volanie API JSON požiadavka, primeraný časový limit, minimálne opakované pokusy (žiadna nekonečná slučka).
- čistenie Nedôverujeme vrátenému HTML kódu. Používame
wp_kses_post(). - výjazd JSON pre administrátora a voliteľne vykresľovanie na front-ende na základe jazykového parametra.
Kompletný kód – krok za krokom
Vytvoríme mini plugin. Odporúčam mu-plugin Ak nechcete, aby ho klient deaktivoval „na testovacie účely“. V opačnom prípade štandardný plugin.
Krok 1 – Vytvorenie mu-pluginu
vytvoriť wp-content/mu-plugins/bpcab-ai-translate.phpAk súbor mu-plugins Ak neexistuje, vytvorte ho.
Realistická chyba: veľa ľudí vloží súbor do wp-content/plugins Potom ho zabudnú aktivovať. Automaticky sa načíta mu-plugin.
Krok 2 – Deklarujte koncový bod REST iba pre správcu
Zverejňujeme koncový bod, ktorý funguje iba pre používateľa s touto schopnosťou edit_posts (upravte podľa potreby) a ktorý vyžaduje REST nonce.
<?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;
}
Krok 3 – Vytvorenie prekladovej funkcie (cache + API)
Ideme na to:
- vyzdvihnúť príspevok,
- vypočítať stabilný kľúč vyrovnávacej pamäte,
- v prípade potreby zavolajte API.
- dezinfekčný prostriedok je späť.
- voliteľne uložiť do meta údajov príspevku.
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);
}
Krok 4 – Prechodná vyrovnávacia pamäť + volanie OpenAI cez wp_remote_post()
Kľúčový bod: Kľúč vyrovnávacej pamäte sa musí zmeniť, ak sa zmení obsah.Používam hash obsahu + názvu + glosára + šablóny. Inak budete niekoľko dní zobrazovať zastaraný preklad.
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,
];
}
Krok 5 – (Voliteľné) Zobrazte preklad priamo na prednej strane
Ak nechcete vytvárať stránky typu „/en/…“, môžete zobraziť preklad za chodu pomocou nastavenia ?lang=enJe to pohodlné na testovanie, ale nie je to kompletná viacjazyčná SEO stratégia.
Často to robím počas fázy overovania: klient klikne, porovná, my upravíme glosár a až potom sa rozhodneme, či uložíme do meta alebo duplikujeme stránky.
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);
Častým problémom je nastavenie príliš nízkej priority tohto filtra (napr. 1) a porušenie shortcodov Elementor/Divi/Avada, ktoré sa spúšťajú neskôr. Priorita 20 je často dobrým kompromisom. Ak váš builder vkladá obsah prostredníctvom vlastných hookov, možno budete musieť túto prioritu upraviť.
Kompletný zostavený kód
Skopírujte a vložte tento súbor tak, ako je, do wp-content/mu-plugins/bpcab-ai-translate.phpKľúč API zostáva v 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);
Vysvetlenie kódu
Prečo REST endpoint a nie tlačidlo v administrátorskom paneli?
Pretože koncový bod REST je stabilný „vstupný bod“. Môžete potom:
- zavolajte preklad z malého JS skriptu v administrátorskom paneli.
- spustenie dávkových prekladov cez WP-CLI (variant nižšie),
- prepojiť redakčný pracovný postup.
A čo je najdôležitejšie: REST vás núti správne spravovať oprávnenia + nonce.
Prečo používať vyrovnávaciu pamäť Transients a nie nejakú možnosť alebo súbor?
Prechod je vhodný, pretože:
- Má pôvodný dátum spotreby.
- Je kompatibilný s objektovou vyrovnávacou pamäťou (Redis/Memcached), ak ju máte.
- zabraňuje znečisteniu stola
postmetapre rýchle testy.
Keď prejdete do „seriózneho“ viacjazyčného produkčného prostredia, pravdepodobne budete ukladať dáta do metaznačiek (alebo vytvárať preložené príspevky). V tomto prípade prechodné prostredie slúži ako vaša rezerva nákladov.
Pourquoi wp_kses_post() Čo sa týka reakcie umelej inteligencie?
Pretože nemáte 100% kontrolu nad tým, čo model vráti. Aj keď ho požiadate o použitie „striktného JSON“, model môže stále nefungovať správne, vkladať značky alebo „opravovať“ váš HTML kód jeho úpravou.
wp_kses_post() Aplikuje bielu listinu povolených značiek v kontexte WordPressu. Oficiálna dokumentácia: wp_kses_post().
Prečo je v kľúči vyrovnávacej pamäte hash užitočného zaťaženia?
Bez hašovania sa do vyrovnávacej pamäte uloží „príspevok 123 v angličtine“ a zobrazí sa rovnaký preklad, aj keď príspevok upravíte. Hašovanie umožňuje vyrovnávacej pamäti reagovať na zmeny bez nutnosti zložitej logiky čistenia.
Náklady na API a optimalizácia
Cena závisí od dodávateľa, modelu a najmä od objemu textu. Ponúkam vám realistickú metódu výpočtu, nie sľub.
Praktický odhad
- „Priemerný“ blogový príspevok (800 – 1 200 slov) často predstavuje niekoľko tisíc tokenov po serializácii (HTML + shortcodes + prompt).
- Ak preložíte 100 článkov mesačne bez ukladania do vyrovnávacej pamäte, platíte za minimálne 100 hovorov mesačne.
Zámerne ponechávam všeobecné znenie: cena = vstupné_žetóny × vstupná_cena + výstupné_žetóny × výstupná_cenaCeny sa často menia, preto si pozrite cenník na stránke vášho dodávateľa.
Optimalizácie, ktoré majú okamžitý vplyv:
- Dlhá vyrovnávacia pamäť (30 dní alebo viac) a kľúč založený na haši obsahu.
- Model „Mini“ na preklad (často postačujúce).
- Znížiť výzvu tvoj
systemmôže byť kratšia po stabilizácii. - Preložiť iba finálne vykreslenie : vyhnúť sa 10-násobnému odosielaniu toho istého bloku (nástroj na tvorbu šablón).
Nákladová pasca, ktorú často vidím
Ľudia testujú v produkcii a 30-krát obnovia stránku. ?lang=ena čudujete sa, prečo sa účet zvyšuje. Bez vyrovnávacej pamäte každé obnovenie spustí volanie. S transient + hash sa okamžite stabilizujete.
Pokročilé varianty a prípady použitia
Variant 1 – Preložiť a uložiť do metadát, potom zobraziť pomocou filtra
Ak sa chcete vyhnúť volaniam API na strane front-endu, uložte údaje do metaznačiek (store_as_meta=truepotom zobrazte meta, keď ?lang=xx je prítomný.
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);
Variant 2 — Dávkové spracovanie cez WP-CLI (nápad, nie kompletný kód)
Na preklad 200 príspevkov naraz je WP-CLI často spoľahlivejšie ako HTTP požiadavky z administrátorského panela. Môžete vytvoriť príkaz, ktorý prechádza ID, volania bpcab_translate_with_cache() a ukladá sa do metadát.
Neuvádzam tu celý kód WP-CLI, aby bol článok prehľadnejší, ale oficiálna dokumentácia je jasná: Kuchárska kniha príkazov WP-CLI.
Variant 3 – kompatibilita s Divi 5 / Elementor / Avada
- Divi 5 Veľká časť obsahu je uložená v krátkych kódoch/štruktúrach. Výzva „neupravovať krátke kódy“ je kľúčová. Testujte na zložitej stránke, inak budete mať nefunkčné moduly.
- Elementor : časť obsahu je v
_elementor_data(JSON). Neprekladajte tento JSON pomocou tohto kódu v pôvodnej podobe. Namiesto toho preložte „vykreslený“ obsah (alebo vytvorte prekladač špeciálne pre schému Elementoru, čo je samostatný projekt). - Avada (výrobca fúzií) Rovnaká logika ako v Divi, veľa krátkych kódov Fusion. Nechajte si ich prísne pre seba, inak stratíte rozloženie.
Ak je vaša stránka založená predovšetkým na tvorcovi stránok, najbezpečnejšou stratégiou je: preložiť iba textové polia (widgety/moduly) a nie štruktúra. Tu sa zaoberáte špecifickým vývojom pre každého staviteľa.
Bezpečnosť a osvedčené postupy
Nikdy nezverejňujte kľúč API na strane klienta
Žiadny JavaScript, ktorý by priamo volal OpenAI/Anthropic. Váš kľúč by skončil v prehliadači. Všetky požiadavky musia prechádzať cez váš WordPress server.
Minimálne obmedzenie rýchlosti
Koncový bod REST sa dá ľahko hacknúť (aj nešikovným správcom). Pridajte jednoduchý zámok pre každého používateľa.
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;
}
Túto funkciu môžete zavolať na začiatku bpcab_translate_endpoint() s get_current_user_id()Nie je to dokonalé, ale vyhýba sa situácii „kliknem 50-krát“.
Overenie vstupu
- Prísne dezinfikujte:
absint,sanitize_key,sanitize_textarea_field. - Biely zoznam typov príspevkov.
- Regex v jazykových kódoch, aby sa predišlo exotickým kľúčom vyrovnávacej pamäte.
GDPR / Údaje odoslané do API
Ak prekladáte:
- používateľské údaje (komentáre, formuláre),
- citlivé údaje (e-maily, adresy),
Tieto údaje odosielate tretej strane. Vykonajte audit: právny základ, autorizácia spracovania údajov (DPA), doba uchovávania, anonymizácia. Vyššie uvedený kód nič neanonymizuje.
Ako testovať a ladiť
1) Povoliť protokolovanie
Dans wp-config.php (v testovacom prostredí):
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
Oficiálny dokument: Ladenie vo WordPress.
2) Otestujte koncový bod REST pomocou funkcie Curl
Získajte REST nonce z vašej administrátorskej relácie (napríklad prostredníctvom wpApiSettings.nonce (ak máte administrátorskú stránku, ktorá ju sprístupňuje) alebo použite nástroj REST v administrátorskom paneli. Príklad curl (schéma):
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) Skontrolujte vyrovnávaciu pamäť
Jednoduchý test: spustite ten istý dotaz dvakrát. Druhýkrát by mal vrátiť výsledok. "cached": trueAk to tak nie je, máte:
- obsah, ktorý sa mení (nástroj na tvorbu, ktorý vkladá časové pečiatky),
- iný glosár,
- iný model,
- alebo vyrovnávaciu pamäť objektov, ktorá agresívne čistí.
4) Skontrolujte výstup modelu
Ak sa zobrazí chyba „Neplatný formát prekladu“, prihláste sa model_output (v testovacom prostredí), aby sme pochopili, čo model v skutočnosti vracia.
Ak to nefunguje
Tu sú poruchy, s ktorými sa stretávam najčastejšie, spolu s rýchlou metódou riešenia problémov.
| symptóm | Príčina pravdepodobná | overenie | Riešenie |
|---|---|---|---|
| HTTP 401 / „neoprávnené“ | Neplatný alebo chýbajúci kľúč API | skontrolovať BPCAB_OPENAI_API_KEY + surová odpoveď v details.body |
Opravte kľúč, skontrolujte práva k projektu na strane dodávateľa |
| HTTP 429 | Prekročená kvóta / limit sadzby dodávateľa | regarder details.status a telo API |
Počkaj, zníž hlasitosť, povoľ vyrovnávaciu pamäť, použi ľahší model |
| Timeout | Časový limit je príliš krátky alebo server je pomalý. | PHP logy + dočasné zvýšenie timeout |
Zvýšte na 40 sekúnd v dávkovom režime alebo preložte mimo frontu (WP-CLI). |
| Nefunkčný HTML (nástroj na tvorbu rozloženia) | Model má upravené shortcodes/atribúty | Porovnajte originál a preklad | Vylepšiť výzvu, preložiť iba textové oblasti, uložiť podľa modulu |
| Preklad nikdy „skrytý“ | Nestabilný kľúč vyrovnávacej pamäte (iný obsah pri každom volaní) | Zaznamenať hash (pri príprave) | Pred hašovaním vyčistite obsah (odstráňte dynamické bloky) alebo ho uložte do metadát. |
| Chyba 403 na koncovom bode | Chýbajúci/neplatný REST nonce alebo nedostatočná kapacita | Skontrolujte hlavičku X-WP-Nonce + používateľská rola |
Správne vygenerujte nonce, upravte permission_callback |
„Hlúpe“, ale časté chyby
- Kód vložený na nesprávne miesto : v úryvku pluginu, ktorý minifikuje/upravuje PHP a porušuje kódovanie. Uprednostňujte súbor mu-plugin.
- Chýbajúca bodkočiarka Zobrazuje sa vám chyba 500. Pozrite sa.
wp-content/debug.log. - Nevhodný háčik : ak sa pokúsite zavolať REST API predtým
rest_api_initNič nie je deklarované. - Testovanie výroby Spustíte desiatky platených hovorov. Pripravte si svoje úsilie a potom migrujte.
- PHP je príliš staré písanie + kláves Enter
: WP_REST_ResponseMôžu sa pokaziť na PHP 7.x. Tu sa zameriavame na PHP 8.1+.
zdroje
- wp_remote_post() — Zdroje pre vývojárov WordPressu
- Príručka REST API — WordPress
- set_transient() — Zdroje pre vývojárov WordPressu
- wp_kses_post() — Zdroje pre vývojárov WordPressu
- Referencia OpenAI API
- Ladenie WordPressu — Zdroje pre vývojárov
- WordPress (zrkadlo) na GitHub – na vyhľadávanie základného kódu
- WordPress Core Trac — História a tikety
- json_decode() — PHP.net
Často kladené otázky
Je to naozaj „bez pluginov“?
Bez prekladového pluginu tretej strany áno. Technicky vzaté, stále pridávate kód vo forme mu-pluginu (alebo vlastného pluginu). Je to zámerné: zachovávate si kontrolu a vyhýbate sa zložitému systému.
Vytvára sa týmto spôsobom preložené stránky s URL adresami /en/?
Nie, s týmto kódom nie. Preklad zobrazíte za chodu prostredníctvom ?lang=en alebo ho uložíte do meta tagov. Pre skutočnú štruktúru URL adresy pre každý jazyk musíte vytvoriť vrstvu smerovania + hreflang + súbory Sitemap (alebo použiť viacjazyčný doplnok).
Prečo neprekladať priamo? post_content a uložiť príspevok?
Pretože riskujete prepísanie zdrojového kódu. Vždy oddeľte zdrojový kód od prekladu (meta, CPT „preklad“ alebo duplikácia). Videl som stránky, ktoré stratili svoj pôvodný obsah po zlej prekladovej slučke.
Model niekedy vracia text, ktorý nie je vo formáte JSON. Čo mám robiť?
Toto je bežné. V tomto prípade kód zámerne zlyháva. Na skutočnej stránke môžete pridať krok „opravy“ (opätovné spustenie výzvy), ale to zdvojnásobí náklady. Uprednostňujem čisté zlyhanie a následnú kontrolu. model_output na inscenácii.
Ako mám preložiť aj úryvok?
pridať excerpt V úžitkovej záťaži vyžiadajte JSON s excerptPotom ho uložte ako meta. Zachovajte rovnaký princíp: dezinfikujte text, nie HTML.
Ako spravovať prehľadný glosár?
Uložte ho do možnosti (napr. get_option('bpcab_translation_glossary')) a odovzdať ho funkcii. Glosár s 20 – 50 riadkami výrazne mení konzistenciu, najmä pri výrazoch týkajúcich sa značiek.
Funguje to s Elementorom, ak sú moje stránky 100% z Elementoru?
Záleží na tom. Ak je váš „skutočný“ obsah v _elementor_data, preložiť post_content nestačí. V prípade Elementoru musíte buď preložiť konečný výstup (riskantné), alebo napísať prekladač, ktorý prehľadá JSON súbor z Elementoru a preloží iba textové polia.
Prečo používať chat/completions A nie vyhradený koncový bod pre „preklad“?
Pretože prístup „chat“ umožňuje obmedziť formát (JSON) a nastaviť prísne pravidlá (zachovanie HTML/shortcodes). Čistý koncový bod „prekladu“ je niekedy jednoduchší, ale ponúka menšiu kontrolu nad výstupným formátom.
Ako sa vyhnúť prekladu nechcených častí (kódu, úryvkov)?
Pridajte do výzvy pravidlo: „Neprekladať obsah v rámci značiek“ <code> et <pre>Ak máte veľa kódu, môžete tiež predspracovať HTML a nahradiť tieto bloky zástupnými symbolmi pred odoslaním a potom ich znova vložiť.
Prechodná vyrovnávacia pamäť sa na mojom hostingu neukladá. Prečo?
Niektorí poskytovatelia hostingu agresívne čistia vyrovnávacie pamäte alebo vaša stránka môže bežať s vyrovnávacou pamäťou objektov, ktorá má vlastné pravidlá. V takom prípade uložte vyrovnávaciu pamäť do metaznačiek (odolnejšie) alebo pridajte správne nakonfigurovanú trvalú vyrovnávaciu pamäť objektov (Redis).
Môžem nahradiť OpenAI systémami Mistral/Anthropic/Google?
Áno: zachovať rovnakú architektúru (cache + sanitácia + chyby), iba nahradiť bpcab_call_openai_translation() funkciou, ktorá volá ich koncový bod s wp_remote_post()Zvyšok nemeňte, pokiaľ váš výstupný formát zostane „striktný JSON“.
