Agar siz inglizcha versiyasini tezda nashr etish uchun soat 23:00 da Google Translate’ga paragrafni qoʻlda joylashtirgan boʻlsangiz, muammoning nimada ekanligini bilasiz: u sekin, nomuvofiq va muharrirda nusxa koʻchirish-joylashtirish vazifasiga aylanadi. WordPress hech qanday kuzatuvsiz.

WordPress 6.9.4 versiyasi (2026-yil aprel) allaqachon yaxshi vositalarni (REST API, transients, hooks) taqdim etadi, lekin u hech narsani asl holatida "tarjima qilmaydi". Bu yerdagi g'oya oddiy: AI tarjima API-sini ulash orqali wp_remote_post()hech qanday plaginlarni o'rnatmasdan va keshlash, xarajatlar va xavfsizlik ustidan nazoratni saqlab qolmasdan.

Ehtiyoj / Foydalanish holati

Aniq muammo: sizda kontent bor (maqolalar(sahifalar, ba'zan ACF maydonlari) va siz ma'lumotlar bazangizni va orqa ofisingizni o'zgartiradigan og'ir (va ko'pincha qimmat) tarjima plaginini qo'shmasdan, tezda, maqbul sifatda tarjima qilingan versiyani yaratmoqchisiz.

Men bu ehtiyojni ko'pincha quyidagi hollarda ko'rganman:

  • Uzoq quyruqli SEO uchun inglizcha versiyasini "yetarli" qilishni istagan fransuz texnik bloglari,
  • 10 ta sahifa 2 tilda mavjud bo'lishi kerak bo'lgan veb-saytlarni (Avada/Divi/Elementor) namoyish eting,
  • tarjimasi deyarli hech qachon o'zgarmaydigan juda barqaror kontentga ega saytlar (hujjatlar, huquqiy sahifalar).

Oxir-oqibat, qanday amalga oshirishni bilib olasiz:

  • administrator tomonidan tarjima so'rash uchun xavfsiz REST so'nggi nuqtasi,
  • orqali AI tarjima mexanizmi (masalan, OpenAI). wp_remote_post(),
  • Har bir post uchun bitta kesh + vaqtinchalik til,
  • filtr orqali "tezkor" renderlash (xabarlarni takrorlamasdan) the_content (ixtiyoriy),
  • agar API sekin yoki ishlamayotgan bo'lsa, toza zaxira strategiyasi xato.

Tez xulosa

  • Siz API kalitini saqlaysiz wp-config.php (hech qachon qattiq shaklda emas).
  • Siz faqat administrator tomonidan boshqariladigan REST so'nggi nuqtasini ochadigan mini-plagin (yoki mu-plagin) yaratasiz.
  • Plagin AI API ni quyidagi bilan chaqiradi wp_remote_post() + vaqt tugashi + xatolarni qayta ishlash.
  • Tarjima keshda saqlanadi set_transient() (har bir post/til uchun + kontent xeshi).
  • 1-variant: "Tezkor" displey (takrorlashsiz). 2-variant: Metama'lumotlarni yaratish va saqlash.
  • Siz tezlikni cheklash + HTML tozalashni qo'shasiz (wp_kses_post()) yoqimsiz kutilmagan hodisalardan qochish uchun.

Buning uchun qachon AI dan foydalanish kerak

Ushbu yondashuvdan quyidagi hollarda foydalaning:

  • Siz to'liq ko'p tilli infratuzilmani yaratmasdan "yaxshi" tarjimani xohlaysiz,
  • Kontent asosan matnli (maqolalar, sahifalar),
  • Agar siz lug'at taqdim etsangiz, nomukammal, ammo izchil tarjimani qabul qilasiz.
  • siz xarajatlarni nazorat qilmoqchisiz (agressiv keshlash, talab bo'yicha tarjima),
  • Siz o'zining ma'lumotlar modelini yuklaydigan plaginga bog'liq bo'lishni xohlamaysiz.

Mening tajribamga ko'ra, bu sahifalarning 80% maqolalar bo'lgan va tarjima asosan qat'iy qonuniy mahalliylashtirish uchun emas, balki sotib olish (SEO + xalqaro o'quvchilar) uchun ishlatiladigan tahririyat saytlari uchun juda yaxshi ishlaydi.

Qachon AI dan foydalanmaslik kerak

Quyidagi hollarda sun'iy intellektdan saqlaning (yoki undan foydalanishni cheklang):

  • Sizda qonuniy talablar mavjud (shartlar va qoidalar, tibbiy, moliyaviy): tekshirilmagan sun'iy intellekt tarjimasi xavf tug'diradi.
  • Sizda allaqachon WPML/Polylang mavjud va haqiqiy ko'p tilli strategiya (har bir til uchun URL manzillari, hreflang, menyular va boshqalar) mavjud,
  • Siz interfeys satrlarini (mavzu satrlari) tarjima qilishingiz kerak: maxsus vosita (gettext) eng yaxshisidir.
  • Siz yuqori dinamik kontentni (sharhlar, UGC) tarjima qilishingiz kerak: narx + GDPR + moderatsiya.
  • Sizda juda katta sayt bor (10 mingdan ortiq post) va siz "hamma narsani birdaniga tarjima qila olaman" deb o'ylaysiz: qonun loyihasi va kvotalar sizni tinchlantiradi.

Soddaroq, "klassik" alternativa: agar sizning yagona ehtiyojingiz tilga qarab turli xil kontentni namoyish qilish bo'lsa, 10 ta sahifani qo'lda nusxalash va har bir til uchun menyu yaratish ba'zan eng yaxshi investitsiya daromadliligi hisoblanadi. Sun'iy intellekt asosan butun saytingizni qayta ko'rib chiqmasdan avtomatlashtirishni xohlaganingizda foydalidir.

qo'ymaydi

versiyalari

  • WordPress 6.9.4+ (2026-yil aprel)
  • PHP 8.1+ (agar xosting provayderingiz qo'llab-quvvatlasa, 8.2/8.3 tavsiya etiladi)
  • HTTPS yoqilgan bo'lishi kerak (aks holda API chaqiruvi yomon fikr)

API kaliti (OpenAI misoli)

Siz API dan HTTP orqali foydalanasiz. Dastlab SDK yo'q, Composer yo'q. Rasmiy hujjatlar:

Kalit wp-config.php faylida saqlanadi

Buni qo'shing wp-config.phpIdeal holda, xosting provayderi tomonidan kiritilgan muhit o'zgaruvchisi orqali (undan ham yaxshiroq), aks holda qattiq kodlangan wp-config.php (agar fayl yaxshi himoyalangan bo'lsa, qabul qilinadi).

/** 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');

Klassik tuzoq: bu doimiyni ichkariga joylashtirish functions.phpMen uni hali ham Divi/Avada saytlarida ko'raman: birinchi mavzu tugmachasida tugma "yo'qoladi". Uni joylashtiring wp-config.php yoki muhit o'zgaruvchisi sifatida.

Yechim arxitekturasi

Oqim (matn sxemasi):

WordPress administratori → REST API (xavfsiz so'nggi nuqta) → wp_remote_post() → AI API (tarjima) → tasdiqlash + tozalash → kesh (vaqtinchalik + meta opsiyasi) → JSON qaytishi → displey (filtr orqali ixtiyoriy)

Sahna ortida nima bo'lyapti

  • kirish : a post_id, manba tili, maqsadli til va ehtimol “lugʻat”.
  • Ekstraksiya : biz kontentni (va agar xohlasangiz, sarlavhani) olamiz, keyin uni tayyorlaymiz (HTML saqlangan).
  • qoplama Biz kesh kalitini kontent xeshi + maqsadli til asosida hisoblaymiz. Agar bu o'zgarmagan bo'lsa, biz API uchun pul to'lamaymiz.
  • API chaqiruvi JSON so'rovi, oqilona vaqt tugashi, minimalist qayta urinishlar (cheksiz tsikl yo'q).
  • tozalash Biz qaytarilgan HTMLga ishonmaymiz. Biz foydalanamiz wp_kses_post().
  • Sortie Administrator uchun JSON va ixtiyoriy ravishda til parametriga asoslangan front-end renderlash.

To'liq kod - bosqichma-bosqich

Biz mini-plagin yaratmoqchimiz. Men tavsiya qilaman mu-plagin Agar mijoz uni "sinov maqsadida" o'chirib qo'yishini istamasangiz. Aks holda, standart plagin.

1-qadam — Mu-plagin yarating

yaratish wp-content/mu-plugins/bpcab-ai-translate.phpAgar fayl mu-plugins Agar u mavjud bo'lmasa, uni yarating.

Haqiqiy xato: ko'p odamlar faylni joylashtiradilar wp-content/plugins Keyin ular uni faollashtirishni unutishadi. Mu-plagin avtomatik ravishda yuklanadi.

2-qadam — Faqat administrator tomonidan boshqariladigan REST so'nggi nuqtasini e'lon qiling

Biz faqat qobiliyatli foydalanuvchi uchun ishlaydigan so'nggi nuqtani ochib beramiz edit_posts (kerak bo'lganda sozlang) va bu REST nonce talab qiladi.

<?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;
}

3-qadam — Tarjima funksiyasini yarating (kesh + API)

Biz quyidagilarni qilmoqchimiz:

  • postni qaytarib olish,
  • barqaror kesh kalitini hisoblang,
  • agar kerak bo'lsa, API ni chaqiring,
  • Dezinfektsiyalovchi vosita qaytdi.
  • ixtiyoriy ravishda post metada saqlang.
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);
}

4-qadam — wp_remote_post() orqali vaqtinchalik kesh + OpenAI chaqiruvi

Asosiy nuqta: Agar tarkib o'zgarsa, kesh kaliti o'zgarishi kerak.Men kontent + sarlavha + lug'at + shablonning xeshidan foydalanaman. Aks holda, siz bir necha kun davomida eskirgan tarjimani taqdim etasiz.

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,
	];
}

5-qadam — (Ixtiyoriy) Tarjimani old tomonda tezkor ravishda ko'rsatish

Agar siz "/en/…" sahifalarini yaratishni xohlamasangiz, sozlama orqali tarjimani darhol ko'rsatishingiz mumkin. ?lang=enSinov uchun qulay, ammo bu to'liq ko'p tilli SEO strategiyasi emas.

Men buni ko'pincha tasdiqlash bosqichida qilaman: mijoz bosadi, taqqoslaydi, biz lug'atni sozlaymiz va shundan keyingina metada saqlash yoki sahifalarni nusxalash to'g'risida qaror qabul qilamiz.

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);

Keng tarqalgan xato bu filtrni juda past ustuvorlik bilan belgilash (masalan, 1) va keyinroq ishga tushadigan Elementor/Divi/Avada qisqa kodlarini buzishdir. 20-ustuvorlik ko'pincha yaxshi murosaga kelishdir. Agar sizning konstruktoringiz kontentni o'zining ilgaklari orqali kiritsa, buni sozlashingiz kerak bo'lishi mumkin.

To'liq yig'ilgan kod

Ushbu faylni nusxalash va joylashtirish wp-content/mu-plugins/bpcab-ai-translate.phpAPI kaliti ichida qoladi 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);

Kodning izohi

Nima uchun administrator panelidagi tugma o'rniga REST so'nggi nuqtasi?

Chunki REST so'nggi nuqtasi barqaror "kirish nuqtasi" hisoblanadi. Shunda siz quyidagilarni amalga oshirishingiz mumkin:

  • administrator panelidagi kichik JS skriptidan tarjimani chaqiring.
  • WP-CLI orqali ommaviy tarjimalarni ishga tushirish (quyidagi variant),
  • tahririyat ish jarayonini ulang.

Va eng muhimi: REST sizni ruxsatnomalarni + nonce ni to'g'ri boshqarishga majbur qiladi.

Nima uchun variant yoki fayl emas, balki Transients keshidan foydalanish kerak?

Vaqtinchalik vosita qulay, chunki:

  • Uning mahalliy yaroqlilik muddati bor.
  • Agar sizda obyekt keshi (Redis/Memcached) bo'lsa, u bilan mos keladi.
  • stolni ifloslanishidan saqlaydi postmeta tezkor testlar uchun.

"Jiddiy" ko'p tilli ishlab chiqarish muhitiga o'tganingizda, ehtimol ma'lumotlarni meta teglarda saqlaysiz (yoki tarjima qilingan postlar yaratasiz). Bu yerda vaqtinchalik jarayon sizning xarajatlaringiz buferi vazifasini bajaradi.

pourquoi wp_kses_post() Sun'iy intellektga javob haqida nima deysiz?

Chunki siz model nimani qaytarishi ustidan 100% nazoratga ega emassiz. Agar siz undan "qat'iy JSON" dan foydalanishni so'rasangiz ham, model baribir ishlamay qolishi, teglar kiritishi yoki HTML kodingizni o'zgartirish orqali "tuzatishi" mumkin.

wp_kses_post() WordPress kontekstida ruxsat etilgan teglarning oq ro'yxatini qo'llaydi. Rasmiy hujjatlar: wp_kses_post().

Nima uchun kesh kalitida foydali yukning xeshi mavjud?

Heshlashsiz siz "123-xabarni EN da" keshlaysiz va hatto postni tahrirlasangiz ham bir xil tarjimani taqdim etasiz. Heshlash murakkab tozalash mantig'ini talab qilmasdan keshni o'zgarishlarga javob beradi.

API xarajatlari va optimallashtirish

Narxi yetkazib beruvchiga, modelga va ayniqsa matn hajmiga bog'liq. Men sizga va'da emas, balki realistik hisoblash usulini taqdim etyapman.

Amaliy baho

  • "O'rtacha" blog posti (800–1200 so'z) ko'pincha seriyalashtirilgandan so'ng bir necha ming tokenni ifodalaydi (HTML + qisqa kodlar + so'rov).
  • Agar siz oyiga 100 ta maqolani keshlashsiz tarjima qilsangiz, oyiga kamida 100 ta qo'ng'iroq uchun to'laysiz.

Men ataylab umumiy so'zlarni saqlab qolaman: narx = kirish_tokens × kirish_narxi + chiqish_tokens × chiqish_narxiNarxlar tez-tez o'zgarib turadi; yetkazib beruvchingizning narxlash sahifasini tekshiring.

Darhol ta'sir ko'rsatadigan optimallashtirishlar:

  • Uzoq kesh (30 kun yoki undan ko'proq) va kontent xeshiga asoslangan kalit.
  • "Mini" model tarjima uchun (ko'pincha yetarli).
  • So'rovni kamaytiring : sizning system barqarorlashgandan so'ng qisqaroq bo'lishi mumkin.
  • Faqat yakuniy tarjimani tarjima qiling : bir xil blokni 10 marta yuborishdan saqlaning (shablonlar quruvchisi).

Men tez-tez ko'radigan xarajatlar tuzog'i

Odamlar ishlab chiqarishda sinovdan o'tkazmoqdalar, sahifani 30 marta yangilamoqdalar ?lang=enva nima uchun hisob-kitob oshib borayotganiga hayron bo'laman. Keshsiz har bir yangilanish qo'ng'iroqni ishga tushiradi. Transient + hash yordamida siz darhol barqarorlashasiz.

Murakkab variantlar va foydalanish holatlari

1-variant — Tarjima qiling va metama'lumotlarda saqlang, keyin filtr orqali ko'rsating

Agar siz old tomonda API chaqiruvlaridan qochmoqchi bo'lsangiz, ma'lumotlarni meta teglarda saqlang (store_as_meta=truekeyin metani qachon ko'rsating ?lang=xx mavjud.

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);

2-variant — WP-CLI orqali ommaviy ravishda (g'oya, to'liq kod emas)

Bir vaqtning o'zida 200 ta postni tarjima qilish uchun WP-CLI ko'pincha administrator panelidan HTTP so'rovlariga qaraganda ishonchliroq. Siz identifikatorlar, qo'ng'iroqlar orqali sikllarni bajaradigan buyruq yaratishingiz mumkin. bpcab_translate_with_cache() va metama'lumotlarda saqlaydi.

Maqola diqqatni jalb qilish uchun men bu yerga to'liq WP-CLI kodini kiritmayapman, ammo rasmiy hujjatlar aniq: WP-CLI buyruqlari bo'yicha oshpazlik kitobi.

Variant 3 — Divi 5 / Elementor / Avada mosligi

  • 5-bo'lim Kontentning katta qismi qisqa kodlar/tuzilmalarda saqlanadi. "Qisqa kodlarni o'zgartirmang" degan so'rov juda muhim. Murakkab sahifada sinab ko'ring, aks holda modullaringiz buzilgan bo'ladi.
  • Elementor : tarkibning bir qismi ichida _elementor_data (JSON). Ushbu JSON kodini shu holatda ishlatib tarjima qilmang. Buning o'rniga, "renderlangan" kontentni tarjima qiling (yoki alohida loyiha bo'lgan Elementor sxemasi uchun maxsus tarjimon yarating).
  • Avada (Fusion Builder) Divi bilan bir xil mantiq, ko'plab Fusion qisqa kodlari. Ularni faqat o'zingizda saqlang, aks holda siz maketni yo'qotasiz.

Agar saytingiz asosan quruvchilarga asoslangan bo'lsa, eng xavfsiz strategiya: faqat tarjima qilish matn qutilari (vidjetlar/modullar) va tuzilma emas. Bu yerda siz har bir quruvchi uchun ma'lum bir ishlab chiqish bilan shug'ullanasiz.

Xavfsizlik va eng yaxshi amaliyotlar

API kalitini mijoz tomonida hech qachon oshkor qilmang

OpenAI/Anthropicni to'g'ridan-to'g'ri chaqiradigan JavaScript yo'q. Kalitingiz brauzerda tugaydi. Barcha so'rovlar WordPress serveringiz orqali o'tishi kerak.

Minimal stavka cheklovi

REST so'nggi nuqtasini osongina buzib kirish mumkin (hatto qo'pol administrator tomonidan ham). Har bir foydalanuvchi uchun oddiy qulf qo'shing.

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;
}

Siz bu funksiyani boshida chaqirishingiz mumkin bpcab_translate_endpoint() avec get_current_user_id()Bu mukammal emas, lekin "Men 50 marta bosaman" degan vaziyatdan qochadi.

Kirishni tasdiqlash

  • Qat'iy dezinfektsiya qiling: absint, sanitize_key, sanitize_textarea_field.
  • Post turlarining oq ro'yxati.
  • Ekzotik kesh kalitlaridan qochish uchun til kodlari bo'yicha Regex.

GDPR / API ga yuborilgan ma'lumotlar

Agar tarjima qilsangiz:

  • foydalanuvchi ma'lumotlari (izohlar, shakllar),
  • maxfiy ma'lumotlar (elektron pochta manzillari,

Siz ushbu ma'lumotlarni uchinchi tomonga yubormoqdasiz. Audit o'tkazing: huquqiy asos, ma'lumotlar bazasi, saqlash muddati, anonimlashtirish. Yuqoridagi kod hech narsani anonimlashtirmaydi.

Qanday qilib sinovdan o'tkazish va disk raskadrovka qilish kerak

1) Jurnalni yoqish

yilda wp-config.php (sahnalashtirish muhitida):

define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);

Rasmiy hujjat: WordPress-da disk raskadrovka.

2) REST so'nggi nuqtasini curl bilan sinab ko'ring

Administrator sessiyangizdan REST nonce ni oling (masalan, orqali) wpApiSettings.nonce (agar sizda uni ochadigan administrator sahifasi bo'lsa) yoki administrator panelida REST vositasidan foydalaning. Misol curl (sxematik):

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) Keshni tekshiring

Oddiy test: bir xil so'rovni ikki marta bajaring. Ikkinchi marta natija qaytishi kerak "cached": trueAgar bunday bo'lmasa, sizda quyidagilar mavjud:

  • o'zgaruvchan kontent (vaqt belgilarini kiritadigan quruvchi),
  • boshqa lug'at,
  • boshqa model,
  • yoki agressiv ravishda tozalaydigan obyekt keshi.

4) Model chiqishini tekshiring

Agar "Noto'g'ri tarjima formati" xatosini ko'rsangiz, tizimga kiring model_output (sinov muhitida) model aslida nimani qaytarayotganini tushunish uchun.

Agar bu ishlamasa

Mana men eng ko'p duch keladigan nosozliklar, tezkor muammolarni bartaraf etish usuli bilan.

alomat Ehtimoliy sabab tekshiruv qaror
HTTP 401 / “ruxsatsiz” API kaliti noto'g'ri yoki yo'q nazorat BPCAB_OPENAI_API_KEY + xom javob details.body Kalitni to'g'rilang, yetkazib beruvchi tomonidagi loyiha huquqlarini tekshiring
Http429 Kvotadan oshib ketdi / yetkazib beruvchi narxining chegarasi nazar details.status va API tanasi Kuting, ovoz balandligini pasaytiring, keshni yoqing, yengilroq modeldan foydalaning
Vaqt tugashi Vaqt tugashi juda past yoki server sekin PHP jurnallari + vaqtincha oshirish timeout Ommaviy rejimda 40 soniyagacha oshiring yoki off-front rejimida tarjima qiling (WP-CLI).
Buzilgan HTML (maket quruvchisi) Modelda o'zgartirilgan qisqa kodlar/atributlar mavjud Asl nusxasini tarjima qilingani bilan solishtiring So'rovni yaxshilang, faqat matn maydonlarini tarjima qiling, modul bo'yicha saqlang
Tarjima hech qachon "yashirin" emas Kesh kaliti beqaror (har bir qo'ng'iroqda turli xil kontent) Xeshni qayd qilish (sahnada) Tarkibni xeshlashdan oldin tozalang (dinamik bloklarni olib tashlang) yoki uni metama'lumotlarda saqlang.
Oxirgi nuqtada 403 xatosi REST yo'q/noto'g'ri yoki yetarli sig'im yo'q Sarlavhani tekshiring X-WP-Nonce + foydalanuvchi roli Nonce ni to'g'ri yarating, sozlang permission_callback

"Ahmoqona", lekin tez-tez xatolar

  • Kod noto'g'ri joyga joylashtirildi : PHP ni minimallashtiradigan/tahrirlaydigan va kodlashni buzadigan plagin parchasida. mu-plagin faylini afzal ko'raman.
  • Nuqtali vergul yo'q Siz 500 xatolik olmoqdasiz. Qarang wp-content/debug.log.
  • Noto'g'ri ilmoq : agar siz oldin REST API-ni chaqirishga harakat qilsangiz rest_api_initHech narsa e'lon qilinmagan.
  • Ishlab chiqarish sinovlari Siz o'nlab pullik qo'ng'iroqlarni ishga tushirasiz. Harakatlaringizni amalga oshiring, keyin boshqa joyga o'ting.
  • PHP juda eski : yozish + qaytarish : WP_REST_Response Ular PHP 7.x da ishlamay qolishi mumkin. Bu yerda biz PHP 8.1+ ni nishonga olamiz.

resurslari

FAQ

Bu haqiqatan ham "plaginsiz"mi?

Uchinchi tomon tarjima plaginisiz, ha. Texnik jihatdan, siz hali ham mu-plagin (yoki maxsus plagin) shaklida kod qo'shmoqdasiz. Bu ataylab qilingan: siz nazoratni saqlab qolasiz va murakkab tizimdan qochasiz.

Bu /en/ URL manzillari bilan tarjima qilingan sahifalarni yaratadimi?

Yo'q, bu kod bilan emas. Siz tarjimani tezkor ravishda ko'rsatasiz ?lang=en yoki uni meta teglarda saqlaysiz. Har bir til uchun haqiqiy URL tuzilishi uchun siz marshrutizatsiya qatlami + hreflang + sayt xaritalarini yaratishingiz kerak (yoki ko'p tilli plagindan foydalaning).

Nega to'g'ridan-to'g'ri tarjima qilmaysiz? post_content va postni saqlang?

Chunki siz manba kodingizni qayta yozish xavfiga duch kelasiz. Har doim manba kodini va tarjimani (meta, CPT “tarjimasi” yoki takrorlash) alohida saqlang. Men saytlarning noto'g'ri tarjima siklidan keyin asl mazmunini yo'qotganini ko'rganman.

Model ba'zan JSON bo'lmagan matnni qaytaradi. Nima qilishim kerak?

Bu keng tarqalgan holat. Bu holda kod ataylab xatolikka uchraydi. Haqiqiy saytda siz "ta'mirlash" bosqichini (qayta so'rov) qo'shishingiz mumkin, ammo bu xarajatlarni ikki baravar oshiradi. Men toza ravishda xatolikni tekshirib ko'rishni afzal ko'raman. model_output sahnalashtirishda.

Qanday qilib parchani tarjima qilaman?

qo'shish excerpt Yuklamada, JSON so'rovini yuboring excerptKeyin uni meta sifatida saqlang. Xuddi shu tamoyilga amal qiling: HTML emas, balki matnni dezinfektsiya qiling.

Toza lug'atni qanday boshqarish mumkin?

Uni variantda saqlang (masalan, get_option('bpcab_translation_glossary')va uni funksiyaga o'tkazing. 20–50 qatorli lug'at, ayniqsa brend atamalari uchun izchillikni sezilarli darajada o'zgartiradi.

Agar sahifalarim 100% Elementor bo'lsa, u Elementor bilan ishlaydimi?

Bu bog'liq. Agar sizning "haqiqiy" kontentingiz ichkarida bo'lsa _elementor_data, tarjima qilish post_content yetarli emas. Elementor uchun siz yakuniy natijani tarjima qilishingiz kerak (xavfli) yoki Elementor JSON faylini skanerlaydigan va faqat matn maydonlarini tarjima qiladigan tarjimon yozishingiz kerak.

Nima uchun foydalanish kerak chat/completions Va maxsus "tarjima" so'nggi nuqtasi emasmi?

Chunki "chat" yondashuvi formatni (JSON) cheklash va qat'iy qoidalarni belgilash (HTML/qisqa kodlarni saqlash) imkonini beradi. Sof "tarjima" so'nggi nuqtasi ba'zan sodda, ammo chiqish formati ustidan kamroq nazoratni taklif qiladi.

Keraksiz qismlarni (kod, parchalar) tarjima qilishdan qanday qochish kerak?

So'rovga qoida qo'shing: “Teglar ichidagi kontentni tarjima qilmang” <code> et <pre>Agar sizda juda ko'p kod bo'lsa, HTMLni oldindan qayta ishlashingiz va yuborishdan oldin bu bloklarni joy egalari bilan almashtirishingiz, keyin ularni keyinroq qayta kiritishingiz mumkin.

Vaqtinchalik kesh mening xostingimda saqlanmayapti. Nima uchun?

Ba'zi xosting provayderlari keshlarni agressiv ravishda tozalaydi yoki saytingiz o'z qoidalariga ega bo'lgan obyekt keshi bilan ishlayotgan bo'lishi mumkin. Bunday holda, keshni meta teglarda saqlang (bardoshliroq) yoki to'g'ri sozlangan doimiy obyekt keshini (Redis) qo'shing.

OpenAI ni Mistral/Anthropic/Google bilan almashtira olamanmi?

Ha: bir xil arxitekturani saqlang (kesh + sanitariya + xatolar), faqat almashtiring bpcab_call_openai_translation() ularning oxirgi nuqtasini chaqiradigan funksiya orqali wp_remote_post()Chiqish formatingiz "qat'iy JSON" bo'lib qolsa, qolganini o'zgartirmang.