This commit is contained in:
Javier Casares 2026-05-25 17:12:22 +00:00
commit 02cd01e862
20 changed files with 1365 additions and 646 deletions

View file

@ -30,7 +30,7 @@
/**
* Calls the REST endpoint to translate the selected fields of the saved post.
*
* @param {Array<string>} fields Field names to translate ('title', 'content').
* @param {Array<string>} fields Field names to translate ('title', 'content', 'excerpt').
* @param {string} targetLocale WordPress locale code.
* @returns {Promise<Object>} Promise resolving to the translated values.
*/
@ -50,7 +50,7 @@
} );
}
function resolveFields( wantTitle, wantContent ) {
function resolveFields( wantTitle, wantContent, wantExcerpt ) {
var fields = [];
if ( wantTitle && data.settings.translateTitle ) {
fields.push( 'title' );
@ -58,6 +58,9 @@
if ( wantContent && data.settings.translateContent ) {
fields.push( 'content' );
}
if ( wantExcerpt && data.settings.translateExcerpt ) {
fields.push( 'excerpt' );
}
return fields;
}
@ -72,7 +75,7 @@
* Applies a translation response using the right method for the current editor.
*
* In the block editor, uses wp.data dispatch (and wp.blocks.parse for content).
* In the classic editor, updates the DOM directly (title input + TinyMCE).
* In the classic editor, updates the DOM directly (title input + TinyMCE + excerpt textarea).
*
* @param {Object} response Translation response.
* @param {Document} doc Document where the metabox lives (for classic fallback).
@ -98,6 +101,9 @@
wp.data.dispatch( 'core/editor' ).editPost( { content: response.content } );
}
}
if ( response.excerpt !== undefined ) {
wp.data.dispatch( 'core/editor' ).editPost( { excerpt: response.excerpt } );
}
return;
}
@ -111,6 +117,12 @@
if ( response.content !== undefined ) {
applyContentToClassic( response.content, doc );
}
if ( response.excerpt !== undefined ) {
var excerptTextarea = doc.getElementById( 'excerpt' );
if ( excerptTextarea ) {
excerptTextarea.value = response.excerpt;
}
}
}
function applyContentToClassic( content, doc ) {
@ -159,7 +171,11 @@
return;
}
var fields = resolveFields( data.settings.translateTitle, data.settings.translateContent );
var fields = resolveFields(
data.settings.translateTitle,
data.settings.translateContent,
data.settings.translateExcerpt
);
if ( fields.length === 0 ) {
setStatus( labels.noFields || '', 'warning' );
return;
@ -244,6 +260,10 @@
var translateContent = contentState[0];
var setTranslateContent = contentState[1];
var excerptState = useState( !! data.settings.translateExcerpt );
var translateExcerpt = excerptState[0];
var setTranslateExcerpt = excerptState[1];
var coreEditor = dataStore.select( 'core/editor' );
var isDirty = coreEditor && typeof coreEditor.isEditedPostDirty === 'function' ? coreEditor.isEditedPostDirty() : false;
@ -253,7 +273,7 @@
);
}
if ( ! data.settings.translateTitle && ! data.settings.translateContent ) {
if ( ! data.settings.translateTitle && ! data.settings.translateContent && ! data.settings.translateExcerpt ) {
return el( 'div', { className: 'ai-translator-sidebar' },
el( components.Notice, { status: 'info', isDismissible: false }, labels.noFields || '' )
);
@ -270,7 +290,7 @@
} );
function runTranslate() {
var fields = resolveFields( translateTitle, translateContent );
var fields = resolveFields( translateTitle, translateContent, translateExcerpt );
if ( fields.length === 0 ) {
setNotice( { status: 'warning', message: labels.noFields || '' } );
@ -350,6 +370,17 @@
} ) );
}
if ( data.settings.translateExcerpt ) {
children.push( el( components.CheckboxControl, {
key: 'excerpt',
label: labels.translateExcerpt || '',
checked: translateExcerpt,
onChange: function ( value ) {
setTranslateExcerpt( !! value );
},
} ) );
}
children.push( el( components.Button, {
key: 'submit',
variant: 'primary',

View file

@ -1,5 +1,39 @@
== Changelog ==
= 1.1.0 =
_Release date: 2026-05-25_
**Highlights**
* MultilingualPress integration: new connected posts are automatically translated at creation time.
* Excerpt translation: `post_excerpt` is now a translatable field.
* Removed dependency on the third-party AI plugin — uses the native WordPress 7.0 AI client.
**Added**
* Excerpt field translation (title, content, and excerpt are now independently toggleable).
* MultilingualPress integration: opt-in "Auto-translate new connected posts" toggle in the settings pages. When enabled and MLP creates a new connected post, the plugin translates the enabled fields synchronously into the target site's language.
**Changed**
* `Requires Plugins: ai` removed from plugin headers. `wp_ai_client_prompt()` is native to WordPress 7.0 — the plugin works with any provider configured in Settings → AI.
* "AI not active" admin notice replaced by a "no provider configured" notice, which fires when no provider supports text generation.
**Compatibility**
* WordPress: 7.0 - 7.1
* PHP: 7.4 - 8.5
* WP-CLI: 2.12 or newer
* MultilingualPress: 5.x (optional)
**Tests**
* PHP Coding Standards: WordPress-Core, WordPress-Docs, WordPress-Extra
* PHPCompatibility: 7.4 - 8.5
* PHPStan: level 9
* PHPUnit: 50/50 tests passing (single-site)
= 1.0.0 =
_Release date: 2026-05-23_
@ -21,12 +55,13 @@ _Release date: 2026-05-23_
**Compatibility**
* WordPress: 7.0
* WordPress: 7.0 - 7.1
* PHP: 7.4 - 8.5
* WP-CLI: 2.10 or newer
* WP-CLI: 2.12 or newer
**Tests**
* PHP Coding Standards: WordPress-Core, WordPress-Docs, WordPress-Extra
* PHPCompatibility: 7.4 - 8.5
* PHPStan: level 9
* PHPUnit: 50/50 tests passing (single-site)

476
class-robotstxt-updater.php Normal file
View file

@ -0,0 +1,476 @@
<?php
/**
* Generic JSON-based updater for ROBOTSTXT plugins.
*
* Copy this file into any ROBOTSTXT plugin. It auto-configures itself by
* reading the plugin headers and looking for a `Gitea Plugin URI` header to
* construct the remote update.json URL.
*
* Usage in the main plugin file:
* require_once AI_TRANSLATOR_DIR . 'class-robotstxt-updater.php';
* Robotstxt_Updater::init( AI_TRANSLATOR_FILE );
*
* @package ROBOTSTXT
* @since 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'Robotstxt_Updater' ) ) {
/**
* Generic updater that works with any ROBOTSTXT plugin.
*
* Reads the plugin headers to construct the remote update.json URL and
* hooks into WordPress's standard update mechanism.
*
* @since 1.0.0
*/
class Robotstxt_Updater {
/**
* Absolute path to the main plugin file.
*
* @since 1.0.0
* @var string
*/
private $plugin_file_path;
/**
* Plugin basename (e.g. 'my-plugin/my-plugin.php').
*
* @since 1.0.0
* @var string
*/
private $plugin_basename;
/**
* Plugin slug (directory name).
*
* @since 1.0.0
* @var string
*/
private $plugin_slug;
/**
* URL of the remote update.json file.
*
* @since 1.0.0
* @var string
*/
private $json_url;
/**
* Transient cache key for this plugin's remote data.
*
* @since 1.0.0
* @var string
*/
private $cache_key;
/**
* Parsed plugin file headers.
*
* @since 1.0.0
* @var array<string,bool|string>
*/
private $plugin_data;
/**
* Creates the updater instance and registers WordPress hooks.
*
* @since 1.0.0
*
* @param string $plugin_file_path Absolute path to the main plugin file.
*
* @return void
*/
public static function init( $plugin_file_path ) {
$instance = new self( (string) $plugin_file_path );
$instance->register();
}
/**
* Constructor populates instance state from the plugin file headers.
*
* @since 1.0.0
*
* @param string $plugin_file_path Absolute path to the main plugin file.
*/
private function __construct( $plugin_file_path ) {
$this->plugin_file_path = (string) $plugin_file_path;
$this->plugin_basename = plugin_basename( $this->plugin_file_path );
$this->plugin_slug = dirname( $this->plugin_basename );
$this->plugin_data = $this->get_plugin_data();
$this->json_url = $this->build_json_url();
$this->cache_key = 'robotstxt_updater_' . md5( $this->plugin_basename );
}
/**
* Registers the WordPress hooks needed for update checking.
*
* @since 1.0.0
*
* @return void
*/
private function register() {
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'inject_update_info' ) );
add_filter( 'plugins_api', array( $this, 'provide_plugin_details' ), 10, 3 );
add_action( 'admin_init', array( $this, 'handle_cache_clear' ) );
add_action( 'robotstxt_updater_clear_cache', array( $this, 'clear_cache' ) );
}
/**
* Returns the plugin headers, loading the admin helper file if needed.
*
* @since 1.0.0
*
* @return array<string,bool|string>
*/
private function get_plugin_data() {
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$data = get_plugin_data( $this->plugin_file_path, false, false );
return is_array( $data ) ? $data : array();
}
/**
* Builds the remote update.json URL from the plugin headers.
*
* Uses the `Gitea Plugin URI` header when present. Falls back to the
* `Plugin URI` header for legacy configurations. If neither yields a
* usable URL, constructs one from the plugin slug.
*
* @since 1.0.0
*
* @return string Fully-qualified URL.
*/
private function build_json_url() {
$gitea_uri = isset( $this->plugin_data['Gitea Plugin URI'] )
? (string) $this->plugin_data['Gitea Plugin URI']
: '';
if ( '' !== $gitea_uri ) {
// Full URL already provided.
if ( 0 === strpos( $gitea_uri, 'http' ) ) {
return rtrim( $gitea_uri, '/' ) . '/raw/branch/main/update.json';
}
// "OWNER/REPO" short-form.
if ( (bool) preg_match( '#^[^/]+/[^/]+$#', $gitea_uri ) ) {
return 'https://git.robotstxt.es/' . $gitea_uri . '/raw/branch/main/update.json';
}
}
// Fallback: Plugin URI on the Gitea server.
$plugin_uri = isset( $this->plugin_data['PluginURI'] )
? (string) $this->plugin_data['PluginURI']
: '';
if ( '' !== $plugin_uri && false !== strpos( $plugin_uri, 'git.robotstxt.es' ) ) {
return rtrim( $plugin_uri, '/' ) . '/raw/branch/main/update.json';
}
// Last resort: derive from the plugin slug.
return 'https://git.robotstxt.es/ROBOTSTXT/' . $this->plugin_slug . '/raw/branch/main/update.json';
}
/**
* Injects update information into WordPress's plugin update transient.
*
* Hooked to `pre_set_site_transient_update_plugins`.
*
* @since 1.0.0
*
* @param mixed $transient The update_plugins transient value.
*
* @return mixed The (possibly modified) transient.
*/
public function inject_update_info( $transient ) {
if ( ! is_object( $transient ) ) {
return $transient;
}
/**
* Type narrowed to stdClass after is_object() check above.
*
* @var stdClass $transient
*/
if ( empty( $transient->checked ) || ! is_array( $transient->checked ) ) {
return $transient;
}
if ( empty( $transient->checked[ $this->plugin_basename ] ) ) {
return $transient;
}
$checked_value = $transient->checked[ $this->plugin_basename ];
$current_version = is_scalar( $checked_value ) ? (string) $checked_value : '';
$remote = $this->get_remote_data();
if ( ! isset( $remote['version'], $remote['download_url'] ) ) {
return $transient;
}
if ( ! is_string( $remote['version'] ) || ! is_string( $remote['download_url'] ) ) {
return $transient;
}
if ( '' === $remote['download_url'] ) {
return $transient;
}
if ( ! $this->is_compatible( $remote ) ) {
return $transient;
}
if ( version_compare( $remote['version'], $current_version, '>' ) ) {
$slug = isset( $remote['slug'] ) && is_string( $remote['slug'] ) ? $remote['slug'] : $this->plugin_slug;
$homepage = isset( $remote['homepage'] ) && is_string( $remote['homepage'] ) ? $remote['homepage'] : '';
if ( '' === $homepage && isset( $this->plugin_data['PluginURI'] ) ) {
$homepage = (string) $this->plugin_data['PluginURI'];
}
$update = (object) array(
'slug' => $slug,
'plugin' => $this->plugin_basename,
'new_version' => $remote['version'],
'url' => $homepage,
'package' => $remote['download_url'],
'tested' => isset( $remote['tested'] ) && is_string( $remote['tested'] ) ? $remote['tested'] : '',
'requires' => isset( $remote['requires'] ) && is_string( $remote['requires'] ) ? $remote['requires'] : '',
'requires_php' => isset( $remote['requires_php'] ) && is_string( $remote['requires_php'] ) ? $remote['requires_php'] : '',
);
if ( ! isset( $transient->response ) || ! is_array( $transient->response ) ) {
$transient->response = array();
}
$transient->response[ $this->plugin_basename ] = $update;
}
return $transient;
}
/**
* Provides plugin details for the "View details" modal.
*
* Hooked to `plugins_api`.
*
* @since 1.0.0
*
* @param false|object|array<string,mixed> $result The existing result.
* @param string $action The type of information being requested.
* @param object $args Plugin API arguments.
*
* @return false|object|array<string,mixed>
*/
public function provide_plugin_details( $result, $action, $args ) {
if ( 'plugin_information' !== $action ) {
return $result;
}
if ( ! is_object( $args ) || empty( $args->slug ) || $args->slug !== $this->plugin_slug ) {
return $result;
}
$remote = $this->get_remote_data();
if ( empty( $remote['version'] ) ) {
return $result;
}
$name = isset( $remote['name'] ) && is_string( $remote['name'] ) ? $remote['name'] : ( isset( $this->plugin_data['Name'] ) ? (string) $this->plugin_data['Name'] : $this->plugin_slug );
$slug = isset( $remote['slug'] ) && is_string( $remote['slug'] ) ? $remote['slug'] : $this->plugin_slug;
$version = is_string( $remote['version'] ) ? $remote['version'] : '';
$author = isset( $remote['author'] ) && is_string( $remote['author'] ) ? $remote['author'] : ( isset( $this->plugin_data['Author'] ) ? (string) $this->plugin_data['Author'] : '' );
$homepage = isset( $remote['homepage'] ) && is_string( $remote['homepage'] ) ? $remote['homepage'] : ( isset( $this->plugin_data['PluginURI'] ) ? (string) $this->plugin_data['PluginURI'] : '' );
$requires = isset( $remote['requires'] ) && is_string( $remote['requires'] ) ? $remote['requires'] : '';
$tested = isset( $remote['tested'] ) && is_string( $remote['tested'] ) ? $remote['tested'] : '';
$req_php = isset( $remote['requires_php'] ) && is_string( $remote['requires_php'] ) ? $remote['requires_php'] : '';
$description = isset( $remote['description'] ) && is_string( $remote['description'] ) ? $remote['description'] : ( isset( $this->plugin_data['Description'] ) ? (string) $this->plugin_data['Description'] : '' );
$changelog = isset( $remote['changelog'] ) && is_string( $remote['changelog'] ) ? $remote['changelog'] : '';
$download = isset( $remote['download_url'] ) && is_string( $remote['download_url'] ) ? $remote['download_url'] : '';
return (object) array(
'name' => $name,
'slug' => $slug,
'version' => $version,
'author' => $author,
'homepage' => $homepage,
'requires' => $requires,
'tested' => $tested,
'requires_php' => $req_php,
'sections' => array(
'description' => $description,
'changelog' => $changelog,
),
'download_link' => $download,
);
}
/**
* Returns the remote update data, reading from cache or fetching fresh.
*
* The data is stored with an HMAC signature (using AUTH_SALT) to detect
* cache tampering. Falls back to unsigned caching when AUTH_SALT is empty.
*
* @since 1.0.0
*
* @return array<string,mixed>
*/
private function get_remote_data() {
$cached = get_site_transient( $this->cache_key );
// Verify HMAC signature when AUTH_SALT is available.
if ( false !== $cached && defined( 'AUTH_SALT' ) && '' !== AUTH_SALT ) {
if ( is_array( $cached ) && isset( $cached['signature'], $cached['data'] ) && is_string( $cached['signature'] ) ) {
$payload_data = is_array( $cached['data'] ) ? $cached['data'] : array();
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Used only for HMAC; data is never unserialized.
$expected_sig = hash_hmac( 'sha256', $this->cache_key . serialize( $payload_data ), AUTH_SALT );
if ( hash_equals( $expected_sig, $cached['signature'] ) ) {
return $payload_data;
}
// Signature invalid — delete corrupted cache entry.
delete_site_transient( $this->cache_key );
$cached = false;
}
}
if ( false !== $cached ) {
// Legacy cache format (no HMAC).
return is_array( $cached ) ? $cached : array();
}
$remote = $this->fetch_json();
$data = is_array( $remote ) ? $remote : array();
if ( defined( 'AUTH_SALT' ) && '' !== AUTH_SALT ) {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Used only for HMAC; data is never unserialized.
$signature = hash_hmac( 'sha256', $this->cache_key . serialize( $data ), AUTH_SALT );
$payload = array(
'data' => $data,
'timestamp' => time(),
'signature' => $signature,
);
set_site_transient( $this->cache_key, $payload, 6 * HOUR_IN_SECONDS );
} else {
set_site_transient( $this->cache_key, $data, 6 * HOUR_IN_SECONDS );
}
return $data;
}
/**
* Fetches and decodes the remote update.json file.
*
* @since 1.0.0
*
* @return array<string,mixed>
*/
private function fetch_json() {
$response = wp_remote_get(
$this->json_url,
array(
'timeout' => 10,
'headers' => array(
'Accept' => 'application/json',
),
)
);
if ( is_wp_error( $response ) ) {
return array();
}
$code = (int) wp_remote_retrieve_response_code( $response );
if ( $code < 200 || $code >= 300 ) {
return array();
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
return is_array( $data ) ? $data : array();
}
/**
* Checks whether the remote version is compatible with the current environment.
*
* @since 1.0.0
*
* @param array<string,mixed> $remote Remote update data.
*
* @return bool True if compatible.
*/
private function is_compatible( array $remote ) {
if ( isset( $remote['requires_php'] ) && is_string( $remote['requires_php'] ) && '' !== $remote['requires_php'] ) {
if ( version_compare( PHP_VERSION, $remote['requires_php'], '<' ) ) {
return false;
}
}
if ( isset( $remote['requires'] ) && is_string( $remote['requires'] ) && '' !== $remote['requires'] ) {
if ( version_compare( (string) get_bloginfo( 'version' ), $remote['requires'], '<' ) ) {
return false;
}
}
return true;
}
/**
* Handles a manual cache-clear request triggered via a URL parameter.
*
* Validates nonce and capability before clearing.
*
* @since 1.0.0
*
* @return void
*/
public function handle_cache_clear() {
$clear_cache = filter_input( INPUT_GET, 'robotstxt_clear_update_cache', FILTER_UNSAFE_RAW );
if ( null === $clear_cache ) {
return;
}
$nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_UNSAFE_RAW );
$nonce = is_string( $nonce_raw ) ? sanitize_text_field( wp_unslash( $nonce_raw ) ) : '';
if ( ! wp_verify_nonce( $nonce, 'robotstxt_clear_update_cache' ) ) {
wp_die( esc_html__( 'Security check failed.', 'robotstxt-ai-translator' ) );
}
if ( ! current_user_can( 'update_plugins' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-ai-translator' ) );
}
$this->clear_cache();
wp_safe_redirect( remove_query_arg( array( 'robotstxt_clear_update_cache', '_wpnonce' ) ) );
exit;
}
/**
* Deletes the cached remote data for this plugin.
*
* @since 1.0.0
*
* @return void
*/
public function clear_cache() {
delete_site_transient( $this->cache_key );
delete_site_transient( 'update_plugins' );
}
}
}

View file

@ -221,16 +221,22 @@ if ( ! class_exists( 'AI_Translator_Admin' ) ) {
<label for="ai-translator-translate-content">
<input type="checkbox" id="ai-translator-translate-content" name="ai_translator_settings[translate_content]" value="1" <?php checked( $current['translate_content'] ); ?> />
<?php echo esc_html__( 'Translate content', 'robotstxt-ai-translator' ); ?>
</label><br />
<label for="ai-translator-translate-excerpt">
<input type="checkbox" id="ai-translator-translate-excerpt" name="ai_translator_settings[translate_excerpt]" value="1" <?php checked( $current['translate_excerpt'] ); ?> />
<?php echo esc_html__( 'Translate excerpt', 'robotstxt-ai-translator' ); ?>
</label>
<?php if ( is_multisite() ) : ?>
<p class="description">
<?php
printf(
/* translators: 1: title default, 2: content default. */
esc_html__( 'Network defaults: title %1$s, content %2$s.', 'robotstxt-ai-translator' ),
/* translators: 1: title default, 2: content default, 3: excerpt default. */
esc_html__( 'Network defaults: title %1$s, content %2$s, excerpt %3$s.', 'robotstxt-ai-translator' ),
isset( $defaults['translate_title'] ) && $defaults['translate_title'] ? esc_html__( 'enabled', 'robotstxt-ai-translator' ) : esc_html__( 'disabled', 'robotstxt-ai-translator' ),
isset( $defaults['translate_content'] ) && $defaults['translate_content'] ? esc_html__( 'enabled', 'robotstxt-ai-translator' ) : esc_html__( 'disabled', 'robotstxt-ai-translator' )
isset( $defaults['translate_content'] ) && $defaults['translate_content'] ? esc_html__( 'enabled', 'robotstxt-ai-translator' ) : esc_html__( 'disabled', 'robotstxt-ai-translator' ),
isset( $defaults['translate_excerpt'] ) && $defaults['translate_excerpt'] ? esc_html__( 'enabled', 'robotstxt-ai-translator' ) : esc_html__( 'disabled', 'robotstxt-ai-translator' )
);
?>
</p>
@ -238,6 +244,26 @@ if ( ! class_exists( 'AI_Translator_Admin' ) ) {
</fieldset>
</td>
</tr>
<?php if ( class_exists( '\Inpsyde\MultilingualPress\TranslationUi\Post\MetaboxAction' ) ) : ?>
<tr>
<th scope="row"><?php echo esc_html__( 'MultilingualPress', 'robotstxt-ai-translator' ); ?></th>
<td>
<fieldset>
<legend class="screen-reader-text"><?php echo esc_html__( 'MultilingualPress', 'robotstxt-ai-translator' ); ?></legend>
<label for="ai-translator-auto-translate-mlp">
<input type="checkbox" id="ai-translator-auto-translate-mlp" name="ai_translator_settings[auto_translate_on_mlp_create]" value="1" <?php checked( $current['auto_translate_on_mlp_create'] ); ?> />
<?php echo esc_html__( 'Auto-translate new connected posts', 'robotstxt-ai-translator' ); ?>
</label>
<p class="description">
<?php echo esc_html__( 'When MultilingualPress creates a new connected post, automatically translate its enabled fields into the target site\'s language. The translation runs synchronously during save.', 'robotstxt-ai-translator' ); ?>
</p>
</fieldset>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
@ -315,6 +341,11 @@ if ( ! class_exists( 'AI_Translator_Admin' ) ) {
<label for="ai-translator-net-translate-content">
<input type="checkbox" id="ai-translator-net-translate-content" name="ai_translator_settings[translate_content]" value="1" <?php checked( $network['translate_content'] ); ?> />
<?php echo esc_html__( 'Translate content', 'robotstxt-ai-translator' ); ?>
</label><br />
<label for="ai-translator-net-translate-excerpt">
<input type="checkbox" id="ai-translator-net-translate-excerpt" name="ai_translator_settings[translate_excerpt]" value="1" <?php checked( $network['translate_excerpt'] ); ?> />
<?php echo esc_html__( 'Translate excerpt', 'robotstxt-ai-translator' ); ?>
</label>
<p class="description">
@ -323,6 +354,26 @@ if ( ! class_exists( 'AI_Translator_Admin' ) ) {
</fieldset>
</td>
</tr>
<?php if ( class_exists( '\Inpsyde\MultilingualPress\TranslationUi\Post\MetaboxAction' ) ) : ?>
<tr>
<th scope="row"><?php echo esc_html__( 'MultilingualPress', 'robotstxt-ai-translator' ); ?></th>
<td>
<fieldset>
<legend class="screen-reader-text"><?php echo esc_html__( 'MultilingualPress', 'robotstxt-ai-translator' ); ?></legend>
<label for="ai-translator-net-auto-translate-mlp">
<input type="checkbox" id="ai-translator-net-auto-translate-mlp" name="ai_translator_settings[auto_translate_on_mlp_create]" value="1" <?php checked( $network['auto_translate_on_mlp_create'] ); ?> />
<?php echo esc_html__( 'Auto-translate new connected posts', 'robotstxt-ai-translator' ); ?>
</label>
<p class="description">
<?php echo esc_html__( 'When MultilingualPress creates a new connected post, automatically translate its enabled fields into the target site\'s language. The translation runs synchronously during save.', 'robotstxt-ai-translator' ); ?>
</p>
</fieldset>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
@ -514,6 +565,11 @@ if ( ! class_exists( 'AI_Translator_Admin' ) ) {
/**
* Sanitises a raw settings array into a strict boolean schema.
*
* This is the single sanitisation boundary for all form input from both the
* site and network settings pages. Raw POST values arrive via wp_unslash()
* and are converted here to strict booleans. Any new field added to this
* schema MUST be sanitised in this method before being stored.
*
* @since 1.0.0
*
* @param array<string,mixed> $raw Raw input from the form.
@ -522,8 +578,10 @@ if ( ! class_exists( 'AI_Translator_Admin' ) ) {
*/
private function sanitize_settings( array $raw ) {
return array(
'translate_title' => ! empty( $raw['translate_title'] ),
'translate_content' => ! empty( $raw['translate_content'] ),
'translate_title' => ! empty( $raw['translate_title'] ),
'translate_content' => ! empty( $raw['translate_content'] ),
'translate_excerpt' => ! empty( $raw['translate_excerpt'] ),
'auto_translate_on_mlp_create' => ! empty( $raw['auto_translate_on_mlp_create'] ),
);
}
}

View file

@ -124,19 +124,8 @@ if ( ! class_exists( 'AI_Translator_CLI' ) ) {
WP_CLI::error( __( 'The --post-id and --post-type flags are mutually exclusive.', 'robotstxt-ai-translator' ) );
}
$current_user = wp_get_current_user();
if ( ! $current_user || 0 === (int) $current_user->ID ) {
WP_CLI::warning( __( 'No user context. Pass --user=<id|login> so post updates carry an author and capability checks apply.', 'robotstxt-ai-translator' ) );
}
if ( ! $dry_run && ! $this->translator->is_available() ) {
WP_CLI::error( __( 'The WordPress AI plugin is not active.', 'robotstxt-ai-translator' ) );
}
if ( ! $dry_run && ! $this->translator->is_supported() ) {
WP_CLI::error( __( 'The WordPress AI plugin has no provider configured for text generation.', 'robotstxt-ai-translator' ) );
}
// Validate locale before AI checks so the user sees the right error when
// both the locale is missing and the AI provider is unconfigured.
if ( ! in_array( $locale, $this->translator->get_installed_locales(), true ) ) {
WP_CLI::error(
sprintf(
@ -147,9 +136,22 @@ if ( ! class_exists( 'AI_Translator_CLI' ) ) {
);
}
$current_user = wp_get_current_user();
if ( ! $current_user || 0 === (int) $current_user->ID ) {
WP_CLI::warning( __( 'No user context. Pass --user=<id|login> so post updates carry an author and capability checks apply.', 'robotstxt-ai-translator' ) );
}
if ( ! $dry_run && ! $this->translator->is_available() ) {
WP_CLI::error( __( 'AI features are disabled for this WordPress installation.', 'robotstxt-ai-translator' ) );
}
if ( ! $dry_run && ! $this->translator->is_supported() ) {
WP_CLI::error( __( 'No AI provider is configured for text generation. Open Settings → AI to set one up.', 'robotstxt-ai-translator' ) );
}
$settings = $this->settings->get_settings();
if ( empty( $settings['translate_title'] ) && empty( $settings['translate_content'] ) ) {
if ( empty( $settings['translate_title'] ) && empty( $settings['translate_content'] ) && empty( $settings['translate_excerpt'] ) ) {
WP_CLI::error( __( 'No translation fields are enabled in the plugin settings.', 'robotstxt-ai-translator' ) );
}
@ -212,6 +214,16 @@ if ( ! class_exists( 'AI_Translator_CLI' ) ) {
$update['post_content'] = $translated_content;
}
if ( ! empty( $settings['translate_excerpt'] ) && post_type_supports( $post->post_type, 'excerpt' ) ) {
$translated_excerpt = $this->translator->translate( $post->post_excerpt, $locale );
if ( is_wp_error( $translated_excerpt ) ) {
WP_CLI::warning( sprintf( '#%d excerpt: %s', $post->ID, $translated_excerpt->get_error_message() ) );
++$failures;
continue;
}
$update['post_excerpt'] = $translated_excerpt;
}
$result = wp_update_post( $update, true );
if ( is_wp_error( $result ) ) {

View file

@ -28,7 +28,7 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) {
* @since 1.0.0
* @var array<int,string>
*/
const ALLOWED_FIELDS = array( 'title', 'content' );
const ALLOWED_FIELDS = array( 'title', 'content', 'excerpt' );
/**
* Settings handler.
@ -210,6 +210,10 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) {
if ( in_array( 'content', $fields, true ) && ! empty( $site_settings['translate_content'] ) ) {
$allowed[] = 'content';
}
if ( in_array( 'excerpt', $fields, true ) && ! empty( $site_settings['translate_excerpt'] )
&& post_type_supports( $post->post_type, 'excerpt' ) ) {
$allowed[] = 'excerpt';
}
if ( empty( $allowed ) ) {
return new WP_Error(
@ -237,6 +241,14 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) {
$response['content'] = $translated;
}
if ( in_array( 'excerpt', $allowed, true ) ) {
$translated = $this->translator->translate( $post->post_excerpt, $target_locale );
if ( is_wp_error( $translated ) ) {
return $this->error_with_status( $translated, 502 );
}
$response['excerpt'] = $translated;
}
return new WP_REST_Response( $response, 200 );
}
@ -304,8 +316,8 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) {
?>
<div id="ai-translator-classic" class="ai-translator-classic" data-post-id="<?php echo esc_attr( (string) $post->ID ); ?>">
<?php if ( ! $this->translator->is_available() ) : ?>
<p><?php echo esc_html__( 'The WordPress AI plugin is not active. Translation is unavailable.', 'robotstxt-ai-translator' ); ?></p>
<?php elseif ( empty( $settings['translate_title'] ) && empty( $settings['translate_content'] ) ) : ?>
<p><?php echo esc_html__( 'AI features are disabled for this WordPress installation.', 'robotstxt-ai-translator' ); ?></p>
<?php elseif ( empty( $settings['translate_title'] ) && empty( $settings['translate_content'] ) && empty( $settings['translate_excerpt'] ) ) : ?>
<p><?php echo esc_html__( 'No translation fields are enabled in settings.', 'robotstxt-ai-translator' ); ?></p>
<?php elseif ( empty( $languages ) ) : ?>
<p><?php echo esc_html__( 'No languages are installed on this site.', 'robotstxt-ai-translator' ); ?></p>
@ -425,6 +437,7 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) {
'settings' => array(
'translateTitle' => ! empty( $settings['translate_title'] ),
'translateContent' => ! empty( $settings['translate_content'] ),
'translateExcerpt' => ! empty( $settings['translate_excerpt'] ),
),
'i18n' => array(
'translate' => __( 'Translate', 'robotstxt-ai-translator' ),
@ -435,10 +448,11 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) {
'genericError' => __( 'Translation failed.', 'robotstxt-ai-translator' ),
'noLanguages' => __( 'No languages are installed on this site.', 'robotstxt-ai-translator' ),
'noFields' => __( 'No translation fields are enabled in settings.', 'robotstxt-ai-translator' ),
'aiUnavailable' => __( 'The WordPress AI plugin is not active.', 'robotstxt-ai-translator' ),
'aiUnavailable' => __( 'AI features are disabled for this WordPress installation.', 'robotstxt-ai-translator' ),
'saveBeforeWarn' => __( 'You have unsaved changes. Save the post before translating to ensure the latest content is used.', 'robotstxt-ai-translator' ),
'translateTitle' => __( 'Translate title', 'robotstxt-ai-translator' ),
'translateContent' => __( 'Translate content', 'robotstxt-ai-translator' ),
'translateExcerpt' => __( 'Translate excerpt', 'robotstxt-ai-translator' ),
),
);
@ -466,9 +480,9 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) {
return (int) $post->ID;
}
if ( isset( $_GET['post'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$post_get = wp_unslash( $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return is_scalar( $post_get ) ? absint( $post_get ) : 0;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['post'] ) && is_scalar( $_GET['post'] ) ) {
return absint( (string) $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
return 0;

View file

@ -0,0 +1,174 @@
<?php
/**
* MultilingualPress integration for AI Translator.
*
* When MultilingualPress creates a new connected post (i.e. the editor saves
* a source post and MLP syncs it to another site for the first time), this
* class translates the source post's fields into the target site's language
* and writes the result back to the newly created remote post.
*
* The hook `multilingualpress.metabox_after_update_remote_post` fires while
* WordPress is already switched to the remote site, so wp_update_post() can
* be called directly without an additional switch_to_blog().
*
* This integration only activates when:
* 1. MultilingualPress is active (class_exists guard in bootstrap).
* 2. "Auto-translate on MLP sync" is enabled in the plugin settings.
* 3. An AI provider is configured for text generation.
*
* @package ROBOTSTXT\AI_Translator
* @since 1.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'AI_Translator_MLP_Integration' ) ) {
/**
* Hooks into MultilingualPress post synchronization to auto-translate
* newly connected posts.
*
* @since 1.1.0
*/
class AI_Translator_MLP_Integration {
/**
* Settings handler.
*
* @since 1.1.0
* @var AI_Translator_Settings
*/
private $settings;
/**
* Translator handler.
*
* @since 1.1.0
* @var AI_Translator_Translator
*/
private $translator;
/**
* Constructor.
*
* @since 1.1.0
*
* @param AI_Translator_Settings $settings Settings handler.
* @param AI_Translator_Translator $translator Translator handler.
*/
public function __construct( AI_Translator_Settings $settings, AI_Translator_Translator $translator ) {
$this->settings = $settings;
$this->translator = $translator;
}
/**
* Registers WordPress hooks.
*
* @since 1.1.0
*
* @return void
*/
public function register() {
add_action(
'multilingualpress.metabox_after_update_remote_post',
array( $this, 'on_mlp_after_update_remote_post' ),
10,
3
);
}
/**
* Translates the source post and updates the newly created remote post.
*
* Fires while WordPress is switched to the remote site, so
* wp_update_post() can be called directly.
*
* Only acts when:
* - $operation is 'new' (first connection, not an update).
* - The "auto_translate_on_mlp_create" setting is enabled.
* - An AI provider is configured for text generation.
*
* @since 1.1.0
*
* @param \Inpsyde\MultilingualPress\TranslationUi\Post\RelationshipContext $context MLP relationship context.
* @param array<string,scalar|null> $post Post data used to create the remote post.
* @param string $operation 'new' = first connection, 'leave' = update.
*
* @return void
*/
public function on_mlp_after_update_remote_post( $context, $post, $operation ) {
unset( $post );
if ( 'new' !== $operation ) {
return;
}
$settings = $this->settings->get_settings();
if ( empty( $settings['auto_translate_on_mlp_create'] ) ) {
return;
}
if ( ! $this->translator->is_supported() ) {
return;
}
// We are already on the remote site (MLP switched before firing this hook).
// get_blog_option() with the explicit site ID is safe and avoids relying
// on locale globals that may not be updated after switch_to_blog().
$raw_locale = get_blog_option( $context->remoteSiteId(), 'WPLANG' );
$target_locale = is_string( $raw_locale ) && '' !== $raw_locale ? $raw_locale : 'en_US';
// sourcePost() performs switch_to_blog internally and restores afterward.
$source_post = $context->sourcePost();
if ( ! $source_post instanceof WP_Post ) {
return;
}
$remote_post_id = $context->remotePostId();
$update = array( 'ID' => $remote_post_id );
$has_changes = false;
// ── Translate title ──────────────────────────────────────────────
if ( ! empty( $settings['translate_title'] ) && '' !== trim( $source_post->post_title ) ) {
$translated = $this->translator->translate( $source_post->post_title, $target_locale );
if ( ! is_wp_error( $translated ) && '' !== $translated ) {
$update['post_title'] = $translated;
$has_changes = true;
}
}
// ── Translate content ────────────────────────────────────────────
if ( ! empty( $settings['translate_content'] ) && '' !== trim( $source_post->post_content ) ) {
$translated = $this->translator->translate( $source_post->post_content, $target_locale );
if ( ! is_wp_error( $translated ) && '' !== $translated ) {
$update['post_content'] = $translated;
$has_changes = true;
}
}
// ── Translate excerpt ────────────────────────────────────────────
if ( ! empty( $settings['translate_excerpt'] )
&& '' !== trim( $source_post->post_excerpt )
&& post_type_supports( $source_post->post_type, 'excerpt' )
) {
$translated = $this->translator->translate( $source_post->post_excerpt, $target_locale );
if ( ! is_wp_error( $translated ) && '' !== $translated ) {
$update['post_excerpt'] = $translated;
$has_changes = true;
}
}
if ( $has_changes ) {
// Already on the remote site — no switch_to_blog() needed.
wp_update_post( wp_slash( $update ) );
}
}
}
}

View file

@ -55,8 +55,10 @@ if ( ! class_exists( 'AI_Translator_Settings' ) ) {
* @var array<string,bool>
*/
const DEFAULTS = array(
'translate_title' => true,
'translate_content' => true,
'translate_title' => true,
'translate_content' => true,
'translate_excerpt' => true,
'auto_translate_on_mlp_create' => false,
);
/**

View file

@ -23,14 +23,18 @@ if ( ! class_exists( 'AI_Translator_Translator' ) ) {
class AI_Translator_Translator {
/**
* Indicates whether the WordPress AI client is available.
* Indicates whether AI features are enabled for this WordPress installation.
*
* `wp_ai_client_prompt()` is a native WordPress 7.0 function the API is always
* present. This method checks `wp_supports_ai()`, which returns false only when the
* `WP_AI_SUPPORT` constant is set to false or the `wp_supports_ai` filter disables it.
*
* @since 1.0.0
*
* @return bool True if the dependency is active.
* @return bool True when WordPress AI is enabled for this installation.
*/
public function is_available() {
return function_exists( 'wp_ai_client_prompt' );
return wp_supports_ai();
}
/**
@ -78,15 +82,15 @@ if ( ! class_exists( 'AI_Translator_Translator' ) ) {
if ( ! $this->is_available() ) {
return new WP_Error(
'ai_translator_unavailable',
__( 'The WordPress AI client is not available. Install and activate the WordPress AI plugin.', 'robotstxt-ai-translator' )
'ai_translator_disabled',
__( 'AI features are disabled for this WordPress installation.', 'robotstxt-ai-translator' )
);
}
if ( ! $this->is_supported() ) {
return new WP_Error(
'ai_translator_unsupported',
__( 'The configured AI providers cannot serve text generation requests. Open the WordPress AI settings to configure a provider.', 'robotstxt-ai-translator' )
__( 'No AI provider is configured for text generation. Open Settings → AI to set one up.', 'robotstxt-ai-translator' )
);
}

File diff suppressed because one or more lines are too long

View file

@ -1,279 +1,409 @@
# Copyright (C) 2026 ROBOTSTXT
# This file is distributed under the GPL-3.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: AI Translator (by ROBOTSTXT) 1.0.0\n"
"Project-Id-Version: AI Translator (by ROBOTSTXT) 1.1.0\n"
"Report-Msgid-Bugs-To: https://git.robotstxt.es/ROBOTSTXT/robotstxt-ai-translator\n"
"Last-Translator: ROBOTSTXT <hello@robotstxt.es>\n"
"Language-Team: Catalan\n"
"Language: ca\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2026-05-23T10:07:42+00:00\n"
"PO-Revision-Date: 2026-05-23 10:30+0000\n"
"Language: ca\n"
"X-Generator: hand-written\n"
"X-Domain: robotstxt-ai-translator\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. Plugin Name of the plugin
#: robotstxt-ai-translator.php
msgid "AI Translator (by ROBOTSTXT)"
msgstr "AI Translator (by ROBOTSTXT)"
#. Description of the plugin
msgid "Translate post titles and content from the editor using the native WordPress AI plugin as backend. Multisite-ready with global or per-site configuration."
msgstr "Tradueix el títol i el contingut de les entrades des de l'editor utilitzant el connector natiu d'IA de WordPress com a backend. Compatible amb Multisite en mode global o per lloc."
#. Author of the plugin
#: robotstxt-ai-translator.php
msgid "ROBOTSTXT"
msgstr "ROBOTSTXT"
#: includes/class-ai-translator-admin.php:112
#: includes/class-ai-translator-admin.php:113
#: includes/class-ai-translator-admin.php:130
#: includes/class-ai-translator-admin.php:131
#: includes/class-ai-translator-editor-ui.php:294
#: includes/class-ai-translator-editor-ui.php:446
msgid "AI Translator"
msgstr "AI Translator"
#: includes/class-ai-translator-admin.php:153
#: includes/class-ai-translator-admin.php:171
msgid "Settings"
msgstr "Ajustaments"
#: includes/class-ai-translator-admin.php:187
#: includes/class-ai-translator-admin.php:287
msgid "You do not have permission to access this page."
msgstr "No tens permís per accedir a aquesta pàgina."
#: includes/class-ai-translator-admin.php:196
msgid "AI Translator Settings"
msgstr "Ajustaments d'AI Translator"
#: includes/class-ai-translator-admin.php:200
msgid "Settings saved."
msgstr "Ajustaments desats."
#: includes/class-ai-translator-admin.php:211
#: includes/class-ai-translator-admin.php:214
#: includes/class-ai-translator-admin.php:330
#: includes/class-ai-translator-admin.php:334
msgid "Translation fields"
msgstr "Camps a traduir"
#: includes/class-ai-translator-admin.php:218
#: includes/class-ai-translator-admin.php:338
#: includes/class-ai-translator-editor-ui.php:453
msgid "Translate title"
msgstr "Tradueix el títol"
#: includes/class-ai-translator-admin.php:223
#: includes/class-ai-translator-admin.php:343
#: includes/class-ai-translator-editor-ui.php:454
msgid "Translate content"
msgstr "Tradueix el contingut"
#. translators: 1: title default, 2: content default.
#, php-format
msgid "Network defaults: title %1$s, content %2$s."
msgstr "Valors predeterminats de la xarxa: títol %1$s, contingut %2$s."
#: includes/class-ai-translator-admin.php:237
#: includes/class-ai-translator-admin.php:238
#: includes/class-ai-translator-admin.php:239
msgid "enabled"
msgstr "activat"
#: includes/class-ai-translator-admin.php:237
#: includes/class-ai-translator-admin.php:238
#: includes/class-ai-translator-admin.php:239
msgid "disabled"
msgstr "desactivat"
#: includes/class-ai-translator-admin.php:296
msgid "AI Translator Network Settings"
msgstr "Ajustaments de xarxa d'AI Translator"
#: includes/class-ai-translator-admin.php:300
msgid "Network settings saved."
msgstr "Ajustaments de xarxa desats."
#: includes/class-ai-translator-admin.php:310
#: includes/class-ai-translator-admin.php:313
msgid "Configuration mode"
msgstr "Mode de configuració"
#: includes/class-ai-translator-admin.php:317
msgid "Global configuration: a single setting applies to every site."
msgstr "Configuració global: un sol ajustament s'aplica a tots els llocs."
#: includes/class-ai-translator-admin.php:322
msgid "Per-site configuration: each site can override these defaults."
msgstr "Configuració per lloc: cada lloc pot sobreescriure aquests valors predeterminats."
#: includes/class-ai-translator-admin.php:352
msgid "In global mode this is the configuration for the whole network. In per-site mode these values are used as defaults for sites that have not been configured individually."
msgstr "En mode global aquesta és la configuració per a tota la xarxa. En mode per lloc aquests valors s'utilitzen com a predeterminats per als llocs que no s'hagin configurat individualment."
#: includes/class-ai-translator-admin.php:397
#: includes/class-ai-translator-admin.php:429
msgid "You do not have permission to perform this action."
msgstr "No tens permís per dur a terme aquesta acció."
#: includes/class-ai-translator-admin.php:473
msgid "Recommended models"
msgstr "Models recomanats"
#: includes/class-ai-translator-admin.php:476
msgid "This plugin does not call AI providers directly. Configure your preferred provider in the WordPress AI plugin settings. The table below is a guide to help you pick a model based on your translation use case."
msgstr "Aquest connector no truca directament als proveïdors d'IA. Configura el teu proveïdor preferit als ajustaments del connector WordPress AI. La taula següent és una guia per ajudar-te a triar un model segons el teu cas d'ús de traducció."
#: includes/class-ai-translator-admin.php:482
msgid "Use case"
msgstr "Cas d'ús"
#: includes/class-ai-translator-admin.php:483
msgid "Recommended model"
msgstr "Model recomanat"
#: includes/class-ai-translator-admin.php:484
msgid "Why"
msgstr "Per què"
#: includes/class-ai-translator-admin.php:499
msgid "These recommendations are based on public benchmarks and community feedback as of the plugin release date. They may change as providers update their models."
msgstr "Aquestes recomanacions es basen en benchmarks públics i comentaris de la comunitat en la data de publicació del connector. Poden canviar a mesura que els proveïdors actualitzin els seus models."
#: includes/class-ai-translator-admin.php:514
msgid "European languages (ES, CA, FR, DE, IT, PT)"
msgstr "Llengües europees (ES, CA, FR, DE, IT, PT)"
#: includes/class-ai-translator-admin.php:515
msgid "DeepL API Pro"
msgstr "DeepL API Pro"
#: includes/class-ai-translator-admin.php:516
msgid "Best fluency and naturalness (92/100 in benchmarks). Formality control and custom glossaries."
msgstr "La millor fluïdesa i naturalitat (92/100 en benchmarks). Control de formalitat i glossaris personalitzats."
#: includes/class-ai-translator-admin.php:519
msgid "Asian languages (ZH, JA, KO)"
msgstr "Llengües asiàtiques (ZH, JA, KO)"
#: includes/class-ai-translator-admin.php:520
msgid "GPT-4o / GPT-5 or Claude Sonnet 4"
msgstr "GPT-4o / GPT-5 o Claude Sonnet 4"
#: includes/class-ai-translator-admin.php:521
msgid "Better handling of implicit subjects, honorifics, and cultural references. DeepL falls behind here."
msgstr "Millor gestió dels subjectes implícits, els honorífics i les referències culturals. DeepL queda enrere aquí."
#: includes/class-ai-translator-admin.php:524
msgid "Marketing / brand tone"
msgstr "Màrqueting / to de marca"
#: includes/class-ai-translator-admin.php:525
msgid "Claude Sonnet 4 / Opus 4"
msgstr "Claude Sonnet 4 / Opus 4"
#: includes/class-ai-translator-admin.php:526
msgid "Better preservation of tone, brand voice, and nuance. Ideal for creative copy."
msgstr "Preserva millor el to, la veu de la marca i els matisos. Ideal per a copy creatiu."
#: includes/class-ai-translator-admin.php:529
msgid "Technical documentation / code"
msgstr "Documentació tècnica / codi"
#: includes/class-ai-translator-admin.php:530
msgid "GPT-4o / GPT-5"
msgstr "GPT-4o / GPT-5"
#: includes/class-ai-translator-admin.php:531
msgid "Higher accuracy with variables, structured formats, and technical terminology."
msgstr "Major precisió amb variables, formats estructurats i terminologia tècnica."
#: includes/class-ai-translator-admin.php:534
msgid "Long documents (100+ pages)"
msgstr "Documents llargs (més de 100 pàgines)"
#: includes/class-ai-translator-admin.php:535
msgid "Gemini 2.5 Pro"
msgstr "Gemini 2.5 Pro"
#: includes/class-ai-translator-admin.php:536
msgid "1M token context window. Maintains terminological consistency across long texts."
msgstr "Finestra de context d'1 milió de tokens. Manté la consistència terminològica en textos llargs."
#: includes/class-ai-translator-admin.php:539
msgid "High volume / low cost"
msgstr "Alt volum / baix cost"
#: includes/class-ai-translator-admin.php:540
msgid "DeepSeek-V3"
msgstr "DeepSeek-V3"
#: includes/class-ai-translator-admin.php:541
msgid "Quality comparable to GPT-5 at ~$0.14/M tokens (20-50x cheaper than Claude/GPT)."
msgstr "Qualitat comparable a GPT-5 a ~0,14 $/M tokens (entre 20 i 50 vegades més barat que Claude/GPT)."
#: includes/class-ai-translator-admin.php:544
msgid "Rare / indigenous languages"
msgstr "Llengües rares / indígenes"
#: includes/class-ai-translator-admin.php:545
msgid "Claude Sonnet or Taskade Translate (multi-model routing)"
msgstr "Claude Sonnet o Taskade Translate (encaminament multimodel)"
#: includes/class-ai-translator-admin.php:546
msgid "Better coverage for uncommon language pairs."
msgstr "Millor cobertura en parells de llengües poc habituals."
#: includes/class-ai-translator-admin.php:549
msgid "Maximum coverage (133+ languages)"
msgstr "Cobertura màxima (més de 133 llengües)"
#: includes/class-ai-translator-admin.php:550
msgid "Google Cloud Translation"
msgstr "Google Cloud Translation"
#: includes/class-ai-translator-admin.php:551
msgid "The most complete option, though with slightly lower quality on European languages."
msgstr "L'opció més completa, tot i que amb una qualitat lleugerament inferior en llengües europees."
#: includes/class-ai-translator-cli.php:120
msgid "The --locale flag is required."
msgstr "El paràmetre --locale és obligatori."
#: includes/class-ai-translator-cli.php:124
msgid "The --post-id and --post-type flags are mutually exclusive."
msgstr "Els paràmetres --post-id i --post-type són mútuament excloents."
#: includes/class-ai-translator-cli.php:141
msgid "No user context. Pass --user=<id|login> so post updates carry an author and capability checks apply."
msgstr "Sense context d'usuari. Passa --user=<id|login> perquè les actualitzacions d'entrades tinguin autor i s'apliquin les comprovacions de capacitats."
msgid "The WordPress AI plugin is not active."
msgstr "El connector WordPress AI no està actiu."
msgid "The WordPress AI plugin has no provider configured for text generation."
msgstr "El connector WordPress AI no té cap proveïdor configurat per a la generació de text."
#. translators: %s: locale code.
#: includes/class-ai-translator-cli.php:133
#, php-format
msgid "The locale %s is not installed on this site."
msgstr "La llengua %s no està instal·lada en aquest lloc."
#: includes/class-ai-translator-cli.php:155
msgid "No translation fields are enabled in the plugin settings."
msgstr "No hi ha cap camp de traducció activat als ajustaments del connector."
#: includes/class-ai-translator-cli.php:181
msgid "No posts matched the given criteria."
msgstr "Cap entrada coincideix amb els criteris donats."
#. translators: 1: success count, 2: failure count, 3: locale.
#: includes/class-ai-translator-cli.php:242
#, php-format
msgid "%1$d translated, %2$d failed (locale: %3$s)."
msgstr "%1$d traduïdes, %2$d amb errors (llengua: %3$s)."
#: includes/class-ai-translator-editor-ui.php:160
msgid "Sorry, you are not allowed to edit this post."
msgstr "Ho sentim, no tens permís per editar aquesta entrada."
#: includes/class-ai-translator-editor-ui.php:190
msgid "Post not found."
msgstr "Entrada no trobada."
#: includes/class-ai-translator-editor-ui.php:199
msgid "The selected language is not installed on this site."
msgstr "La llengua seleccionada no està instal·lada en aquest lloc."
#: includes/class-ai-translator-editor-ui.php:221
msgid "No translatable fields were selected, or the requested fields are disabled in the settings."
msgstr "No s'ha seleccionat cap camp traduïble, o els camps sol·licitats estan desactivats als ajustaments."
msgid "The WordPress AI plugin is not active. Translation is unavailable."
msgstr "El connector WordPress AI no està actiu. La traducció no està disponible."
#: includes/class-ai-translator-editor-ui.php:321
#: includes/class-ai-translator-editor-ui.php:450
msgid "No translation fields are enabled in settings."
msgstr "No hi ha cap camp de traducció activat als ajustaments."
#: includes/class-ai-translator-editor-ui.php:323
#: includes/class-ai-translator-editor-ui.php:449
msgid "No languages are installed on this site."
msgstr "No hi ha cap llengua instal·lada en aquest lloc."
#: includes/class-ai-translator-editor-ui.php:326
#: includes/class-ai-translator-editor-ui.php:445
msgid "Target language"
msgstr "Llengua de destinació"
#: includes/class-ai-translator-editor-ui.php:336
#: includes/class-ai-translator-editor-ui.php:443
msgid "Translate"
msgstr "Tradueix"
#: includes/class-ai-translator-editor-ui.php:341
msgid "Save the post before translating to ensure the latest content is used. The translation replaces the current title and/or content."
msgstr "Desa l'entrada abans de traduir perquè s'utilitzi la versió més recent. La traducció substitueix el títol i/o el contingut actuals."
#: includes/class-ai-translator-editor-ui.php:444
msgid "Translating…"
msgstr "Traduint…"
#: includes/class-ai-translator-editor-ui.php:447
msgid "Translation applied. Review and save the post."
msgstr "Traducció aplicada. Revisa i desa l'entrada."
#: includes/class-ai-translator-editor-ui.php:448
msgid "Translation failed."
msgstr "La traducció ha fallat."
#: includes/class-ai-translator-editor-ui.php:452
msgid "You have unsaved changes. Save the post before translating to ensure the latest content is used."
msgstr "Tens canvis sense desar. Desa l'entrada abans de traduir perquè s'utilitzi la versió més recent."
msgid "The WordPress AI client is not available. Install and activate the WordPress AI plugin."
msgstr "El client de WordPress AI no està disponible. Instal·la i activa el connector WordPress AI."
msgid "The configured AI providers cannot serve text generation requests. Open the WordPress AI settings to configure a provider."
msgstr "Els proveïdors d'IA configurats no poden atendre peticions de generació de text. Obre els ajustaments de WordPress AI per configurar un proveïdor."
#: includes/class-ai-translator-translator.php:102
msgid "The target language is not installed on this site."
msgstr "La llengua de destinació no està instal·lada en aquest lloc."
#. translators: %s: target language name.
#: includes/class-ai-translator-translator.php:108
#, php-format
msgid "You are a professional translator. Translate the user message into %s. Preserve any HTML, shortcodes, line breaks, and Markdown exactly as they appear. Return only the translated text, without explanations, prefaces, or quotation marks."
msgstr "Ets un traductor professional. Tradueix el missatge de l'usuari al %s. Preserva l'HTML, els shortcodes, els salts de línia i el Markdown exactament com apareixen. Retorna només el text traduït, sense explicacions, prefacis ni cometes."
#: includes/class-ai-translator-translator.php:129
msgid "The AI service returned an unexpected response."
msgstr "El servei d'IA ha retornat una resposta inesperada."
#. translators: %s: WordPress AI plugin link.
#, php-format
msgid "AI Translator (by ROBOTSTXT) requires the %s plugin to be active. Translation features will be disabled until it is installed and configured."
msgstr "AI Translator (by ROBOTSTXT) necessita que el connector %s estigui actiu. Les funcions de traducció estaran desactivades fins que s'instal·li i es configuri."
msgid "WordPress AI"
msgstr "WordPress AI"
msgid "Security check failed"
msgstr "Comprovació de seguretat fallida"
#: class-robotstxt-updater.php:456
msgid "You do not have sufficient permissions to access this page."
msgstr "No tens permisos suficients per accedir a aquesta pàgina."
#. Plugin URI of the plugin
#: robotstxt-ai-translator.php
msgid "https://git.robotstxt.es/ROBOTSTXT/robotstxt-ai-translator"
msgstr ""
#. Description of the plugin
#: robotstxt-ai-translator.php
msgid "Translate post titles and content from the editor using the native WordPress AI client (available in WP 7.0+). Multisite-ready with global or per-site configuration."
msgstr ""
#. Author URI of the plugin
#: robotstxt-ai-translator.php
msgid "https://www.robotstxt.es/"
msgstr ""
#: class-robotstxt-updater.php:452
msgid "Security check failed."
msgstr "Error de verificació de seguretat."
#: includes/class-ai-translator-admin.php:228
#: includes/class-ai-translator-admin.php:348
#: includes/class-ai-translator-editor-ui.php:455
msgid "Translate excerpt"
msgstr "Traduir l'extracte"
#. translators: 1: title default, 2: content default, 3: excerpt default.
#: includes/class-ai-translator-admin.php:236
#, php-format
msgid "Network defaults: title %1$s, content %2$s, excerpt %3$s."
msgstr "Valors predeterminats de la xarxa: títol %1$s, contingut %2$s, extracte %3$s."
#: includes/class-ai-translator-admin.php:250
#: includes/class-ai-translator-admin.php:253
#: includes/class-ai-translator-admin.php:360
#: includes/class-ai-translator-admin.php:363
msgid "MultilingualPress"
msgstr "MultilingualPress"
#: includes/class-ai-translator-admin.php:257
#: includes/class-ai-translator-admin.php:367
msgid "Auto-translate new connected posts"
msgstr "Traduir automàticament les entrades connectades noves"
#: includes/class-ai-translator-admin.php:261
#: includes/class-ai-translator-admin.php:371
msgid "When MultilingualPress creates a new connected post, automatically translate its enabled fields into the target site's language. The translation runs synchronously during save."
msgstr "Quan MultilingualPress crea una entrada connectada nova, tradueix automàticament els camps activats a l'idioma del lloc de destinació. La traducció s'executa de manera síncrona en desar."
#: includes/class-ai-translator-cli.php:145
#: includes/class-ai-translator-editor-ui.php:319
#: includes/class-ai-translator-editor-ui.php:451
#: includes/class-ai-translator-translator.php:86
msgid "AI features are disabled for this WordPress installation."
msgstr "Les funcions d'IA estan desactivades per a aquesta instal·lació de WordPress."
#: includes/class-ai-translator-cli.php:149
#: includes/class-ai-translator-translator.php:93
msgid "No AI provider is configured for text generation. Open Settings → AI to set one up."
msgstr "No hi ha cap proveïdor d'IA configurat per a la generació de text. Obre Ajustos → IA per configurar-ne un."
#. translators: %s: Link to the WordPress AI settings page.
#: robotstxt-ai-translator.php:144
#, php-format
msgid "AI Translator (by ROBOTSTXT) requires a configured AI provider for text generation. Open %s to set one up."
msgstr "AI Translator (by ROBOTSTXT) requereix un proveïdor d'IA configurat per a la generació de text. Obre %s per configurar-ne un."
#: robotstxt-ai-translator.php:145
msgid "Settings → AI"
msgstr "Ajustos → IA"

File diff suppressed because one or more lines are too long

View file

@ -1,279 +1,409 @@
# Copyright (C) 2026 ROBOTSTXT
# This file is distributed under the GPL-3.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: AI Translator (by ROBOTSTXT) 1.0.0\n"
"Project-Id-Version: AI Translator (by ROBOTSTXT) 1.1.0\n"
"Report-Msgid-Bugs-To: https://git.robotstxt.es/ROBOTSTXT/robotstxt-ai-translator\n"
"Last-Translator: ROBOTSTXT <hello@robotstxt.es>\n"
"Language-Team: Spanish (Spain)\n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2026-05-23T10:07:42+00:00\n"
"PO-Revision-Date: 2026-05-23 10:30+0000\n"
"Language: es_ES\n"
"X-Generator: hand-written\n"
"X-Domain: robotstxt-ai-translator\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. Plugin Name of the plugin
#: robotstxt-ai-translator.php
msgid "AI Translator (by ROBOTSTXT)"
msgstr "AI Translator (by ROBOTSTXT)"
#. Description of the plugin
msgid "Translate post titles and content from the editor using the native WordPress AI plugin as backend. Multisite-ready with global or per-site configuration."
msgstr "Traduce el título y el contenido de las entradas desde el editor usando el plugin nativo de IA de WordPress como backend. Compatible con Multisitio en modo global o por sitio."
#. Author of the plugin
#: robotstxt-ai-translator.php
msgid "ROBOTSTXT"
msgstr "ROBOTSTXT"
#: includes/class-ai-translator-admin.php:112
#: includes/class-ai-translator-admin.php:113
#: includes/class-ai-translator-admin.php:130
#: includes/class-ai-translator-admin.php:131
#: includes/class-ai-translator-editor-ui.php:294
#: includes/class-ai-translator-editor-ui.php:446
msgid "AI Translator"
msgstr "AI Translator"
#: includes/class-ai-translator-admin.php:153
#: includes/class-ai-translator-admin.php:171
msgid "Settings"
msgstr "Ajustes"
#: includes/class-ai-translator-admin.php:187
#: includes/class-ai-translator-admin.php:287
msgid "You do not have permission to access this page."
msgstr "No tienes permiso para acceder a esta página."
#: includes/class-ai-translator-admin.php:196
msgid "AI Translator Settings"
msgstr "Ajustes de AI Translator"
#: includes/class-ai-translator-admin.php:200
msgid "Settings saved."
msgstr "Ajustes guardados."
#: includes/class-ai-translator-admin.php:211
#: includes/class-ai-translator-admin.php:214
#: includes/class-ai-translator-admin.php:330
#: includes/class-ai-translator-admin.php:334
msgid "Translation fields"
msgstr "Campos a traducir"
#: includes/class-ai-translator-admin.php:218
#: includes/class-ai-translator-admin.php:338
#: includes/class-ai-translator-editor-ui.php:453
msgid "Translate title"
msgstr "Traducir título"
#: includes/class-ai-translator-admin.php:223
#: includes/class-ai-translator-admin.php:343
#: includes/class-ai-translator-editor-ui.php:454
msgid "Translate content"
msgstr "Traducir contenido"
#. translators: 1: title default, 2: content default.
#, php-format
msgid "Network defaults: title %1$s, content %2$s."
msgstr "Valores por defecto de la red: título %1$s, contenido %2$s."
#: includes/class-ai-translator-admin.php:237
#: includes/class-ai-translator-admin.php:238
#: includes/class-ai-translator-admin.php:239
msgid "enabled"
msgstr "activado"
#: includes/class-ai-translator-admin.php:237
#: includes/class-ai-translator-admin.php:238
#: includes/class-ai-translator-admin.php:239
msgid "disabled"
msgstr "desactivado"
#: includes/class-ai-translator-admin.php:296
msgid "AI Translator Network Settings"
msgstr "Ajustes de red de AI Translator"
#: includes/class-ai-translator-admin.php:300
msgid "Network settings saved."
msgstr "Ajustes de red guardados."
#: includes/class-ai-translator-admin.php:310
#: includes/class-ai-translator-admin.php:313
msgid "Configuration mode"
msgstr "Modo de configuración"
#: includes/class-ai-translator-admin.php:317
msgid "Global configuration: a single setting applies to every site."
msgstr "Configuración global: un único ajuste se aplica a todos los sitios."
#: includes/class-ai-translator-admin.php:322
msgid "Per-site configuration: each site can override these defaults."
msgstr "Configuración por sitio: cada sitio puede sobrescribir estos valores por defecto."
#: includes/class-ai-translator-admin.php:352
msgid "In global mode this is the configuration for the whole network. In per-site mode these values are used as defaults for sites that have not been configured individually."
msgstr "En modo global esta es la configuración para toda la red. En modo por sitio estos valores se usan como predeterminados para los sitios que no se hayan configurado individualmente."
#: includes/class-ai-translator-admin.php:397
#: includes/class-ai-translator-admin.php:429
msgid "You do not have permission to perform this action."
msgstr "No tienes permiso para realizar esta acción."
#: includes/class-ai-translator-admin.php:473
msgid "Recommended models"
msgstr "Modelos recomendados"
#: includes/class-ai-translator-admin.php:476
msgid "This plugin does not call AI providers directly. Configure your preferred provider in the WordPress AI plugin settings. The table below is a guide to help you pick a model based on your translation use case."
msgstr "Este plugin no llama directamente a los proveedores de IA. Configura tu proveedor preferido en los ajustes del plugin WordPress AI. La tabla siguiente es una guía para ayudarte a elegir un modelo según tu caso de uso de traducción."
#: includes/class-ai-translator-admin.php:482
msgid "Use case"
msgstr "Caso de uso"
#: includes/class-ai-translator-admin.php:483
msgid "Recommended model"
msgstr "Modelo recomendado"
#: includes/class-ai-translator-admin.php:484
msgid "Why"
msgstr "Por qué"
#: includes/class-ai-translator-admin.php:499
msgid "These recommendations are based on public benchmarks and community feedback as of the plugin release date. They may change as providers update their models."
msgstr "Estas recomendaciones se basan en benchmarks públicos y comentarios de la comunidad en la fecha de publicación del plugin. Pueden cambiar a medida que los proveedores actualicen sus modelos."
#: includes/class-ai-translator-admin.php:514
msgid "European languages (ES, CA, FR, DE, IT, PT)"
msgstr "Idiomas europeos (ES, CA, FR, DE, IT, PT)"
#: includes/class-ai-translator-admin.php:515
msgid "DeepL API Pro"
msgstr "DeepL API Pro"
#: includes/class-ai-translator-admin.php:516
msgid "Best fluency and naturalness (92/100 in benchmarks). Formality control and custom glossaries."
msgstr "La mejor fluidez y naturalidad (92/100 en benchmarks). Control de formalidad y glosarios personalizados."
#: includes/class-ai-translator-admin.php:519
msgid "Asian languages (ZH, JA, KO)"
msgstr "Idiomas asiáticos (ZH, JA, KO)"
#: includes/class-ai-translator-admin.php:520
msgid "GPT-4o / GPT-5 or Claude Sonnet 4"
msgstr "GPT-4o / GPT-5 o Claude Sonnet 4"
#: includes/class-ai-translator-admin.php:521
msgid "Better handling of implicit subjects, honorifics, and cultural references. DeepL falls behind here."
msgstr "Mejor manejo de sujetos implícitos, honoríficos y referencias culturales. DeepL queda por detrás aquí."
#: includes/class-ai-translator-admin.php:524
msgid "Marketing / brand tone"
msgstr "Marketing / tono de marca"
#: includes/class-ai-translator-admin.php:525
msgid "Claude Sonnet 4 / Opus 4"
msgstr "Claude Sonnet 4 / Opus 4"
#: includes/class-ai-translator-admin.php:526
msgid "Better preservation of tone, brand voice, and nuance. Ideal for creative copy."
msgstr "Preserva mejor el tono, la voz de marca y los matices. Ideal para copy creativo."
#: includes/class-ai-translator-admin.php:529
msgid "Technical documentation / code"
msgstr "Documentación técnica / código"
#: includes/class-ai-translator-admin.php:530
msgid "GPT-4o / GPT-5"
msgstr "GPT-4o / GPT-5"
#: includes/class-ai-translator-admin.php:531
msgid "Higher accuracy with variables, structured formats, and technical terminology."
msgstr "Mayor precisión con variables, formatos estructurados y terminología técnica."
#: includes/class-ai-translator-admin.php:534
msgid "Long documents (100+ pages)"
msgstr "Documentos largos (más de 100 páginas)"
#: includes/class-ai-translator-admin.php:535
msgid "Gemini 2.5 Pro"
msgstr "Gemini 2.5 Pro"
#: includes/class-ai-translator-admin.php:536
msgid "1M token context window. Maintains terminological consistency across long texts."
msgstr "Ventana de contexto de 1 millón de tokens. Mantiene la consistencia terminológica en textos extensos."
#: includes/class-ai-translator-admin.php:539
msgid "High volume / low cost"
msgstr "Alto volumen / bajo coste"
#: includes/class-ai-translator-admin.php:540
msgid "DeepSeek-V3"
msgstr "DeepSeek-V3"
#: includes/class-ai-translator-admin.php:541
msgid "Quality comparable to GPT-5 at ~$0.14/M tokens (20-50x cheaper than Claude/GPT)."
msgstr "Calidad comparable a GPT-5 a ~0,14 $/M tokens (entre 20 y 50 veces más barato que Claude/GPT)."
#: includes/class-ai-translator-admin.php:544
msgid "Rare / indigenous languages"
msgstr "Idiomas raros / indígenas"
#: includes/class-ai-translator-admin.php:545
msgid "Claude Sonnet or Taskade Translate (multi-model routing)"
msgstr "Claude Sonnet o Taskade Translate (enrutado multimodelo)"
#: includes/class-ai-translator-admin.php:546
msgid "Better coverage for uncommon language pairs."
msgstr "Mejor cobertura en pares de idiomas poco comunes."
#: includes/class-ai-translator-admin.php:549
msgid "Maximum coverage (133+ languages)"
msgstr "Cobertura máxima (más de 133 idiomas)"
#: includes/class-ai-translator-admin.php:550
msgid "Google Cloud Translation"
msgstr "Google Cloud Translation"
#: includes/class-ai-translator-admin.php:551
msgid "The most complete option, though with slightly lower quality on European languages."
msgstr "La opción más completa, aunque con calidad ligeramente inferior en idiomas europeos."
#: includes/class-ai-translator-cli.php:120
msgid "The --locale flag is required."
msgstr "El parámetro --locale es obligatorio."
#: includes/class-ai-translator-cli.php:124
msgid "The --post-id and --post-type flags are mutually exclusive."
msgstr "Los parámetros --post-id y --post-type son mutuamente excluyentes."
#: includes/class-ai-translator-cli.php:141
msgid "No user context. Pass --user=<id|login> so post updates carry an author and capability checks apply."
msgstr "Sin contexto de usuario. Pasa --user=<id|login> para que las actualizaciones de entradas tengan autor y se apliquen los controles de capacidades."
msgid "The WordPress AI plugin is not active."
msgstr "El plugin WordPress AI no está activo."
msgid "The WordPress AI plugin has no provider configured for text generation."
msgstr "El plugin WordPress AI no tiene ningún proveedor configurado para generación de texto."
#. translators: %s: locale code.
#: includes/class-ai-translator-cli.php:133
#, php-format
msgid "The locale %s is not installed on this site."
msgstr "El idioma %s no está instalado en este sitio."
#: includes/class-ai-translator-cli.php:155
msgid "No translation fields are enabled in the plugin settings."
msgstr "No hay ningún campo de traducción activado en los ajustes del plugin."
#: includes/class-ai-translator-cli.php:181
msgid "No posts matched the given criteria."
msgstr "Ninguna entrada coincide con los criterios indicados."
#. translators: 1: success count, 2: failure count, 3: locale.
#: includes/class-ai-translator-cli.php:242
#, php-format
msgid "%1$d translated, %2$d failed (locale: %3$s)."
msgstr "%1$d traducidas, %2$d con fallos (idioma: %3$s)."
#: includes/class-ai-translator-editor-ui.php:160
msgid "Sorry, you are not allowed to edit this post."
msgstr "Lo sentimos, no tienes permiso para editar esta entrada."
#: includes/class-ai-translator-editor-ui.php:190
msgid "Post not found."
msgstr "Entrada no encontrada."
#: includes/class-ai-translator-editor-ui.php:199
msgid "The selected language is not installed on this site."
msgstr "El idioma seleccionado no está instalado en este sitio."
#: includes/class-ai-translator-editor-ui.php:221
msgid "No translatable fields were selected, or the requested fields are disabled in the settings."
msgstr "No se ha seleccionado ningún campo traducible, o los campos solicitados están desactivados en los ajustes."
msgid "The WordPress AI plugin is not active. Translation is unavailable."
msgstr "El plugin WordPress AI no está activo. La traducción no está disponible."
#: includes/class-ai-translator-editor-ui.php:321
#: includes/class-ai-translator-editor-ui.php:450
msgid "No translation fields are enabled in settings."
msgstr "No hay ningún campo de traducción activado en los ajustes."
#: includes/class-ai-translator-editor-ui.php:323
#: includes/class-ai-translator-editor-ui.php:449
msgid "No languages are installed on this site."
msgstr "No hay ningún idioma instalado en este sitio."
#: includes/class-ai-translator-editor-ui.php:326
#: includes/class-ai-translator-editor-ui.php:445
msgid "Target language"
msgstr "Idioma de destino"
#: includes/class-ai-translator-editor-ui.php:336
#: includes/class-ai-translator-editor-ui.php:443
msgid "Translate"
msgstr "Traducir"
#: includes/class-ai-translator-editor-ui.php:341
msgid "Save the post before translating to ensure the latest content is used. The translation replaces the current title and/or content."
msgstr "Guarda la entrada antes de traducir para que se use la versión más reciente. La traducción reemplaza el título y/o el contenido actuales."
#: includes/class-ai-translator-editor-ui.php:444
msgid "Translating…"
msgstr "Traduciendo…"
#: includes/class-ai-translator-editor-ui.php:447
msgid "Translation applied. Review and save the post."
msgstr "Traducción aplicada. Revisa y guarda la entrada."
#: includes/class-ai-translator-editor-ui.php:448
msgid "Translation failed."
msgstr "La traducción ha fallado."
#: includes/class-ai-translator-editor-ui.php:452
msgid "You have unsaved changes. Save the post before translating to ensure the latest content is used."
msgstr "Tienes cambios sin guardar. Guarda la entrada antes de traducir para que se use la versión más reciente."
msgid "The WordPress AI client is not available. Install and activate the WordPress AI plugin."
msgstr "El cliente de WordPress AI no está disponible. Instala y activa el plugin WordPress AI."
msgid "The configured AI providers cannot serve text generation requests. Open the WordPress AI settings to configure a provider."
msgstr "Los proveedores de IA configurados no pueden atender peticiones de generación de texto. Abre los ajustes de WordPress AI para configurar un proveedor."
#: includes/class-ai-translator-translator.php:102
msgid "The target language is not installed on this site."
msgstr "El idioma de destino no está instalado en este sitio."
#. translators: %s: target language name.
#: includes/class-ai-translator-translator.php:108
#, php-format
msgid "You are a professional translator. Translate the user message into %s. Preserve any HTML, shortcodes, line breaks, and Markdown exactly as they appear. Return only the translated text, without explanations, prefaces, or quotation marks."
msgstr "Eres un traductor profesional. Traduce el mensaje del usuario al %s. Preserva el HTML, los shortcodes, los saltos de línea y el Markdown exactamente como aparecen. Devuelve solo el texto traducido, sin explicaciones, prefacios ni comillas."
#: includes/class-ai-translator-translator.php:129
msgid "The AI service returned an unexpected response."
msgstr "El servicio de IA ha devuelto una respuesta inesperada."
#. translators: %s: WordPress AI plugin link.
#, php-format
msgid "AI Translator (by ROBOTSTXT) requires the %s plugin to be active. Translation features will be disabled until it is installed and configured."
msgstr "AI Translator (by ROBOTSTXT) necesita que el plugin %s esté activo. Las funciones de traducción estarán desactivadas hasta que se instale y configure."
msgid "WordPress AI"
msgstr "WordPress AI"
msgid "Security check failed"
msgstr "Comprobación de seguridad fallida"
#: class-robotstxt-updater.php:456
msgid "You do not have sufficient permissions to access this page."
msgstr "No tienes permisos suficientes para acceder a esta página."
#. Plugin URI of the plugin
#: robotstxt-ai-translator.php
msgid "https://git.robotstxt.es/ROBOTSTXT/robotstxt-ai-translator"
msgstr ""
#. Description of the plugin
#: robotstxt-ai-translator.php
msgid "Translate post titles and content from the editor using the native WordPress AI client (available in WP 7.0+). Multisite-ready with global or per-site configuration."
msgstr ""
#. Author URI of the plugin
#: robotstxt-ai-translator.php
msgid "https://www.robotstxt.es/"
msgstr ""
#: class-robotstxt-updater.php:452
msgid "Security check failed."
msgstr "Error de verificación de seguridad."
#: includes/class-ai-translator-admin.php:228
#: includes/class-ai-translator-admin.php:348
#: includes/class-ai-translator-editor-ui.php:455
msgid "Translate excerpt"
msgstr "Traducir extracto"
#. translators: 1: title default, 2: content default, 3: excerpt default.
#: includes/class-ai-translator-admin.php:236
#, php-format
msgid "Network defaults: title %1$s, content %2$s, excerpt %3$s."
msgstr "Valores predeterminados de la red: título %1$s, contenido %2$s, extracto %3$s."
#: includes/class-ai-translator-admin.php:250
#: includes/class-ai-translator-admin.php:253
#: includes/class-ai-translator-admin.php:360
#: includes/class-ai-translator-admin.php:363
msgid "MultilingualPress"
msgstr "MultilingualPress"
#: includes/class-ai-translator-admin.php:257
#: includes/class-ai-translator-admin.php:367
msgid "Auto-translate new connected posts"
msgstr "Traducir automáticamente las entradas conectadas nuevas"
#: includes/class-ai-translator-admin.php:261
#: includes/class-ai-translator-admin.php:371
msgid "When MultilingualPress creates a new connected post, automatically translate its enabled fields into the target site's language. The translation runs synchronously during save."
msgstr "Cuando MultilingualPress crea una entrada conectada nueva, traduce automáticamente los campos activados al idioma del sitio de destino. La traducción se ejecuta de forma síncrona al guardar."
#: includes/class-ai-translator-cli.php:145
#: includes/class-ai-translator-editor-ui.php:319
#: includes/class-ai-translator-editor-ui.php:451
#: includes/class-ai-translator-translator.php:86
msgid "AI features are disabled for this WordPress installation."
msgstr "Las funciones de IA están desactivadas para esta instalación de WordPress."
#: includes/class-ai-translator-cli.php:149
#: includes/class-ai-translator-translator.php:93
msgid "No AI provider is configured for text generation. Open Settings → AI to set one up."
msgstr "No hay ningún proveedor de IA configurado para la generación de texto. Abre Ajustes → IA para configurar uno."
#. translators: %s: Link to the WordPress AI settings page.
#: robotstxt-ai-translator.php:144
#, php-format
msgid "AI Translator (by ROBOTSTXT) requires a configured AI provider for text generation. Open %s to set one up."
msgstr "AI Translator (by ROBOTSTXT) requiere un proveedor de IA configurado para la generación de texto. Abre %s para configurar uno."
#: robotstxt-ai-translator.php:145
msgid "Settings → AI"
msgstr "Ajustes → IA"

View file

@ -2,14 +2,14 @@
# This file is distributed under the GPL-3.0-or-later.
msgid ""
msgstr ""
"Project-Id-Version: AI Translator (by ROBOTSTXT) 1.0.0\n"
"Project-Id-Version: AI Translator (by ROBOTSTXT) 1.1.0\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/robotstxt-ai-translator\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2026-05-23T10:07:42+00:00\n"
"POT-Creation-Date: 2026-05-25T16:53:01+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"X-Generator: WP-CLI 2.12.0\n"
"X-Domain: robotstxt-ai-translator\n"
@ -26,7 +26,7 @@ msgstr ""
#. Description of the plugin
#: robotstxt-ai-translator.php
msgid "Translate post titles and content from the editor using the native WordPress AI plugin as backend. Multisite-ready with global or per-site configuration."
msgid "Translate post titles and content from the editor using the native WordPress AI client (available in WP 7.0+). Multisite-ready with global or per-site configuration."
msgstr ""
#. Author of the plugin
@ -39,12 +39,20 @@ msgstr ""
msgid "https://www.robotstxt.es/"
msgstr ""
#: class-robotstxt-updater.php:452
msgid "Security check failed."
msgstr ""
#: class-robotstxt-updater.php:456
msgid "You do not have sufficient permissions to access this page."
msgstr ""
#: includes/class-ai-translator-admin.php:112
#: includes/class-ai-translator-admin.php:113
#: includes/class-ai-translator-admin.php:130
#: includes/class-ai-translator-admin.php:131
#: includes/class-ai-translator-editor-ui.php:282
#: includes/class-ai-translator-editor-ui.php:433
#: includes/class-ai-translator-editor-ui.php:294
#: includes/class-ai-translator-editor-ui.php:446
msgid "AI Translator"
msgstr ""
@ -54,7 +62,7 @@ msgid "Settings"
msgstr ""
#: includes/class-ai-translator-admin.php:187
#: includes/class-ai-translator-admin.php:261
#: includes/class-ai-translator-admin.php:287
msgid "You do not have permission to access this page."
msgstr ""
@ -68,186 +76,211 @@ msgstr ""
#: includes/class-ai-translator-admin.php:211
#: includes/class-ai-translator-admin.php:214
#: includes/class-ai-translator-admin.php:304
#: includes/class-ai-translator-admin.php:308
#: includes/class-ai-translator-admin.php:330
#: includes/class-ai-translator-admin.php:334
msgid "Translation fields"
msgstr ""
#: includes/class-ai-translator-admin.php:218
#: includes/class-ai-translator-admin.php:312
#: includes/class-ai-translator-editor-ui.php:440
#: includes/class-ai-translator-admin.php:338
#: includes/class-ai-translator-editor-ui.php:453
msgid "Translate title"
msgstr ""
#: includes/class-ai-translator-admin.php:223
#: includes/class-ai-translator-admin.php:317
#: includes/class-ai-translator-editor-ui.php:441
#: includes/class-ai-translator-admin.php:343
#: includes/class-ai-translator-editor-ui.php:454
msgid "Translate content"
msgstr ""
#. translators: 1: title default, 2: content default.
#: includes/class-ai-translator-admin.php:231
#, php-format
msgid "Network defaults: title %1$s, content %2$s."
#: includes/class-ai-translator-admin.php:228
#: includes/class-ai-translator-admin.php:348
#: includes/class-ai-translator-editor-ui.php:455
msgid "Translate excerpt"
msgstr ""
#: includes/class-ai-translator-admin.php:232
#: includes/class-ai-translator-admin.php:233
#. translators: 1: title default, 2: content default, 3: excerpt default.
#: includes/class-ai-translator-admin.php:236
#, php-format
msgid "Network defaults: title %1$s, content %2$s, excerpt %3$s."
msgstr ""
#: includes/class-ai-translator-admin.php:237
#: includes/class-ai-translator-admin.php:238
#: includes/class-ai-translator-admin.php:239
msgid "enabled"
msgstr ""
#: includes/class-ai-translator-admin.php:232
#: includes/class-ai-translator-admin.php:233
#: includes/class-ai-translator-admin.php:237
#: includes/class-ai-translator-admin.php:238
#: includes/class-ai-translator-admin.php:239
msgid "disabled"
msgstr ""
#: includes/class-ai-translator-admin.php:270
msgid "AI Translator Network Settings"
#: includes/class-ai-translator-admin.php:250
#: includes/class-ai-translator-admin.php:253
#: includes/class-ai-translator-admin.php:360
#: includes/class-ai-translator-admin.php:363
msgid "MultilingualPress"
msgstr ""
#: includes/class-ai-translator-admin.php:274
msgid "Network settings saved."
#: includes/class-ai-translator-admin.php:257
#: includes/class-ai-translator-admin.php:367
msgid "Auto-translate new connected posts"
msgstr ""
#: includes/class-ai-translator-admin.php:284
#: includes/class-ai-translator-admin.php:287
msgid "Configuration mode"
msgstr ""
#: includes/class-ai-translator-admin.php:291
msgid "Global configuration: a single setting applies to every site."
#: includes/class-ai-translator-admin.php:261
#: includes/class-ai-translator-admin.php:371
msgid "When MultilingualPress creates a new connected post, automatically translate its enabled fields into the target site's language. The translation runs synchronously during save."
msgstr ""
#: includes/class-ai-translator-admin.php:296
msgid "AI Translator Network Settings"
msgstr ""
#: includes/class-ai-translator-admin.php:300
msgid "Network settings saved."
msgstr ""
#: includes/class-ai-translator-admin.php:310
#: includes/class-ai-translator-admin.php:313
msgid "Configuration mode"
msgstr ""
#: includes/class-ai-translator-admin.php:317
msgid "Global configuration: a single setting applies to every site."
msgstr ""
#: includes/class-ai-translator-admin.php:322
msgid "Per-site configuration: each site can override these defaults."
msgstr ""
#: includes/class-ai-translator-admin.php:321
#: includes/class-ai-translator-admin.php:352
msgid "In global mode this is the configuration for the whole network. In per-site mode these values are used as defaults for sites that have not been configured individually."
msgstr ""
#: includes/class-ai-translator-admin.php:346
#: includes/class-ai-translator-admin.php:378
#: includes/class-ai-translator-admin.php:397
#: includes/class-ai-translator-admin.php:429
msgid "You do not have permission to perform this action."
msgstr ""
#: includes/class-ai-translator-admin.php:422
#: includes/class-ai-translator-admin.php:473
msgid "Recommended models"
msgstr ""
#: includes/class-ai-translator-admin.php:425
#: includes/class-ai-translator-admin.php:476
msgid "This plugin does not call AI providers directly. Configure your preferred provider in the WordPress AI plugin settings. The table below is a guide to help you pick a model based on your translation use case."
msgstr ""
#: includes/class-ai-translator-admin.php:431
#: includes/class-ai-translator-admin.php:482
msgid "Use case"
msgstr ""
#: includes/class-ai-translator-admin.php:432
#: includes/class-ai-translator-admin.php:483
msgid "Recommended model"
msgstr ""
#: includes/class-ai-translator-admin.php:433
#: includes/class-ai-translator-admin.php:484
msgid "Why"
msgstr ""
#: includes/class-ai-translator-admin.php:448
#: includes/class-ai-translator-admin.php:499
msgid "These recommendations are based on public benchmarks and community feedback as of the plugin release date. They may change as providers update their models."
msgstr ""
#: includes/class-ai-translator-admin.php:463
#: includes/class-ai-translator-admin.php:514
msgid "European languages (ES, CA, FR, DE, IT, PT)"
msgstr ""
#: includes/class-ai-translator-admin.php:464
#: includes/class-ai-translator-admin.php:515
msgid "DeepL API Pro"
msgstr ""
#: includes/class-ai-translator-admin.php:465
#: includes/class-ai-translator-admin.php:516
msgid "Best fluency and naturalness (92/100 in benchmarks). Formality control and custom glossaries."
msgstr ""
#: includes/class-ai-translator-admin.php:468
#: includes/class-ai-translator-admin.php:519
msgid "Asian languages (ZH, JA, KO)"
msgstr ""
#: includes/class-ai-translator-admin.php:469
#: includes/class-ai-translator-admin.php:520
msgid "GPT-4o / GPT-5 or Claude Sonnet 4"
msgstr ""
#: includes/class-ai-translator-admin.php:470
#: includes/class-ai-translator-admin.php:521
msgid "Better handling of implicit subjects, honorifics, and cultural references. DeepL falls behind here."
msgstr ""
#: includes/class-ai-translator-admin.php:473
#: includes/class-ai-translator-admin.php:524
msgid "Marketing / brand tone"
msgstr ""
#: includes/class-ai-translator-admin.php:474
#: includes/class-ai-translator-admin.php:525
msgid "Claude Sonnet 4 / Opus 4"
msgstr ""
#: includes/class-ai-translator-admin.php:475
#: includes/class-ai-translator-admin.php:526
msgid "Better preservation of tone, brand voice, and nuance. Ideal for creative copy."
msgstr ""
#: includes/class-ai-translator-admin.php:478
#: includes/class-ai-translator-admin.php:529
msgid "Technical documentation / code"
msgstr ""
#: includes/class-ai-translator-admin.php:479
#: includes/class-ai-translator-admin.php:530
msgid "GPT-4o / GPT-5"
msgstr ""
#: includes/class-ai-translator-admin.php:480
#: includes/class-ai-translator-admin.php:531
msgid "Higher accuracy with variables, structured formats, and technical terminology."
msgstr ""
#: includes/class-ai-translator-admin.php:483
#: includes/class-ai-translator-admin.php:534
msgid "Long documents (100+ pages)"
msgstr ""
#: includes/class-ai-translator-admin.php:484
#: includes/class-ai-translator-admin.php:535
msgid "Gemini 2.5 Pro"
msgstr ""
#: includes/class-ai-translator-admin.php:485
#: includes/class-ai-translator-admin.php:536
msgid "1M token context window. Maintains terminological consistency across long texts."
msgstr ""
#: includes/class-ai-translator-admin.php:488
#: includes/class-ai-translator-admin.php:539
msgid "High volume / low cost"
msgstr ""
#: includes/class-ai-translator-admin.php:489
#: includes/class-ai-translator-admin.php:540
msgid "DeepSeek-V3"
msgstr ""
#: includes/class-ai-translator-admin.php:490
#: includes/class-ai-translator-admin.php:541
msgid "Quality comparable to GPT-5 at ~$0.14/M tokens (20-50x cheaper than Claude/GPT)."
msgstr ""
#: includes/class-ai-translator-admin.php:493
#: includes/class-ai-translator-admin.php:544
msgid "Rare / indigenous languages"
msgstr ""
#: includes/class-ai-translator-admin.php:494
#: includes/class-ai-translator-admin.php:545
msgid "Claude Sonnet or Taskade Translate (multi-model routing)"
msgstr ""
#: includes/class-ai-translator-admin.php:495
#: includes/class-ai-translator-admin.php:546
msgid "Better coverage for uncommon language pairs."
msgstr ""
#: includes/class-ai-translator-admin.php:498
#: includes/class-ai-translator-admin.php:549
msgid "Maximum coverage (133+ languages)"
msgstr ""
#: includes/class-ai-translator-admin.php:499
#: includes/class-ai-translator-admin.php:550
msgid "Google Cloud Translation"
msgstr ""
#: includes/class-ai-translator-admin.php:500
#: includes/class-ai-translator-admin.php:551
msgid "The most complete option, though with slightly lower quality on European languages."
msgstr ""
@ -259,35 +292,38 @@ msgstr ""
msgid "The --post-id and --post-type flags are mutually exclusive."
msgstr ""
#: includes/class-ai-translator-cli.php:129
msgid "No user context. Pass --user=<id|login> so post updates carry an author and capability checks apply."
msgstr ""
#: includes/class-ai-translator-cli.php:133
#: includes/class-ai-translator-editor-ui.php:438
msgid "The WordPress AI plugin is not active."
msgstr ""
#: includes/class-ai-translator-cli.php:137
msgid "The WordPress AI plugin has no provider configured for text generation."
msgstr ""
#. translators: %s: locale code.
#: includes/class-ai-translator-cli.php:144
#: includes/class-ai-translator-cli.php:133
#, php-format
msgid "The locale %s is not installed on this site."
msgstr ""
#: includes/class-ai-translator-cli.php:153
#: includes/class-ai-translator-cli.php:141
msgid "No user context. Pass --user=<id|login> so post updates carry an author and capability checks apply."
msgstr ""
#: includes/class-ai-translator-cli.php:145
#: includes/class-ai-translator-editor-ui.php:319
#: includes/class-ai-translator-editor-ui.php:451
#: includes/class-ai-translator-translator.php:86
msgid "AI features are disabled for this WordPress installation."
msgstr ""
#: includes/class-ai-translator-cli.php:149
#: includes/class-ai-translator-translator.php:93
msgid "No AI provider is configured for text generation. Open Settings → AI to set one up."
msgstr ""
#: includes/class-ai-translator-cli.php:155
msgid "No translation fields are enabled in the plugin settings."
msgstr ""
#: includes/class-ai-translator-cli.php:179
#: includes/class-ai-translator-cli.php:181
msgid "No posts matched the given criteria."
msgstr ""
#. translators: 1: success count, 2: failure count, 3: locale.
#: includes/class-ai-translator-cli.php:230
#: includes/class-ai-translator-cli.php:242
#, php-format
msgid "%1$d translated, %2$d failed (locale: %3$s)."
msgstr ""
@ -304,90 +340,70 @@ msgstr ""
msgid "The selected language is not installed on this site."
msgstr ""
#: includes/class-ai-translator-editor-ui.php:217
#: includes/class-ai-translator-editor-ui.php:221
msgid "No translatable fields were selected, or the requested fields are disabled in the settings."
msgstr ""
#: includes/class-ai-translator-editor-ui.php:307
msgid "The WordPress AI plugin is not active. Translation is unavailable."
msgstr ""
#: includes/class-ai-translator-editor-ui.php:309
#: includes/class-ai-translator-editor-ui.php:437
#: includes/class-ai-translator-editor-ui.php:321
#: includes/class-ai-translator-editor-ui.php:450
msgid "No translation fields are enabled in settings."
msgstr ""
#: includes/class-ai-translator-editor-ui.php:311
#: includes/class-ai-translator-editor-ui.php:436
#: includes/class-ai-translator-editor-ui.php:323
#: includes/class-ai-translator-editor-ui.php:449
msgid "No languages are installed on this site."
msgstr ""
#: includes/class-ai-translator-editor-ui.php:314
#: includes/class-ai-translator-editor-ui.php:432
#: includes/class-ai-translator-editor-ui.php:326
#: includes/class-ai-translator-editor-ui.php:445
msgid "Target language"
msgstr ""
#: includes/class-ai-translator-editor-ui.php:324
#: includes/class-ai-translator-editor-ui.php:430
#: includes/class-ai-translator-editor-ui.php:336
#: includes/class-ai-translator-editor-ui.php:443
msgid "Translate"
msgstr ""
#: includes/class-ai-translator-editor-ui.php:329
#: includes/class-ai-translator-editor-ui.php:341
msgid "Save the post before translating to ensure the latest content is used. The translation replaces the current title and/or content."
msgstr ""
#: includes/class-ai-translator-editor-ui.php:431
#: includes/class-ai-translator-editor-ui.php:444
msgid "Translating…"
msgstr ""
#: includes/class-ai-translator-editor-ui.php:434
#: includes/class-ai-translator-editor-ui.php:447
msgid "Translation applied. Review and save the post."
msgstr ""
#: includes/class-ai-translator-editor-ui.php:435
#: includes/class-ai-translator-editor-ui.php:448
msgid "Translation failed."
msgstr ""
#: includes/class-ai-translator-editor-ui.php:439
#: includes/class-ai-translator-editor-ui.php:452
msgid "You have unsaved changes. Save the post before translating to ensure the latest content is used."
msgstr ""
#: includes/class-ai-translator-translator.php:82
msgid "The WordPress AI client is not available. Install and activate the WordPress AI plugin."
msgstr ""
#: includes/class-ai-translator-translator.php:89
msgid "The configured AI providers cannot serve text generation requests. Open the WordPress AI settings to configure a provider."
msgstr ""
#: includes/class-ai-translator-translator.php:98
#: includes/class-ai-translator-translator.php:102
msgid "The target language is not installed on this site."
msgstr ""
#. translators: %s: target language name.
#: includes/class-ai-translator-translator.php:104
#: includes/class-ai-translator-translator.php:108
#, php-format
msgid "You are a professional translator. Translate the user message into %s. Preserve any HTML, shortcodes, line breaks, and Markdown exactly as they appear. Return only the translated text, without explanations, prefaces, or quotation marks."
msgstr ""
#: includes/class-ai-translator-translator.php:125
#: includes/class-ai-translator-translator.php:129
msgid "The AI service returned an unexpected response."
msgstr ""
#. translators: %s: WordPress AI plugin link.
#: robotstxt-ai-translator.php:109
#. translators: %s: Link to the WordPress AI settings page.
#: robotstxt-ai-translator.php:144
#, php-format
msgid "AI Translator (by ROBOTSTXT) requires the %s plugin to be active. Translation features will be disabled until it is installed and configured."
msgid "AI Translator (by ROBOTSTXT) requires a configured AI provider for text generation. Open %s to set one up."
msgstr ""
#: robotstxt-ai-translator.php:110
msgid "WordPress AI"
msgstr ""
#: robotstxt-updater.php:362
msgid "Security check failed"
msgstr ""
#: robotstxt-updater.php:367
msgid "You do not have sufficient permissions to access this page."
#: robotstxt-ai-translator.php:145
msgid "Settings → AI"
msgstr ""

View file

@ -2,10 +2,9 @@
Contributors: robotstxt, javiercasares
Tags: ai, translation, multilingual, multisite, editor
Requires at least: 7.0
Tested up to: 7.0
Tested up to: 7.1
Requires PHP: 7.4
Requires Plugins: ai
Stable tag: 1.0.0
Stable tag: 1.1.0
License: GPL-3.0-or-later
License URI: https://www.gnu.org/licenses/gpl-3.0.txt
@ -109,9 +108,9 @@ The endpoint reads the **saved** title and content from the database, not from t
= Requirements =
* WordPress 7.0 or higher.
* WordPress 7.0 or higher (the `wp_ai_client_prompt()` function is part of WordPress core since 7.0).
* PHP 7.4 or higher.
* The official WordPress AI plugin (https://wordpress.org/plugins/ai/) installed, active, and configured with valid provider credentials.
* At least one AI provider configured in Settings → AI. The plugin works with any provider registered through the WordPress AI client — no specific AI plugin is required.
= Manual download =
@ -123,7 +122,7 @@ On Multisite, network-activate the plugin and configure it under Network Admin -
= Does this plugin call any AI provider directly? =
No. Every AI request is delegated to the official WordPress AI plugin, which handles authentication, model selection, and provider routing. This plugin does not store API keys.
No. Every AI request goes through `wp_ai_client_prompt()`, WordPress's native AI client (introduced in WP 7.0). The plugin does not talk to any provider directly and does not store API keys. Authentication, model selection, and provider routing are handled by WordPress and whatever AI provider plugin you have configured.
= Does it work with the classic editor? =
@ -135,12 +134,19 @@ The plugin reads the post title and content from the database, not from the edit
== Compatibility ==
* WordPress: 7.0
* WordPress: 7.0 - 7.1
* PHP: 7.4 - 8.5
* WP-CLI: 2.10 or newer
* WP-CLI: 2.12 or newer
== Changelog ==
= 1.1.0 =
* Added excerpt field (`post_excerpt`) as a translatable field, with a dedicated settings toggle.
* Added MultilingualPress integration: when MLP creates a new connected post, the plugin automatically translates the enabled fields into the target site's language (opt-in, disabled by default).
* Removed `Requires Plugins: ai` header — `wp_ai_client_prompt()` is a native WordPress 7.0 function; the plugin now works with any AI provider configured through WordPress core.
* Admin notice now shows when no AI provider is configured for text generation (instead of checking for a function that always exists in WP 7.0+).
= 1.0.0 =
* Initial release. Translation panel for block and classic editors, REST endpoint, WP-CLI command, single-site and Multisite (global / per-site) configuration modes.

View file

@ -3,8 +3,8 @@
* Plugin Name: AI Translator (by ROBOTSTXT)
* Plugin URI: https://git.robotstxt.es/ROBOTSTXT/robotstxt-ai-translator
* Gitea Plugin URI: ROBOTSTXT/robotstxt-ai-translator
* Description: Translate post titles and content from the editor using the native WordPress AI plugin as backend. Multisite-ready with global or per-site configuration.
* Version: 1.0.0
* Description: Translate post titles and content from the editor using the native WordPress AI client (available in WP 7.0+). Multisite-ready with global or per-site configuration.
* Version: 1.1.0
* Author: ROBOTSTXT
* Author URI: https://www.robotstxt.es/
* License: GPL-3.0-or-later
@ -13,7 +13,6 @@
* Domain Path: /languages
* Requires at least: 7.0
* Requires PHP: 7.4
* Requires Plugins: ai
* Network: true
*
* @package ROBOTSTXT\AI_Translator
@ -26,7 +25,7 @@ if ( ! defined( 'ABSPATH' ) ) {
/**
* Plugin version.
*/
define( 'AI_TRANSLATOR_VERSION', '1.0.0' );
define( 'AI_TRANSLATOR_VERSION', '1.1.0' );
/**
* Main plugin file path.
@ -57,7 +56,11 @@ require_once AI_TRANSLATOR_DIR . 'includes/class-ai-translator-settings.php';
require_once AI_TRANSLATOR_DIR . 'includes/class-ai-translator-translator.php';
require_once AI_TRANSLATOR_DIR . 'includes/class-ai-translator-admin.php';
require_once AI_TRANSLATOR_DIR . 'includes/class-ai-translator-editor-ui.php';
require_once AI_TRANSLATOR_DIR . 'robotstxt-updater.php';
require_once AI_TRANSLATOR_DIR . 'class-robotstxt-updater.php';
if ( class_exists( '\Inpsyde\MultilingualPress\TranslationUi\Post\MetaboxAction' ) ) {
require_once AI_TRANSLATOR_DIR . 'includes/class-ai-translator-mlp-integration.php';
}
if ( defined( 'WP_CLI' ) && WP_CLI ) {
require_once AI_TRANSLATOR_DIR . 'includes/class-ai-translator-cli.php';
@ -103,30 +106,43 @@ function ai_translator_bootstrap() {
$cli->register();
}
if ( class_exists( 'AI_Translator_MLP_Integration' ) ) {
$mlp = new AI_Translator_MLP_Integration( $settings, $translator );
$mlp->register();
}
Robotstxt_Updater::init( AI_TRANSLATOR_FILE );
}
add_action( 'plugins_loaded', 'ai_translator_bootstrap' );
/**
* Renders an admin notice when the WordPress AI plugin is not active.
* Renders an admin notice when no AI provider is configured for text generation.
*
* `wp_ai_client_prompt()` is part of WordPress 7.0 core, so the function always
* exists. The meaningful check is whether a provider capable of text generation has
* been configured which is what `AI_Translator_Translator::is_supported()` tests.
*
* @since 1.0.0
*
* @return void
*/
function ai_translator_dependency_notice() {
if ( function_exists( 'wp_ai_client_prompt' ) ) {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
if ( ! current_user_can( 'activate_plugins' ) ) {
$translator = new AI_Translator_Translator();
if ( $translator->is_supported() ) {
return;
}
$settings_url = admin_url( 'options-general.php?page=options-connectors-wp-admin' );
$message = sprintf(
/* translators: %s: WordPress AI plugin link. */
esc_html__( 'AI Translator (by ROBOTSTXT) requires the %s plugin to be active. Translation features will be disabled until it is installed and configured.', 'robotstxt-ai-translator' ),
'<a href="https://wordpress.org/plugins/ai/" target="_blank" rel="noopener noreferrer">' . esc_html__( 'WordPress AI', 'robotstxt-ai-translator' ) . '</a>'
/* translators: %s: Link to the WordPress AI settings page. */
esc_html__( 'AI Translator (by ROBOTSTXT) requires a configured AI provider for text generation. Open %s to set one up.', 'robotstxt-ai-translator' ),
'<a href="' . esc_url( $settings_url ) . '">' . esc_html__( 'Settings → AI', 'robotstxt-ai-translator' ) . '</a>'
);
printf(
@ -135,9 +151,7 @@ function ai_translator_dependency_notice() {
$message,
array(
'a' => array(
'href' => array(),
'target' => array(),
'rel' => array(),
'href' => array(),
),
)
)

View file

@ -1,383 +0,0 @@
<?php
/**
* Generic JSON-based updater for ROBOTSTXT plugins.
*
* This file is designed to be copied to any ROBOTSTXT plugin.
* It auto-configures itself by reading the plugin headers.
*
* @package ROBOTSTXT
* @version 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'Robotstxt_Updater' ) ) {
/**
* Class Robotstxt_Updater
*
* Generic updater that works with any plugin.
* Reads plugin headers and constructs update URL automatically.
*/
class Robotstxt_Updater {
/**
* Plugin file path.
*
* @var string
*/
private string $plugin_file_path;
/**
* Plugin basename (e.g., 'my-plugin/my-plugin.php').
*
* @var string
*/
private string $plugin_basename;
/**
* Plugin slug (directory name).
*
* @var string
*/
private string $plugin_slug;
/**
* Remote JSON URL.
*
* @var string
*/
private string $json_url;
/**
* Cache key.
*
* @var string
*/
private string $cache_key;
/**
* Plugin headers.
*
* @var array
*/
private array $plugin_data;
/**
* Initialize the updater.
*
* Usage in your main plugin file:
* require_once __DIR__ . '/robotstxt-updater.php';
* Robotstxt_Updater::init( __FILE__ );
*
* @param string $plugin_file_path Absolute path to the main plugin file.
*/
public static function init( string $plugin_file_path ): void {
$instance = new self( $plugin_file_path );
$instance->register();
}
/**
* Constructor.
*
* @param string $plugin_file_path Absolute path to the main plugin file.
*/
private function __construct( string $plugin_file_path ) {
$this->plugin_file_path = $plugin_file_path;
$this->plugin_basename = plugin_basename( $plugin_file_path );
$this->plugin_slug = dirname( $this->plugin_basename );
$this->plugin_data = $this->get_plugin_data();
$this->json_url = $this->build_json_url();
$this->cache_key = 'robotstxt_updater_' . md5( $this->plugin_basename );
}
/**
* Register WordPress hooks.
*/
private function register(): void {
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'inject_update_info' ) );
add_filter( 'plugins_api', array( $this, 'provide_plugin_details' ), 10, 3 );
add_action( 'admin_init', array( $this, 'handle_cache_clear' ) );
add_action( 'robotstxt_updater_clear_cache', array( $this, 'clear_cache' ) );
}
/**
* Get plugin headers.
*
* @return array Plugin data.
*/
private function get_plugin_data(): array {
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
return get_plugin_data( $this->plugin_file_path, false, false );
}
/**
* Build JSON URL from plugin headers.
*
* Tries to use "Gitea Plugin URI" header to construct the URL.
* Falls back to Plugin URI if Gitea URI is not available.
*
* @return string JSON URL.
*/
private function build_json_url(): string {
// Try Gitea Plugin URI (format: "OWNER/REPO" or full URL).
if ( ! empty( $this->plugin_data['Gitea Plugin URI'] ) ) {
$gitea_uri = $this->plugin_data['Gitea Plugin URI'];
// If it's already a full URL, use it.
if ( str_starts_with( $gitea_uri, 'http' ) ) {
// Extract base URL and construct JSON path.
return rtrim( $gitea_uri, '/' ) . '/raw/branch/main/update.json';
}
// If it's in format "OWNER/REPO", construct full URL.
if ( preg_match( '#^[^/]+/[^/]+$#', $gitea_uri ) ) {
return "https://git.robotstxt.es/{$gitea_uri}/raw/branch/main/update.json";
}
}
// Fallback: try to extract from Plugin URI.
if ( ! empty( $this->plugin_data['PluginURI'] ) ) {
$plugin_uri = $this->plugin_data['PluginURI'];
if ( str_contains( $plugin_uri, 'git.robotstxt.es' ) ) {
return rtrim( $plugin_uri, '/' ) . '/raw/branch/main/update.json';
}
}
// Last resort: construct from plugin slug.
return "https://git.robotstxt.es/ROBOTSTXT/{$this->plugin_slug}/raw/branch/main/update.json";
}
/**
* Inject update info into WP's plugin update transient.
*
* @param object|mixed $transient The update_plugins transient.
*
* @return object The modified transient.
*/
public function inject_update_info( $transient ) {
if ( ! is_object( $transient ) ) {
$transient = new stdClass();
}
if ( empty( $transient->checked ) || ! is_array( $transient->checked ) ) {
return $transient;
}
if ( empty( $transient->checked[ $this->plugin_basename ] ) ) {
return $transient;
}
$current_version = $transient->checked[ $this->plugin_basename ];
$remote = $this->get_remote_data();
if ( empty( $remote['version'] ) || empty( $remote['download_url'] ) ) {
return $transient;
}
if ( ! $this->is_compatible( $remote ) ) {
return $transient;
}
if ( version_compare( $remote['version'], $current_version, '>' ) ) {
$update = (object) array(
'slug' => $remote['slug'] ?? $this->plugin_slug,
'plugin' => $this->plugin_basename,
'new_version' => $remote['version'],
'url' => $remote['homepage'] ?? $this->plugin_data['PluginURI'] ?? '',
'package' => $remote['download_url'],
'tested' => $remote['tested'] ?? '',
'requires' => $remote['requires'] ?? '',
'requires_php' => $remote['requires_php'] ?? '',
);
$transient->response[ $this->plugin_basename ] = $update;
}
return $transient;
}
/**
* Provide "View details" modal content.
*
* @param false|object|array $result The result object or array.
* @param string $action The type of information being requested.
* @param object $args Plugin API arguments.
*
* @return false|object The plugin information object or false.
*/
public function provide_plugin_details( $result, string $action, object $args ) {
if ( 'plugin_information' !== $action ) {
return $result;
}
if ( empty( $args->slug ) || $args->slug !== $this->plugin_slug ) {
return $result;
}
$remote = $this->get_remote_data();
if ( empty( $remote['version'] ) ) {
return $result;
}
return (object) array(
'name' => $remote['name'] ?? $this->plugin_data['Name'] ?? $this->plugin_slug,
'slug' => $remote['slug'] ?? $this->plugin_slug,
'version' => $remote['version'],
'author' => $remote['author'] ?? $this->plugin_data['Author'] ?? '',
'homepage' => $remote['homepage'] ?? $this->plugin_data['PluginURI'] ?? '',
'requires' => $remote['requires'] ?? '',
'tested' => $remote['tested'] ?? '',
'requires_php' => $remote['requires_php'] ?? '',
'sections' => array(
'description' => $remote['description'] ?? $this->plugin_data['Description'] ?? '',
'changelog' => $remote['changelog'] ?? '',
),
'download_link' => $remote['download_url'] ?? '',
);
}
/**
* Get remote data with caching and HMAC signature verification.
*
* @return array Remote data.
*/
private function get_remote_data(): array {
$cached = get_site_transient( $this->cache_key );
// Verify HMAC signature if AUTH_SALT is defined and cache has signature.
if ( false !== $cached && defined( 'AUTH_SALT' ) && '' !== AUTH_SALT ) {
if ( is_array( $cached ) && isset( $cached['signature'], $cached['data'] ) ) {
$expected_sig = hash_hmac( 'sha256', $this->cache_key . serialize( $cached['data'] ), AUTH_SALT );
if ( hash_equals( $expected_sig, $cached['signature'] ) ) {
// Signature valid, return data.
return is_array( $cached['data'] ) ? $cached['data'] : array();
}
// Signature invalid, delete corrupted cache.
delete_site_transient( $this->cache_key );
$cached = false;
}
}
// If no valid cache, fetch fresh data.
if ( false === $cached ) {
$remote = $this->fetch_json();
// Store with HMAC signature if AUTH_SALT is available.
if ( defined( 'AUTH_SALT' ) && '' !== AUTH_SALT ) {
$payload = array(
'data' => $remote ?: array(),
'timestamp' => time(),
'signature' => hash_hmac( 'sha256', $this->cache_key . serialize( $remote ?: array() ), AUTH_SALT ),
);
set_site_transient( $this->cache_key, $payload, 6 * HOUR_IN_SECONDS );
} else {
// Fallback to standard caching.
set_site_transient( $this->cache_key, $remote ?: array(), 6 * HOUR_IN_SECONDS );
}
return is_array( $remote ) ? $remote : array();
}
// Legacy cache format without signature (backward compatibility).
return is_array( $cached ) ? $cached : array();
}
/**
* Fetch JSON from remote URL.
*
* @return array Decoded JSON data.
*/
private function fetch_json(): array {
$response = wp_remote_get(
$this->json_url,
array(
'timeout' => 10,
'headers' => array(
'Accept' => 'application/json',
),
)
);
if ( is_wp_error( $response ) ) {
return array();
}
$code = (int) wp_remote_retrieve_response_code( $response );
if ( $code < 200 || $code >= 300 ) {
return array();
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
return is_array( $data ) ? $data : array();
}
/**
* Check compatibility.
*
* @param array $remote Remote data.
*
* @return bool True if compatible.
*/
private function is_compatible( array $remote ): bool {
if ( ! empty( $remote['requires_php'] ) ) {
if ( version_compare( PHP_VERSION, $remote['requires_php'], '<' ) ) {
return false;
}
}
if ( ! empty( $remote['requires'] ) ) {
if ( version_compare( get_bloginfo( 'version' ), $remote['requires'], '<' ) ) {
return false;
}
}
return true;
}
/**
* Handle manual cache clear via URL parameter.
*/
public function handle_cache_clear(): void {
// Check if this is a cache clear request first.
$clear_cache = filter_input( INPUT_GET, 'robotstxt_clear_update_cache', FILTER_UNSAFE_RAW );
if ( null === $clear_cache ) {
return;
}
// This is a cache clear request - now verify nonce.
$nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_UNSAFE_RAW );
$nonce = $nonce_raw ? sanitize_text_field( wp_unslash( $nonce_raw ) ) : '';
if ( ! wp_verify_nonce( $nonce, 'robotstxt_clear_update_cache' ) ) {
wp_die( esc_html__( 'Security check failed', 'robotstxt-ai-translator' ) );
}
// Check permissions.
if ( ! current_user_can( 'update_plugins' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-ai-translator' ) );
}
$this->clear_cache();
wp_safe_redirect( remove_query_arg( array( 'robotstxt_clear_update_cache', '_wpnonce' ) ) );
exit;
}
/**
* Clear update cache.
*/
public function clear_cache(): void {
delete_site_transient( $this->cache_key );
delete_site_transient( 'update_plugins' );
}
}
}

View file

@ -1,20 +1,20 @@
{
"name": "AI Translator (by ROBOTSTXT)",
"slug": "robotstxt-ai-translator",
"version": "1.0.0",
"download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-ai-translator/releases/download/1.0.0/robotstxt-ai-translator-1.0.0.zip",
"version": "1.1.0",
"download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-ai-translator/releases/download/1.1.0/robotstxt-ai-translator-1.1.0.zip",
"requires": "7.0",
"requires_php": "7.4",
"tested": "7.0",
"last_updated": "2026-05-23",
"tested": "7.1",
"last_updated": "2026-05-25",
"author": "ROBOTSTXT",
"author_profile": "https://www.robotstxt.es/",
"homepage": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-ai-translator",
"description": "Translate post titles and content from the WordPress editor using the native WordPress AI plugin as backend. Multisite-ready with global or per-site configuration.",
"changelog": "<h3>1.0.0 - 2026-05-23</h3><ul><li><strong>Added:</strong> Block editor sidebar and Classic editor metabox with language selector and Translate button.</li><li><strong>Added:</strong> REST endpoint POST /wp-json/ai-translator/v1/translate.</li><li><strong>Added:</strong> WP-CLI command wp ai-translator translate for single-post and bulk translation.</li><li><strong>Added:</strong> Network admin settings page with global / per-site configuration mode.</li><li><strong>Added:</strong> Site admin settings page with title and content translation toggles.</li><li><strong>Added:</strong> Model recommendations table in the settings pages.</li></ul>",
"description": "Translate post titles and content from the WordPress editor using the native WordPress AI client (available in WP 7.0+). Multisite-ready with global or per-site configuration.",
"changelog": "<h3>1.1.0 - 2026-05-25</h3><ul><li><strong>Added:</strong> Excerpt translation — post_excerpt is now a translatable field with its own settings toggle.</li><li><strong>Added:</strong> MultilingualPress integration — auto-translate new connected posts at creation time (opt-in).</li><li><strong>Changed:</strong> Removed Requires Plugins: ai header — wp_ai_client_prompt() is native to WordPress 7.0; works with any configured AI provider.</li><li><strong>Changed:</strong> Admin notice now fires when no text-generation provider is configured.</li></ul><h3>1.0.0 - 2026-05-23</h3><ul><li><strong>Added:</strong> Block editor sidebar and Classic editor metabox with language selector and Translate button.</li><li><strong>Added:</strong> REST endpoint POST /wp-json/ai-translator/v1/translate.</li><li><strong>Added:</strong> WP-CLI command wp ai-translator translate for single-post and bulk translation.</li><li><strong>Added:</strong> Network admin settings page with global / per-site configuration mode.</li><li><strong>Added:</strong> Site admin settings page with title and content translation toggles.</li><li><strong>Added:</strong> Model recommendations table in the settings pages.</li></ul>",
"sections": {
"description": "Translate post titles and content from the WordPress editor using the native WordPress AI plugin as backend. Multisite-ready with global or per-site configuration.",
"changelog": "<h3>1.0.0 - 2026-05-23</h3><ul><li><strong>Added:</strong> Block editor sidebar and Classic editor metabox with language selector and Translate button.</li><li><strong>Added:</strong> REST endpoint POST /wp-json/ai-translator/v1/translate.</li><li><strong>Added:</strong> WP-CLI command wp ai-translator translate for single-post and bulk translation.</li><li><strong>Added:</strong> Network admin settings page with global / per-site configuration mode.</li><li><strong>Added:</strong> Site admin settings page with title and content translation toggles.</li><li><strong>Added:</strong> Model recommendations table in the settings pages.</li></ul>"
"description": "Translate post titles and content from the WordPress editor using the native WordPress AI client (available in WP 7.0+). Multisite-ready with global or per-site configuration.",
"changelog": "<h3>1.1.0 - 2026-05-25</h3><ul><li><strong>Added:</strong> Excerpt translation — post_excerpt is now a translatable field with its own settings toggle.</li><li><strong>Added:</strong> MultilingualPress integration — auto-translate new connected posts at creation time (opt-in).</li><li><strong>Changed:</strong> Removed Requires Plugins: ai header — wp_ai_client_prompt() is native to WordPress 7.0; works with any configured AI provider.</li><li><strong>Changed:</strong> Admin notice now fires when no text-generation provider is configured.</li></ul><h3>1.0.0 - 2026-05-23</h3><ul><li><strong>Added:</strong> Block editor sidebar and Classic editor metabox with language selector and Translate button.</li><li><strong>Added:</strong> REST endpoint POST /wp-json/ai-translator/v1/translate.</li><li><strong>Added:</strong> WP-CLI command wp ai-translator translate for single-post and bulk translation.</li><li><strong>Added:</strong> Network admin settings page with global / per-site configuration mode.</li><li><strong>Added:</strong> Site admin settings page with title and content translation toggles.</li><li><strong>Added:</strong> Model recommendations table in the settings pages.</li></ul>"
},
"banners": {
"low": "",