This article documents the provider layer of the WPEstate Translate plugin (folder wpestate-translate): the admin view, the settings option, the normalized request, the four provider files, the OpenAI request shape and the AJAX endpoints that drive bulk runs. It complements the user article and the broader multi-language real estate website overview.
Files Involved
| File | Role |
|---|---|
| includes/admin/views/automatic-translation.php | Settings form + bulk-run UI (post-type selection, target language, start/stop). |
| includes/admin/post-editor.php | wpr_translate_auto_translate_post() – builds the request, dispatches it to the selected engine and writes the result to the translated post. Also hosts the editor AJAX handlers. |
| includes/admin/translation-request.php | wpr_translate_build_normalized_engine_request() and wpr_translate_normalize_engine_response() – one request shape shared by every engine. |
| includes/admin/auto-translate-openai.php | Implemented provider. Transport adapter wpr_translate_openai_transport_request() and the HTTP helper wpr_translate_openai_translate_json_payload(). wpr_translate_auto_translate_post_openai() is kept as a legacy public wrapper. |
| includes/admin/auto-translate-google.php | Stub. Returns WP_Error( ‘wpr_translate_google_not_implemented’ ). |
| includes/admin/auto-translate-deepl.php | Stub. Returns WP_Error( ‘wpr_translate_deepl_not_implemented’ ). |
| includes/admin/auto-translate-microsoft-azure.php | Stub. Returns WP_Error( ‘wpr_translate_azure_not_implemented’ ). |
Options & Storage
All settings live in one option, wpr_translate_auto_translation, registered with the sanitizer wpr_translate_admin_sanitize_auto_translation_settings() (includes/admin/settings-helpers.php):
| Key | Purpose |
|---|---|
| enabled | bool. |
| engine | One of openai, google_translate, microsoft_azure, deepl. Anything else is saved as openai. |
| openai_api_key, openai_system_prompt, openai_user_prompt | OpenAI credentials and prompt templates. |
| post_types | Per-post-type choice (yes/no) for bulk runs. Written by the form and by the save_post_type_preferences AJAX call. |
| target_language | Language code selected for the bulk run. |
There is no glossary or translation memory: the wpestate_translation_glossary and wpestate_translation_memory tables are dropped on activation, and no admin page renders the glossary view.
Settings Form Defaults
The view applies these defaults via wp_parse_args():
'enabled' => false, 'engine' => 'openai', 'openai_api_key' => '', 'openai_system_prompt' => 'You are a translator that converts WordPress post data into the requested language.', 'openai_user_prompt' => 'Translate the following WordPress post payload into the %1$s language.',
The $engines map in the view contains only ‘openai’ => ‘OpenAI’, so OpenAI is the only engine an admin can pick, even though the sanitizer accepts the other three keys.
Excluded Post Types
wpr_translate_get_auto_translation_excluded_post_types() removes these from the post-type list and from bulk runs: attachment, nav_menu_item, wp_navigation, wp_template, wp_template_part, wp_block, elementor_library, wp_font_family, wp_font_face, wpestate_booking, wpestate_invoice. Change the list with the filter wpr_translate_auto_translation_excluded_post_types.
Normalized Request
wpr_translate_auto_translate_post( $post_id, $language, $engine, $source_post_id ) builds one request for every engine with wpr_translate_build_normalized_engine_request():
array( 'language' => <code>, 'post_type' => <sanitised key>, 'post' => array( 'post_title', 'post_excerpt', 'post_content', 'post_name' ), 'post_meta_fields' => array( ... translatable meta ... ), 'taxonomies' => array( ... terms prepared for LLM ... ), 'elementor' => array( 'units' => array( ... ) ), // only when the Elementor document is ready );
Taxonomy terms pass through wpr_translate_prepare_taxonomy_payload_for_llm(), which strips term identity fields. Elementor text is sent as extracted units, not as raw _elementor_data. The request is encoded with JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES to keep non-Latin characters intact.
The engine response is checked by wpr_translate_normalize_engine_response() before the first write to the target post. It rebuilds the Elementor document and taxonomy identities locally.
Provider Dispatch
wpr_translate_auto_translate_post() switches on the engine key:
case 'google_translate': wpr_translate_auto_translate_post_google( $post_id, $language, $payload, $payload_json ); case 'microsoft_azure': wpr_translate_auto_translate_post_microsoft_azure( ... ); case 'deepl': wpr_translate_auto_translate_post_deepl( ... ); case 'openai': default: wpr_translate_openai_transport_request( ... );
Every adapter takes ( $post_id, $language, $payload, $payload_json ) and must return the decoded translated array or a WP_Error. Only OpenAI does real work today.
OpenAI Request Shape
wpr_translate_openai_translate_json_payload( $language, $payload_json, $settings_context = ‘default’ ) builds:
- Endpoint: https://api.openai.com/v1/chat/completions
- Auth: Authorization: Bearer <openai_api_key>
- Body:
{ "model": "gpt-4o-mini", "response_format": { "type": "json_object" }, "messages": [ { "role": "system", "content": "<system_prompt> Keep the JSON structure identical. Respond with valid JSON only." }, { "role": "user", "content": "<user_prompt_template> Preserve the JSON structure and keys. Respond with JSON only.\n\n<payload_json>" } ] } - Context: posts use the context posts; string runs use strings, which adds “This request contains WordPress string translations.” before the payload.
- Timeout: 540 seconds.
- Retry: up to 3 attempts on WP_Error or non-2xx responses.
- User-Agent: WPR-Translate/<WPR_TRANSLATE_VERSION>; <home_url>
| WP_Error code | When |
|---|---|
| openai_no_api_key | No API key saved. |
| openai_http_error | Transport failure after 3 attempts (timeout, DNS, SSL); message includes the transport error. |
| openai_http_status | Non-2xx after 3 attempts; message includes the status, the API message and a hint for 401 (key rejected) and 429 (rate limit). |
| openai_empty_response | Empty body or no message content. |
| openai_json_decode | Response body is not JSON. |
| openai_api_error | 2xx response that carries an error object. |
| openai_inner_json_decode | The model’s content is not valid JSON. |
AJAX Endpoints
| Action | Callback | Purpose |
|---|---|---|
| wp_ajax_wpr_translate_auto_translate_strings | wpr_translate_admin_ajax_auto_translate_strings | Auto-translate untranslated rows on the String Translation screen (WPEstate Translate > Theme & Plugins Strings). |
| wp_ajax_wpr_translate_get_posts_for_auto_translation | wpr_translate_admin_ajax_get_posts_for_auto_translation | List posts awaiting bulk translation for the selected language + post types. |
| wp_ajax_wpr_translate_save_post_type_preferences | wpr_translate_admin_ajax_save_post_type_preferences | Save post_types and target_language into wpr_translate_auto_translation (manage_options, nonce wpr_translate_save_post_types). |
| wp_ajax_wpr_translate_auto_translate_post | wpr_translate_handle_auto_translate_post_ajax | Translate one post on demand from the post editor. |
| wp_ajax_wpr_translate_auto_translate_post_publish | wpr_translate_handle_auto_translate_post_publish_ajax | Translate and publish. |
The two post handlers require edit_post on the target post and on the source post.
Debug Logging
The OpenAI file has no active request logging. wpr_translate_log_openai_elementor_widget_payload() is still called before and after the request, but returns early (“Debug logging intentionally disabled”). To inspect a failed run, read the WP_Error message returned to the editor or add temporary logging around wpr_translate_openai_translate_json_payload().
Extending With a New Provider
- Implement your adapter in a file of your own with the signature ( $post_id, $language, $payload, $payload_json ).
- Send $payload_json (or the $payload array) to your provider’s endpoint.
- Return the translated payload as an array with the same keys, or a WP_Error.
- Use one of the engine keys the plugin already accepts (google_translate, microsoft_azure, deepl). The sanitizer (settings-helpers.php) and the editor script data (post-editor.php) reset any other key to openai.
- The dispatch switch in wpr_translate_auto_translate_post() calls the matching wpr_translate_auto_translate_post_{engine}() function, and the view’s $engines map must list the engine before an admin can select it. Both are plugin code, so this needs a plugin change, not a child-theme hook.
UTF-8 Safety
All payloads are JSON-encoded with JSON_UNESCAPED_UNICODE, and the response content is decoded without any sanitisation that would corrupt non-Latin text. Keep this invariant in custom providers – do not run translation output through sanitize_title() or sanitize_key().
Known Limitations
- Google/DeepL/Azure files are stubs – do not advertise them as working integrations.
- The model is hardcoded to gpt-4o-mini, and there is no filter to change it.
- OpenAI HTTP timeout is 540 seconds. Match your PHP max_execution_time accordingly for very large posts.
Further Reading
- Gettext Pipeline & MO Files – how translated strings reach the front end.
- Meta Sync Across Language Variants – which meta keys are copied or translated.
- Translation Linking (trid system) – how source and translated posts are paired.
For product positioning see the multi-language real estate website page.