From 02cd01e862b7fade42aeebfaf7196963109e7a17 Mon Sep 17 00:00:00 2001 From: Javier Casares Date: Mon, 25 May 2026 17:12:22 +0000 Subject: [PATCH] v1.1.0 --- assets/js/editor.js | 43 +- changelog.txt | 39 +- class-robotstxt-updater.php | 476 ++++++++++++++++++ includes/class-ai-translator-admin.php | 68 ++- includes/class-ai-translator-cli.php | 40 +- includes/class-ai-translator-editor-ui.php | 28 +- .../class-ai-translator-mlp-integration.php | 174 +++++++ includes/class-ai-translator-settings.php | 6 +- includes/class-ai-translator-translator.php | 16 +- languages/robotstxt-ai-translator-ca.l10n.php | 2 +- languages/robotstxt-ai-translator-ca.mo | Bin 11058 -> 10719 bytes languages/robotstxt-ai-translator-ca.po | 208 ++++++-- .../robotstxt-ai-translator-es_ES.l10n.php | 2 +- languages/robotstxt-ai-translator-es_ES.mo | Bin 11030 -> 10714 bytes languages/robotstxt-ai-translator-es_ES.po | 208 ++++++-- languages/robotstxt-ai-translator.pot | 262 +++++----- readme.txt | 22 +- robotstxt-ai-translator.php | 42 +- robotstxt-updater.php | 383 -------------- update.json | 16 +- 20 files changed, 1377 insertions(+), 658 deletions(-) create mode 100644 class-robotstxt-updater.php create mode 100644 includes/class-ai-translator-mlp-integration.php delete mode 100644 robotstxt-updater.php diff --git a/assets/js/editor.js b/assets/js/editor.js index f5231eb..4fbd603 100644 --- a/assets/js/editor.js +++ b/assets/js/editor.js @@ -30,7 +30,7 @@ /** * Calls the REST endpoint to translate the selected fields of the saved post. * - * @param {Array} fields Field names to translate ('title', 'content'). + * @param {Array} fields Field names to translate ('title', 'content', 'excerpt'). * @param {string} targetLocale WordPress locale code. * @returns {Promise} 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', diff --git a/changelog.txt b/changelog.txt index d5ea8d3..88b93f7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -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) diff --git a/class-robotstxt-updater.php b/class-robotstxt-updater.php new file mode 100644 index 0000000..039d5da --- /dev/null +++ b/class-robotstxt-updater.php @@ -0,0 +1,476 @@ + + */ + 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 + */ + 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 $result The existing result. + * @param string $action The type of information being requested. + * @param object $args Plugin API arguments. + * + * @return false|object|array + */ + 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 + */ + 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 + */ + 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 $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' ); + } + } +} diff --git a/includes/class-ai-translator-admin.php b/includes/class-ai-translator-admin.php index f474e4d..cbf2918 100644 --- a/includes/class-ai-translator-admin.php +++ b/includes/class-ai-translator-admin.php @@ -221,16 +221,22 @@ if ( ! class_exists( 'AI_Translator_Admin' ) ) {
+ +

@@ -238,6 +244,26 @@ if ( ! class_exists( 'AI_Translator_Admin' ) ) { + + + + + +
+ + + + +

+ +

+
+ + + @@ -315,6 +341,11 @@ if ( ! class_exists( 'AI_Translator_Admin' ) ) {
+ +

@@ -323,6 +354,26 @@ if ( ! class_exists( 'AI_Translator_Admin' ) ) { + + + + + +

+ + + + +

+ +

+
+ + + @@ -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 $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'] ), ); } } diff --git a/includes/class-ai-translator-cli.php b/includes/class-ai-translator-cli.php index 354fa15..f869f0b 100644 --- a/includes/class-ai-translator-cli.php +++ b/includes/class-ai-translator-cli.php @@ -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= 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= 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 ) ) { diff --git a/includes/class-ai-translator-editor-ui.php b/includes/class-ai-translator-editor-ui.php index 336581f..bde21b5 100644 --- a/includes/class-ai-translator-editor-ui.php +++ b/includes/class-ai-translator-editor-ui.php @@ -28,7 +28,7 @@ if ( ! class_exists( 'AI_Translator_Editor_UI' ) ) { * @since 1.0.0 * @var array */ - 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' ) ) { ?>
translator->is_available() ) : ?> -

- +

+

@@ -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; diff --git a/includes/class-ai-translator-mlp-integration.php b/includes/class-ai-translator-mlp-integration.php new file mode 100644 index 0000000..5561495 --- /dev/null +++ b/includes/class-ai-translator-mlp-integration.php @@ -0,0 +1,174 @@ +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 $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 ) ); + } + } + } +} diff --git a/includes/class-ai-translator-settings.php b/includes/class-ai-translator-settings.php index 41c9cf2..97b1628 100644 --- a/includes/class-ai-translator-settings.php +++ b/includes/class-ai-translator-settings.php @@ -55,8 +55,10 @@ if ( ! class_exists( 'AI_Translator_Settings' ) ) { * @var array */ const DEFAULTS = array( - 'translate_title' => true, - 'translate_content' => true, + 'translate_title' => true, + 'translate_content' => true, + 'translate_excerpt' => true, + 'auto_translate_on_mlp_create' => false, ); /** diff --git a/includes/class-ai-translator-translator.php b/includes/class-ai-translator-translator.php index 6c5437e..07f9b0f 100644 --- a/includes/class-ai-translator-translator.php +++ b/includes/class-ai-translator-translator.php @@ -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' ) ); } diff --git a/languages/robotstxt-ai-translator-ca.l10n.php b/languages/robotstxt-ai-translator-ca.l10n.php index 63bb90e..1ea8742 100644 --- a/languages/robotstxt-ai-translator-ca.l10n.php +++ b/languages/robotstxt-ai-translator-ca.l10n.php @@ -1,2 +1,2 @@ 'robotstxt-ai-translator','plural-forms'=>'nplurals=2; plural=(n != 1);','language'=>'ca','project-id-version'=>'AI Translator (by ROBOTSTXT) 1.0.0','pot-creation-date'=>'2026-05-23T10:07:42+00:00','po-revision-date'=>'2026-05-23 10:30+0000','x-generator'=>'hand-written','messages'=>['AI Translator (by ROBOTSTXT)'=>'AI Translator (by ROBOTSTXT)','Translate post titles and content from the editor using the native WordPress AI plugin as backend. Multisite-ready with global or per-site configuration.'=>'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.','ROBOTSTXT'=>'ROBOTSTXT','AI Translator'=>'AI Translator','Settings'=>'Ajustaments','You do not have permission to access this page.'=>'No tens permís per accedir a aquesta pàgina.','AI Translator Settings'=>'Ajustaments d\'AI Translator','Settings saved.'=>'Ajustaments desats.','Translation fields'=>'Camps a traduir','Translate title'=>'Tradueix el títol','Translate content'=>'Tradueix el contingut','Network defaults: title %1$s, content %2$s.'=>'Valors predeterminats de la xarxa: títol %1$s, contingut %2$s.','enabled'=>'activat','disabled'=>'desactivat','AI Translator Network Settings'=>'Ajustaments de xarxa d\'AI Translator','Network settings saved.'=>'Ajustaments de xarxa desats.','Configuration mode'=>'Mode de configuració','Global configuration: a single setting applies to every site.'=>'Configuració global: un sol ajustament s\'aplica a tots els llocs.','Per-site configuration: each site can override these defaults.'=>'Configuració per lloc: cada lloc pot sobreescriure aquests valors predeterminats.','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.'=>'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.','You do not have permission to perform this action.'=>'No tens permís per dur a terme aquesta acció.','Recommended models'=>'Models recomanats','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.'=>'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ó.','Use case'=>'Cas d\'ús','Recommended model'=>'Model recomanat','Why'=>'Per què','These recommendations are based on public benchmarks and community feedback as of the plugin release date. They may change as providers update their models.'=>'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.','European languages (ES, CA, FR, DE, IT, PT)'=>'Llengües europees (ES, CA, FR, DE, IT, PT)','DeepL API Pro'=>'DeepL API Pro','Best fluency and naturalness (92/100 in benchmarks). Formality control and custom glossaries.'=>'La millor fluïdesa i naturalitat (92/100 en benchmarks). Control de formalitat i glossaris personalitzats.','Asian languages (ZH, JA, KO)'=>'Llengües asiàtiques (ZH, JA, KO)','GPT-4o / GPT-5 or Claude Sonnet 4'=>'GPT-4o / GPT-5 o Claude Sonnet 4','Better handling of implicit subjects, honorifics, and cultural references. DeepL falls behind here.'=>'Millor gestió dels subjectes implícits, els honorífics i les referències culturals. DeepL queda enrere aquí.','Marketing / brand tone'=>'Màrqueting / to de marca','Claude Sonnet 4 / Opus 4'=>'Claude Sonnet 4 / Opus 4','Better preservation of tone, brand voice, and nuance. Ideal for creative copy.'=>'Preserva millor el to, la veu de la marca i els matisos. Ideal per a copy creatiu.','Technical documentation / code'=>'Documentació tècnica / codi','GPT-4o / GPT-5'=>'GPT-4o / GPT-5','Higher accuracy with variables, structured formats, and technical terminology.'=>'Major precisió amb variables, formats estructurats i terminologia tècnica.','Long documents (100+ pages)'=>'Documents llargs (més de 100 pàgines)','Gemini 2.5 Pro'=>'Gemini 2.5 Pro','1M token context window. Maintains terminological consistency across long texts.'=>'Finestra de context d\'1 milió de tokens. Manté la consistència terminològica en textos llargs.','High volume / low cost'=>'Alt volum / baix cost','DeepSeek-V3'=>'DeepSeek-V3','Quality comparable to GPT-5 at ~$0.14/M tokens (20-50x cheaper than Claude/GPT).'=>'Qualitat comparable a GPT-5 a ~0,14 $/M tokens (entre 20 i 50 vegades més barat que Claude/GPT).','Rare / indigenous languages'=>'Llengües rares / indígenes','Claude Sonnet or Taskade Translate (multi-model routing)'=>'Claude Sonnet o Taskade Translate (encaminament multimodel)','Better coverage for uncommon language pairs.'=>'Millor cobertura en parells de llengües poc habituals.','Maximum coverage (133+ languages)'=>'Cobertura màxima (més de 133 llengües)','Google Cloud Translation'=>'Google Cloud Translation','The most complete option, though with slightly lower quality on European languages.'=>'L\'opció més completa, tot i que amb una qualitat lleugerament inferior en llengües europees.','The --locale flag is required.'=>'El paràmetre --locale és obligatori.','The --post-id and --post-type flags are mutually exclusive.'=>'Els paràmetres --post-id i --post-type són mútuament excloents.','No user context. Pass --user= so post updates carry an author and capability checks apply.'=>'Sense context d\'usuari. Passa --user= perquè les actualitzacions d\'entrades tinguin autor i s\'apliquin les comprovacions de capacitats.','The WordPress AI plugin is not active.'=>'El connector WordPress AI no està actiu.','The WordPress AI plugin has no provider configured for text generation.'=>'El connector WordPress AI no té cap proveïdor configurat per a la generació de text.','The locale %s is not installed on this site.'=>'La llengua %s no està instal·lada en aquest lloc.','No translation fields are enabled in the plugin settings.'=>'No hi ha cap camp de traducció activat als ajustaments del connector.','No posts matched the given criteria.'=>'Cap entrada coincideix amb els criteris donats.','%1$d translated, %2$d failed (locale: %3$s).'=>'%1$d traduïdes, %2$d amb errors (llengua: %3$s).','Sorry, you are not allowed to edit this post.'=>'Ho sentim, no tens permís per editar aquesta entrada.','Post not found.'=>'Entrada no trobada.','The selected language is not installed on this site.'=>'La llengua seleccionada no està instal·lada en aquest lloc.','No translatable fields were selected, or the requested fields are disabled in the settings.'=>'No s\'ha seleccionat cap camp traduïble, o els camps sol·licitats estan desactivats als ajustaments.','The WordPress AI plugin is not active. Translation is unavailable.'=>'El connector WordPress AI no està actiu. La traducció no està disponible.','No translation fields are enabled in settings.'=>'No hi ha cap camp de traducció activat als ajustaments.','No languages are installed on this site.'=>'No hi ha cap llengua instal·lada en aquest lloc.','Target language'=>'Llengua de destinació','Translate'=>'Tradueix','Save the post before translating to ensure the latest content is used. The translation replaces the current title and/or content.'=>'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.','Translating…'=>'Traduint…','Translation applied. Review and save the post.'=>'Traducció aplicada. Revisa i desa l\'entrada.','Translation failed.'=>'La traducció ha fallat.','You have unsaved changes. Save the post before translating to ensure the latest content is used.'=>'Tens canvis sense desar. Desa l\'entrada abans de traduir perquè s\'utilitzi la versió més recent.','The WordPress AI client is not available. Install and activate the WordPress AI plugin.'=>'El client de WordPress AI no està disponible. Instal·la i activa el connector WordPress AI.','The configured AI providers cannot serve text generation requests. Open the WordPress AI settings to configure a provider.'=>'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.','The target language is not installed on this site.'=>'La llengua de destinació no està instal·lada en aquest lloc.','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.'=>'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.','The AI service returned an unexpected response.'=>'El servei d\'IA ha retornat una resposta inesperada.','AI Translator (by ROBOTSTXT) requires the %s plugin to be active. Translation features will be disabled until it is installed and configured.'=>'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.','WordPress AI'=>'WordPress AI','Security check failed'=>'Comprovació de seguretat fallida','You do not have sufficient permissions to access this page.'=>'No tens permisos suficients per accedir a aquesta pàgina.']]; \ No newline at end of file +return ['domain'=>'robotstxt-ai-translator','plural-forms'=>'nplurals=2; plural=(n != 1);','language'=>'ca','project-id-version'=>'AI Translator (by ROBOTSTXT) 1.1.0','pot-creation-date'=>'2026-05-23T10:07:42+00:00','po-revision-date'=>'2026-05-23 10:30+0000','x-generator'=>'hand-written','messages'=>['AI Translator (by ROBOTSTXT)'=>'AI Translator (by ROBOTSTXT)','ROBOTSTXT'=>'ROBOTSTXT','AI Translator'=>'AI Translator','Settings'=>'Ajustaments','You do not have permission to access this page.'=>'No tens permís per accedir a aquesta pàgina.','AI Translator Settings'=>'Ajustaments d\'AI Translator','Settings saved.'=>'Ajustaments desats.','Translation fields'=>'Camps a traduir','Translate title'=>'Tradueix el títol','Translate content'=>'Tradueix el contingut','enabled'=>'activat','disabled'=>'desactivat','AI Translator Network Settings'=>'Ajustaments de xarxa d\'AI Translator','Network settings saved.'=>'Ajustaments de xarxa desats.','Configuration mode'=>'Mode de configuració','Global configuration: a single setting applies to every site.'=>'Configuració global: un sol ajustament s\'aplica a tots els llocs.','Per-site configuration: each site can override these defaults.'=>'Configuració per lloc: cada lloc pot sobreescriure aquests valors predeterminats.','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.'=>'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.','You do not have permission to perform this action.'=>'No tens permís per dur a terme aquesta acció.','Recommended models'=>'Models recomanats','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.'=>'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ó.','Use case'=>'Cas d\'ús','Recommended model'=>'Model recomanat','Why'=>'Per què','These recommendations are based on public benchmarks and community feedback as of the plugin release date. They may change as providers update their models.'=>'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.','European languages (ES, CA, FR, DE, IT, PT)'=>'Llengües europees (ES, CA, FR, DE, IT, PT)','DeepL API Pro'=>'DeepL API Pro','Best fluency and naturalness (92/100 in benchmarks). Formality control and custom glossaries.'=>'La millor fluïdesa i naturalitat (92/100 en benchmarks). Control de formalitat i glossaris personalitzats.','Asian languages (ZH, JA, KO)'=>'Llengües asiàtiques (ZH, JA, KO)','GPT-4o / GPT-5 or Claude Sonnet 4'=>'GPT-4o / GPT-5 o Claude Sonnet 4','Better handling of implicit subjects, honorifics, and cultural references. DeepL falls behind here.'=>'Millor gestió dels subjectes implícits, els honorífics i les referències culturals. DeepL queda enrere aquí.','Marketing / brand tone'=>'Màrqueting / to de marca','Claude Sonnet 4 / Opus 4'=>'Claude Sonnet 4 / Opus 4','Better preservation of tone, brand voice, and nuance. Ideal for creative copy.'=>'Preserva millor el to, la veu de la marca i els matisos. Ideal per a copy creatiu.','Technical documentation / code'=>'Documentació tècnica / codi','GPT-4o / GPT-5'=>'GPT-4o / GPT-5','Higher accuracy with variables, structured formats, and technical terminology.'=>'Major precisió amb variables, formats estructurats i terminologia tècnica.','Long documents (100+ pages)'=>'Documents llargs (més de 100 pàgines)','Gemini 2.5 Pro'=>'Gemini 2.5 Pro','1M token context window. Maintains terminological consistency across long texts.'=>'Finestra de context d\'1 milió de tokens. Manté la consistència terminològica en textos llargs.','High volume / low cost'=>'Alt volum / baix cost','DeepSeek-V3'=>'DeepSeek-V3','Quality comparable to GPT-5 at ~$0.14/M tokens (20-50x cheaper than Claude/GPT).'=>'Qualitat comparable a GPT-5 a ~0,14 $/M tokens (entre 20 i 50 vegades més barat que Claude/GPT).','Rare / indigenous languages'=>'Llengües rares / indígenes','Claude Sonnet or Taskade Translate (multi-model routing)'=>'Claude Sonnet o Taskade Translate (encaminament multimodel)','Better coverage for uncommon language pairs.'=>'Millor cobertura en parells de llengües poc habituals.','Maximum coverage (133+ languages)'=>'Cobertura màxima (més de 133 llengües)','Google Cloud Translation'=>'Google Cloud Translation','The most complete option, though with slightly lower quality on European languages.'=>'L\'opció més completa, tot i que amb una qualitat lleugerament inferior en llengües europees.','The --locale flag is required.'=>'El paràmetre --locale és obligatori.','The --post-id and --post-type flags are mutually exclusive.'=>'Els paràmetres --post-id i --post-type són mútuament excloents.','No user context. Pass --user= so post updates carry an author and capability checks apply.'=>'Sense context d\'usuari. Passa --user= perquè les actualitzacions d\'entrades tinguin autor i s\'apliquin les comprovacions de capacitats.','The locale %s is not installed on this site.'=>'La llengua %s no està instal·lada en aquest lloc.','No translation fields are enabled in the plugin settings.'=>'No hi ha cap camp de traducció activat als ajustaments del connector.','No posts matched the given criteria.'=>'Cap entrada coincideix amb els criteris donats.','%1$d translated, %2$d failed (locale: %3$s).'=>'%1$d traduïdes, %2$d amb errors (llengua: %3$s).','Sorry, you are not allowed to edit this post.'=>'Ho sentim, no tens permís per editar aquesta entrada.','Post not found.'=>'Entrada no trobada.','The selected language is not installed on this site.'=>'La llengua seleccionada no està instal·lada en aquest lloc.','No translatable fields were selected, or the requested fields are disabled in the settings.'=>'No s\'ha seleccionat cap camp traduïble, o els camps sol·licitats estan desactivats als ajustaments.','No translation fields are enabled in settings.'=>'No hi ha cap camp de traducció activat als ajustaments.','No languages are installed on this site.'=>'No hi ha cap llengua instal·lada en aquest lloc.','Target language'=>'Llengua de destinació','Translate'=>'Tradueix','Save the post before translating to ensure the latest content is used. The translation replaces the current title and/or content.'=>'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.','Translating…'=>'Traduint…','Translation applied. Review and save the post.'=>'Traducció aplicada. Revisa i desa l\'entrada.','Translation failed.'=>'La traducció ha fallat.','You have unsaved changes. Save the post before translating to ensure the latest content is used.'=>'Tens canvis sense desar. Desa l\'entrada abans de traduir perquè s\'utilitzi la versió més recent.','The target language is not installed on this site.'=>'La llengua de destinació no està instal·lada en aquest lloc.','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.'=>'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.','The AI service returned an unexpected response.'=>'El servei d\'IA ha retornat una resposta inesperada.','You do not have sufficient permissions to access this page.'=>'No tens permisos suficients per accedir a aquesta pàgina.','Security check failed.'=>'Error de verificació de seguretat.','Translate excerpt'=>'Traduir l\'extracte','Network defaults: title %1$s, content %2$s, excerpt %3$s.'=>'Valors predeterminats de la xarxa: títol %1$s, contingut %2$s, extracte %3$s.','MultilingualPress'=>'MultilingualPress','Auto-translate new connected posts'=>'Traduir automàticament les entrades connectades noves','When MultilingualPress creates a new connected post, automatically translate its enabled fields into the target site\'s language. The translation runs synchronously during save.'=>'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.','AI features are disabled for this WordPress installation.'=>'Les funcions d\'IA estan desactivades per a aquesta instal·lació de WordPress.','No AI provider is configured for text generation. Open Settings → AI to set one up.'=>'No hi ha cap proveïdor d\'IA configurat per a la generació de text. Obre Ajustos → IA per configurar-ne un.','AI Translator (by ROBOTSTXT) requires a configured AI provider for text generation. Open %s to set one up.'=>'AI Translator (by ROBOTSTXT) requereix un proveïdor d\'IA configurat per a la generació de text. Obre %s per configurar-ne un.','Settings → AI'=>'Ajustos → IA']]; \ No newline at end of file diff --git a/languages/robotstxt-ai-translator-ca.mo b/languages/robotstxt-ai-translator-ca.mo index 8380433d6f9aeecd0a10a44d57b0146dee706a51..6135395ecf38d2bdc2f10d3b0f1dcf07a5241fa8 100644 GIT binary patch delta 2507 zcmZwHeP~s67{KwLGdFc^^DT9$>*t)h>Gg^)_|j0DX1LHfUrNwKkN5tz9lbl}?!3(H zugiK72?E(EFtRer5-K!rVp0?|gIZ!_f%He2l~I&LL13WP_qq3qt_EYD-+6n^^E|)j z+>?)7+I;YV%pRpwaj{ZW*nt!9ATGn#aSC3>YP^X%a9W8{G0b2B-^3;yJzS|txD%IR z7mmfZu?){*DPF)i`0a3&QROT~jvy#jq8!)ba(oCS-~p7xUPgJ*yLb#sMk+NKPoP{s zhZ5%p9D~Dnn-nq;C*nqY7;UV@4@)yj)v)-P6Wg(Dlu~iI0p-C%n8XXX0+&!m1$JUH z?niP_7f}+qjPl|eD3w=GO%GP1B={;)ym}8M&e=?1@fFVH#E*sJv0RlcsV>-n^&IcQ zd3YFOcn&w?-?#|Zk5*~{K8A7I$Vw8eCM!uWRxq=I1<$Hx_B{2jShtzhFF+>X+jew5Z;M}x6@^Pg8QKFINB*o$LzzQUs@TXzjD z9K|Ogd+(sc??Tz~XE7tgaFoRXJdM-we)=gLNTLKvqjca2lom%Q)7+2btS;kB{0CRz zOoA*#yWk7BljARNInHMvSE5@(|Ie~G&WS%Tn2{fn7NSWjU1ZMGlLZfMdTjv{APhD?CSve*V5FzdQfuH@U_{z{R>%&xFr);M+v{JcPQUAy}%VOGeC5)F2^o)fy zeq1$~V4v&ll?(&!^|fzxnk47l0R`HA^i+kOp08>1t$V-U-z#?wC@@=Adc9~!JQ`6x zxqRS#QFZy%>@%agib@J8M9+`w$xzTFBt@U?LY zt+Y)lDeWViDJx<3Wx4bA)WoCrD{B^S@vLNMd--kT2jr1=r;ZZrBt0f$M9Tk;aBWi_dmfbq(FQ#`C0I zMt2&|ZXwBA<@iQEw7?4DDi@zpETk@--{l2%H Z1XC$l$-|ZxSIrDnN7#4ll?8Q0{{n;eMrQy3 delta 2813 zcma*nd2AF_9Ki9{R?AT-&{`!m;BlrD#1nd9Yl{$;b=UMO?VO6Q`M4=Y&;vs;wqF( zH(@7wSb{f@D(+YewIr{FPMhCR3w zOU6Vk*@tWSd=}^7SfZPQEAcSyz)x@-(MlrUp(OA#zJTf6;waL?$lmI8jNuvNPhI6d zo>hOMWH$CrrSfnhQXEx*KVm)3#8S#I2OmU9Y!^O`r%>K6rLT;Qs={A#=>Ie>W^<#A z#y8_?q>Ab-lp%Z%yYLH?UhbX{wc-`5=JQ$Hf?wf$Y@DdnR(uj4#@~=vRRjH#fSQq5 z)Jr&u{nbe>m+xfhO8kKuM-YX4u0v_TLX-t}qbzg~8}S&P#^13PPn0V~OzJb7 zhBxp&tY83mL_LPH*Y%3KXsk|B;jJpmzEw^a&b4#MY^FjBf+Xpl;P{bWq2Ov;)u!7@4{k~ zVLgPB;jbu_D5p;caWl&Es)myS^Kda9!27VLhWfwHMG?DP$4e+f@&Wae()Qpw>*~WtZtrOc+Du*J>!h7(wHzI|y4lo5Jg_>G=h|8`g)sX z+wxGt@{MNOOz4yoShltTZTZ@A{J^l~uHhtf+;v*4R^Cd4ZvMjX>-@4z)v#~!!tn)l z;hKW-%(;S_x#2s7lfrKce@Jg|y+jim_$WLI?DKpP!~Wy@wU-4Jb%T|OJ8 z5tv$X81j0QO-!x3^%uI$@U`RWq~~^63Dc86!#xFM>3@8I)Uts@h54V44XGb1q@007 zvP%rqM{-Vj;gbCPO}>sBj%?_Q#gZDi*FO>MY!$ z2CtrZqIgbj&aBMaC5LmcR2RmLWOf?N?l%&y z7q*v9P5=8_QB1OR>Oa;W{G@I)23P)8xv?$3Jf1thBZ`G?)ix~cD>?%ool-Z!pmWZh zgl95Afz{X9u(NdHB9k?Y%_$n&&ID&0J|t~MZeT?w+8LC5=FsG8xoOkdE)@;Bj|VP4 zWWCjuQkl+Tbh$2nlez\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= so post updates carry an author and capability checks apply." msgstr "Sense context d'usuari. Passa --user= 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" diff --git a/languages/robotstxt-ai-translator-es_ES.l10n.php b/languages/robotstxt-ai-translator-es_ES.l10n.php index c426b35..e182e68 100644 --- a/languages/robotstxt-ai-translator-es_ES.l10n.php +++ b/languages/robotstxt-ai-translator-es_ES.l10n.php @@ -1,2 +1,2 @@ 'robotstxt-ai-translator','plural-forms'=>'nplurals=2; plural=(n != 1);','language'=>'es_ES','project-id-version'=>'AI Translator (by ROBOTSTXT) 1.0.0','pot-creation-date'=>'2026-05-23T10:07:42+00:00','po-revision-date'=>'2026-05-23 10:30+0000','x-generator'=>'hand-written','messages'=>['AI Translator (by ROBOTSTXT)'=>'AI Translator (by ROBOTSTXT)','Translate post titles and content from the editor using the native WordPress AI plugin as backend. Multisite-ready with global or per-site configuration.'=>'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.','ROBOTSTXT'=>'ROBOTSTXT','AI Translator'=>'AI Translator','Settings'=>'Ajustes','You do not have permission to access this page.'=>'No tienes permiso para acceder a esta página.','AI Translator Settings'=>'Ajustes de AI Translator','Settings saved.'=>'Ajustes guardados.','Translation fields'=>'Campos a traducir','Translate title'=>'Traducir título','Translate content'=>'Traducir contenido','Network defaults: title %1$s, content %2$s.'=>'Valores por defecto de la red: título %1$s, contenido %2$s.','enabled'=>'activado','disabled'=>'desactivado','AI Translator Network Settings'=>'Ajustes de red de AI Translator','Network settings saved.'=>'Ajustes de red guardados.','Configuration mode'=>'Modo de configuración','Global configuration: a single setting applies to every site.'=>'Configuración global: un único ajuste se aplica a todos los sitios.','Per-site configuration: each site can override these defaults.'=>'Configuración por sitio: cada sitio puede sobrescribir estos valores por defecto.','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.'=>'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.','You do not have permission to perform this action.'=>'No tienes permiso para realizar esta acción.','Recommended models'=>'Modelos recomendados','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.'=>'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.','Use case'=>'Caso de uso','Recommended model'=>'Modelo recomendado','Why'=>'Por qué','These recommendations are based on public benchmarks and community feedback as of the plugin release date. They may change as providers update their models.'=>'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.','European languages (ES, CA, FR, DE, IT, PT)'=>'Idiomas europeos (ES, CA, FR, DE, IT, PT)','DeepL API Pro'=>'DeepL API Pro','Best fluency and naturalness (92/100 in benchmarks). Formality control and custom glossaries.'=>'La mejor fluidez y naturalidad (92/100 en benchmarks). Control de formalidad y glosarios personalizados.','Asian languages (ZH, JA, KO)'=>'Idiomas asiáticos (ZH, JA, KO)','GPT-4o / GPT-5 or Claude Sonnet 4'=>'GPT-4o / GPT-5 o Claude Sonnet 4','Better handling of implicit subjects, honorifics, and cultural references. DeepL falls behind here.'=>'Mejor manejo de sujetos implícitos, honoríficos y referencias culturales. DeepL queda por detrás aquí.','Marketing / brand tone'=>'Marketing / tono de marca','Claude Sonnet 4 / Opus 4'=>'Claude Sonnet 4 / Opus 4','Better preservation of tone, brand voice, and nuance. Ideal for creative copy.'=>'Preserva mejor el tono, la voz de marca y los matices. Ideal para copy creativo.','Technical documentation / code'=>'Documentación técnica / código','GPT-4o / GPT-5'=>'GPT-4o / GPT-5','Higher accuracy with variables, structured formats, and technical terminology.'=>'Mayor precisión con variables, formatos estructurados y terminología técnica.','Long documents (100+ pages)'=>'Documentos largos (más de 100 páginas)','Gemini 2.5 Pro'=>'Gemini 2.5 Pro','1M token context window. Maintains terminological consistency across long texts.'=>'Ventana de contexto de 1 millón de tokens. Mantiene la consistencia terminológica en textos extensos.','High volume / low cost'=>'Alto volumen / bajo coste','DeepSeek-V3'=>'DeepSeek-V3','Quality comparable to GPT-5 at ~$0.14/M tokens (20-50x cheaper than Claude/GPT).'=>'Calidad comparable a GPT-5 a ~0,14 $/M tokens (entre 20 y 50 veces más barato que Claude/GPT).','Rare / indigenous languages'=>'Idiomas raros / indígenas','Claude Sonnet or Taskade Translate (multi-model routing)'=>'Claude Sonnet o Taskade Translate (enrutado multimodelo)','Better coverage for uncommon language pairs.'=>'Mejor cobertura en pares de idiomas poco comunes.','Maximum coverage (133+ languages)'=>'Cobertura máxima (más de 133 idiomas)','Google Cloud Translation'=>'Google Cloud Translation','The most complete option, though with slightly lower quality on European languages.'=>'La opción más completa, aunque con calidad ligeramente inferior en idiomas europeos.','The --locale flag is required.'=>'El parámetro --locale es obligatorio.','The --post-id and --post-type flags are mutually exclusive.'=>'Los parámetros --post-id y --post-type son mutuamente excluyentes.','No user context. Pass --user= so post updates carry an author and capability checks apply.'=>'Sin contexto de usuario. Pasa --user= para que las actualizaciones de entradas tengan autor y se apliquen los controles de capacidades.','The WordPress AI plugin is not active.'=>'El plugin WordPress AI no está activo.','The WordPress AI plugin has no provider configured for text generation.'=>'El plugin WordPress AI no tiene ningún proveedor configurado para generación de texto.','The locale %s is not installed on this site.'=>'El idioma %s no está instalado en este sitio.','No translation fields are enabled in the plugin settings.'=>'No hay ningún campo de traducción activado en los ajustes del plugin.','No posts matched the given criteria.'=>'Ninguna entrada coincide con los criterios indicados.','%1$d translated, %2$d failed (locale: %3$s).'=>'%1$d traducidas, %2$d con fallos (idioma: %3$s).','Sorry, you are not allowed to edit this post.'=>'Lo sentimos, no tienes permiso para editar esta entrada.','Post not found.'=>'Entrada no encontrada.','The selected language is not installed on this site.'=>'El idioma seleccionado no está instalado en este sitio.','No translatable fields were selected, or the requested fields are disabled in the settings.'=>'No se ha seleccionado ningún campo traducible, o los campos solicitados están desactivados en los ajustes.','The WordPress AI plugin is not active. Translation is unavailable.'=>'El plugin WordPress AI no está activo. La traducción no está disponible.','No translation fields are enabled in settings.'=>'No hay ningún campo de traducción activado en los ajustes.','No languages are installed on this site.'=>'No hay ningún idioma instalado en este sitio.','Target language'=>'Idioma de destino','Translate'=>'Traducir','Save the post before translating to ensure the latest content is used. The translation replaces the current title and/or content.'=>'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.','Translating…'=>'Traduciendo…','Translation applied. Review and save the post.'=>'Traducción aplicada. Revisa y guarda la entrada.','Translation failed.'=>'La traducción ha fallado.','You have unsaved changes. Save the post before translating to ensure the latest content is used.'=>'Tienes cambios sin guardar. Guarda la entrada antes de traducir para que se use la versión más reciente.','The WordPress AI client is not available. Install and activate the WordPress AI plugin.'=>'El cliente de WordPress AI no está disponible. Instala y activa el plugin WordPress AI.','The configured AI providers cannot serve text generation requests. Open the WordPress AI settings to configure a provider.'=>'Los proveedores de IA configurados no pueden atender peticiones de generación de texto. Abre los ajustes de WordPress AI para configurar un proveedor.','The target language is not installed on this site.'=>'El idioma de destino no está instalado en este sitio.','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.'=>'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.','The AI service returned an unexpected response.'=>'El servicio de IA ha devuelto una respuesta inesperada.','AI Translator (by ROBOTSTXT) requires the %s plugin to be active. Translation features will be disabled until it is installed and configured.'=>'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.','WordPress AI'=>'WordPress AI','Security check failed'=>'Comprobación de seguridad fallida','You do not have sufficient permissions to access this page.'=>'No tienes permisos suficientes para acceder a esta página.']]; \ No newline at end of file +return ['domain'=>'robotstxt-ai-translator','plural-forms'=>'nplurals=2; plural=(n != 1);','language'=>'es_ES','project-id-version'=>'AI Translator (by ROBOTSTXT) 1.1.0','pot-creation-date'=>'2026-05-23T10:07:42+00:00','po-revision-date'=>'2026-05-23 10:30+0000','x-generator'=>'hand-written','messages'=>['AI Translator (by ROBOTSTXT)'=>'AI Translator (by ROBOTSTXT)','ROBOTSTXT'=>'ROBOTSTXT','AI Translator'=>'AI Translator','Settings'=>'Ajustes','You do not have permission to access this page.'=>'No tienes permiso para acceder a esta página.','AI Translator Settings'=>'Ajustes de AI Translator','Settings saved.'=>'Ajustes guardados.','Translation fields'=>'Campos a traducir','Translate title'=>'Traducir título','Translate content'=>'Traducir contenido','enabled'=>'activado','disabled'=>'desactivado','AI Translator Network Settings'=>'Ajustes de red de AI Translator','Network settings saved.'=>'Ajustes de red guardados.','Configuration mode'=>'Modo de configuración','Global configuration: a single setting applies to every site.'=>'Configuración global: un único ajuste se aplica a todos los sitios.','Per-site configuration: each site can override these defaults.'=>'Configuración por sitio: cada sitio puede sobrescribir estos valores por defecto.','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.'=>'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.','You do not have permission to perform this action.'=>'No tienes permiso para realizar esta acción.','Recommended models'=>'Modelos recomendados','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.'=>'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.','Use case'=>'Caso de uso','Recommended model'=>'Modelo recomendado','Why'=>'Por qué','These recommendations are based on public benchmarks and community feedback as of the plugin release date. They may change as providers update their models.'=>'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.','European languages (ES, CA, FR, DE, IT, PT)'=>'Idiomas europeos (ES, CA, FR, DE, IT, PT)','DeepL API Pro'=>'DeepL API Pro','Best fluency and naturalness (92/100 in benchmarks). Formality control and custom glossaries.'=>'La mejor fluidez y naturalidad (92/100 en benchmarks). Control de formalidad y glosarios personalizados.','Asian languages (ZH, JA, KO)'=>'Idiomas asiáticos (ZH, JA, KO)','GPT-4o / GPT-5 or Claude Sonnet 4'=>'GPT-4o / GPT-5 o Claude Sonnet 4','Better handling of implicit subjects, honorifics, and cultural references. DeepL falls behind here.'=>'Mejor manejo de sujetos implícitos, honoríficos y referencias culturales. DeepL queda por detrás aquí.','Marketing / brand tone'=>'Marketing / tono de marca','Claude Sonnet 4 / Opus 4'=>'Claude Sonnet 4 / Opus 4','Better preservation of tone, brand voice, and nuance. Ideal for creative copy.'=>'Preserva mejor el tono, la voz de marca y los matices. Ideal para copy creativo.','Technical documentation / code'=>'Documentación técnica / código','GPT-4o / GPT-5'=>'GPT-4o / GPT-5','Higher accuracy with variables, structured formats, and technical terminology.'=>'Mayor precisión con variables, formatos estructurados y terminología técnica.','Long documents (100+ pages)'=>'Documentos largos (más de 100 páginas)','Gemini 2.5 Pro'=>'Gemini 2.5 Pro','1M token context window. Maintains terminological consistency across long texts.'=>'Ventana de contexto de 1 millón de tokens. Mantiene la consistencia terminológica en textos extensos.','High volume / low cost'=>'Alto volumen / bajo coste','DeepSeek-V3'=>'DeepSeek-V3','Quality comparable to GPT-5 at ~$0.14/M tokens (20-50x cheaper than Claude/GPT).'=>'Calidad comparable a GPT-5 a ~0,14 $/M tokens (entre 20 y 50 veces más barato que Claude/GPT).','Rare / indigenous languages'=>'Idiomas raros / indígenas','Claude Sonnet or Taskade Translate (multi-model routing)'=>'Claude Sonnet o Taskade Translate (enrutado multimodelo)','Better coverage for uncommon language pairs.'=>'Mejor cobertura en pares de idiomas poco comunes.','Maximum coverage (133+ languages)'=>'Cobertura máxima (más de 133 idiomas)','Google Cloud Translation'=>'Google Cloud Translation','The most complete option, though with slightly lower quality on European languages.'=>'La opción más completa, aunque con calidad ligeramente inferior en idiomas europeos.','The --locale flag is required.'=>'El parámetro --locale es obligatorio.','The --post-id and --post-type flags are mutually exclusive.'=>'Los parámetros --post-id y --post-type son mutuamente excluyentes.','No user context. Pass --user= so post updates carry an author and capability checks apply.'=>'Sin contexto de usuario. Pasa --user= para que las actualizaciones de entradas tengan autor y se apliquen los controles de capacidades.','The locale %s is not installed on this site.'=>'El idioma %s no está instalado en este sitio.','No translation fields are enabled in the plugin settings.'=>'No hay ningún campo de traducción activado en los ajustes del plugin.','No posts matched the given criteria.'=>'Ninguna entrada coincide con los criterios indicados.','%1$d translated, %2$d failed (locale: %3$s).'=>'%1$d traducidas, %2$d con fallos (idioma: %3$s).','Sorry, you are not allowed to edit this post.'=>'Lo sentimos, no tienes permiso para editar esta entrada.','Post not found.'=>'Entrada no encontrada.','The selected language is not installed on this site.'=>'El idioma seleccionado no está instalado en este sitio.','No translatable fields were selected, or the requested fields are disabled in the settings.'=>'No se ha seleccionado ningún campo traducible, o los campos solicitados están desactivados en los ajustes.','No translation fields are enabled in settings.'=>'No hay ningún campo de traducción activado en los ajustes.','No languages are installed on this site.'=>'No hay ningún idioma instalado en este sitio.','Target language'=>'Idioma de destino','Translate'=>'Traducir','Save the post before translating to ensure the latest content is used. The translation replaces the current title and/or content.'=>'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.','Translating…'=>'Traduciendo…','Translation applied. Review and save the post.'=>'Traducción aplicada. Revisa y guarda la entrada.','Translation failed.'=>'La traducción ha fallado.','You have unsaved changes. Save the post before translating to ensure the latest content is used.'=>'Tienes cambios sin guardar. Guarda la entrada antes de traducir para que se use la versión más reciente.','The target language is not installed on this site.'=>'El idioma de destino no está instalado en este sitio.','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.'=>'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.','The AI service returned an unexpected response.'=>'El servicio de IA ha devuelto una respuesta inesperada.','You do not have sufficient permissions to access this page.'=>'No tienes permisos suficientes para acceder a esta página.','Security check failed.'=>'Error de verificación de seguridad.','Translate excerpt'=>'Traducir extracto','Network defaults: title %1$s, content %2$s, excerpt %3$s.'=>'Valores predeterminados de la red: título %1$s, contenido %2$s, extracto %3$s.','MultilingualPress'=>'MultilingualPress','Auto-translate new connected posts'=>'Traducir automáticamente las entradas conectadas nuevas','When MultilingualPress creates a new connected post, automatically translate its enabled fields into the target site\'s language. The translation runs synchronously during save.'=>'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.','AI features are disabled for this WordPress installation.'=>'Las funciones de IA están desactivadas para esta instalación de WordPress.','No AI provider is configured for text generation. Open Settings → AI to set one up.'=>'No hay ningún proveedor de IA configurado para la generación de texto. Abre Ajustes → IA para configurar uno.','AI Translator (by ROBOTSTXT) requires a configured AI provider for text generation. Open %s to set one up.'=>'AI Translator (by ROBOTSTXT) requiere un proveedor de IA configurado para la generación de texto. Abre %s para configurar uno.','Settings → AI'=>'Ajustes → IA']]; \ No newline at end of file diff --git a/languages/robotstxt-ai-translator-es_ES.mo b/languages/robotstxt-ai-translator-es_ES.mo index 74a970791593c142161025827bf0d835eb00f38e..db0ef73c925d203d49ea91d6778d27f7ed5c9b4c 100644 GIT binary patch delta 2581 zcmbumzzKA=p{I1;lNu*uWAl@jT{(BfK;zo)B zb>T@QdukSim*$&?R7tJE`)~;PQhO+QZq#aZe>+xxn%p60Fr}Ed|;~K87pafjNZdp^UMp@`d{1`t#X}0*i zx## zFRZe3%yl~LuFx}a-O{R)p4)H7ji)aJ#?iID4qWYi2~*dGiNH?SPFH9pva!-%O0c}71T)Cc`JJW{Ixxel^D6_^LFjHD`y|5sf zo?JG&Z2Y`*r0nm^%M}BK#rZ9yU!T4wMUHOSsg8N%N3wI9<27iD4D^zb7>OCwH&bB; zzBW!im9|O5C4Foz=r%gAye^91+ku(y>x3m~=`vBhWTQOHXOpsNFLX%4pcCu%T*nQ4 zR)~ilsmW$0smqgz|Xy2XmSI%#

*hNoW)Ec?nI&E0Ms>?hBG4<@S))y6c?5OSd#jeWw_CpM44oJ|BrUn<#t(*$N)X?Q z1$Ms`XHNEg9$5LtvBnk}YpH1Zbk&^Zjh+<`W44z|=Fm~eX0LH*s03jc@SFi#p|P?< zCp7(*pZU2edPn^&6;qq7ge(1=bg?tIpW1AMeUOG>gq;IKAiuN zsFt44m-dkUBh9x-T4qP}feGu@cwVj+{l>F9IpJ6~e8Wd<$1OTNA8b@jV|YAp`8dX} zlWB}p7G(adIa63d-bRmKko(Oe~}!Xd3>%@TNcXz7wOl XEs2&ptrt}tWU4P5dOK56UsCuVgcx9@ delta 2797 zcma*ndx#ZP9Ki9jYpv$>vAV6Ux~|8y^|kKxf#%vnFf(0zr)U^rvv=%=w+) z-6MjQis_6$h}pDfkfX!FD{0gV?Q3H| z!5A8!#z&AUiW8`Z@C(#Cy^JI9=^^oqyp8()BV3E$;$)mVRETxhj!W<&@~N0c{~B=- z>e+e*`x0O5<$`$-hj1Kzh`JINP~xwJ`+@9c4)-HbFPKJRip{74?Z)@(a0ed2hjG4)GuVr3xIc`#x0M_P zk`*IRx9lFID`FvT!)C0-PSh=^;3lVE9L9w%O%nBRq>*<-JL=(k4QJy?oQ`FqXcNvX zxC?cLoj4z>ICHzuL_JG?;R>v;i{os?ySYDBNBvK5@f!~=VV-DuNcOUt-q<_%9v;PV zTu)tfYo0>g>*r8sd>r-0ze3&0OV|tlKs}`YU?=vWzj{MgQ6I|*<7GUhtH#GevlI0K zCH3(Fee1(jx1VbX)+$-&%Z!~(TaFA|*{r0I3amDjl*>KC@ogipTt~JjBglHnmm4kH z)-RcsZ#3J=lvyXRY-t73@}=eYfnn>XhGWW<>$F&DzA{6%Y({vYtU5ok=fxi3;KcZF zabkG>Wa3I`c(mWJ@P~evb1PlXTtWa}&T5h=+fq)nqT>eAXfrHZ2TRJPI6$;~v`r&W zQs>a^^EjKNSbp;#^s+h&={(;E#pC8~4(`2nd#8;J{Zi?4O7A1h>?Yl%cl zT&s`HIpc*h%gWaHGG#bAn6JDxa#N27GOZlt#eMM9hOF`fKPeYy6b-vELth1f<)nSx z%HsOcDCU|!-G5VQF5~)v46MMWp@mKby5KFI+ZwH@Oe=6bne|yKdf*VA?cAiyl43R+ zsrAY+lX79!4lLgaRD-7svz=)P*2=W)HXF8Ny^Qi2__bUW_ZDT93>OdF(yv`yzj}Ua z#gx*LN%_N-2TOB~N-4fGB*{x|bHMt(Ov)xuki<(4X*@dq-1(s=;vW3&ZMd;R6=Xcjul+27L zo#S*vZ5dCU+CF{GGOlA?H~j|Ni}x?XGj)Tdm{Tms^##-u9&n6QxU{CCf7dJ+CUb9i z;R{uRbN};U96O5QCU5mU%HOTvH_JgRaH$*zPpL*TDkGUU%_`QL{l#Z9t4!vb}vR8Rdu7yd&at~AH\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= so post updates carry an author and capability checks apply." msgstr "Sin contexto de usuario. Pasa --user= 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" diff --git a/languages/robotstxt-ai-translator.pot b/languages/robotstxt-ai-translator.pot index a6c55c7..0d57159 100644 --- a/languages/robotstxt-ai-translator.pot +++ b/languages/robotstxt-ai-translator.pot @@ -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 \n" "Language-Team: LANGUAGE \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= 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= 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 "" diff --git a/readme.txt b/readme.txt index 64adf1e..669f59e 100644 --- a/readme.txt +++ b/readme.txt @@ -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. diff --git a/robotstxt-ai-translator.php b/robotstxt-ai-translator.php index 92aaf60..03c02c4 100644 --- a/robotstxt-ai-translator.php +++ b/robotstxt-ai-translator.php @@ -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' ), - '' . esc_html__( 'WordPress AI', 'robotstxt-ai-translator' ) . '' + /* 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' ), + '' . esc_html__( 'Settings → AI', 'robotstxt-ai-translator' ) . '' ); printf( @@ -135,9 +151,7 @@ function ai_translator_dependency_notice() { $message, array( 'a' => array( - 'href' => array(), - 'target' => array(), - 'rel' => array(), + 'href' => array(), ), ) ) diff --git a/robotstxt-updater.php b/robotstxt-updater.php deleted file mode 100644 index d5ce9ab..0000000 --- a/robotstxt-updater.php +++ /dev/null @@ -1,383 +0,0 @@ -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' ); - } - } -} diff --git a/update.json b/update.json index 8243207..efe6a82 100644 --- a/update.json +++ b/update.json @@ -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": "

1.0.0 - 2026-05-23

  • Added: Block editor sidebar and Classic editor metabox with language selector and Translate button.
  • Added: REST endpoint POST /wp-json/ai-translator/v1/translate.
  • Added: WP-CLI command wp ai-translator translate for single-post and bulk translation.
  • Added: Network admin settings page with global / per-site configuration mode.
  • Added: Site admin settings page with title and content translation toggles.
  • Added: Model recommendations table in the settings pages.
", + "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": "

1.1.0 - 2026-05-25

  • Added: Excerpt translation — post_excerpt is now a translatable field with its own settings toggle.
  • Added: MultilingualPress integration — auto-translate new connected posts at creation time (opt-in).
  • Changed: Removed Requires Plugins: ai header — wp_ai_client_prompt() is native to WordPress 7.0; works with any configured AI provider.
  • Changed: Admin notice now fires when no text-generation provider is configured.

1.0.0 - 2026-05-23

  • Added: Block editor sidebar and Classic editor metabox with language selector and Translate button.
  • Added: REST endpoint POST /wp-json/ai-translator/v1/translate.
  • Added: WP-CLI command wp ai-translator translate for single-post and bulk translation.
  • Added: Network admin settings page with global / per-site configuration mode.
  • Added: Site admin settings page with title and content translation toggles.
  • Added: Model recommendations table in the settings pages.
", "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": "

1.0.0 - 2026-05-23

  • Added: Block editor sidebar and Classic editor metabox with language selector and Translate button.
  • Added: REST endpoint POST /wp-json/ai-translator/v1/translate.
  • Added: WP-CLI command wp ai-translator translate for single-post and bulk translation.
  • Added: Network admin settings page with global / per-site configuration mode.
  • Added: Site admin settings page with title and content translation toggles.
  • Added: Model recommendations table in the settings pages.
" + "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": "

1.1.0 - 2026-05-25

  • Added: Excerpt translation — post_excerpt is now a translatable field with its own settings toggle.
  • Added: MultilingualPress integration — auto-translate new connected posts at creation time (opt-in).
  • Changed: Removed Requires Plugins: ai header — wp_ai_client_prompt() is native to WordPress 7.0; works with any configured AI provider.
  • Changed: Admin notice now fires when no text-generation provider is configured.

1.0.0 - 2026-05-23

  • Added: Block editor sidebar and Classic editor metabox with language selector and Translate button.
  • Added: REST endpoint POST /wp-json/ai-translator/v1/translate.
  • Added: WP-CLI command wp ai-translator translate for single-post and bulk translation.
  • Added: Network admin settings page with global / per-site configuration mode.
  • Added: Site admin settings page with title and content translation toggles.
  • Added: Model recommendations table in the settings pages.
" }, "banners": { "low": "",