Plugin Directory

Changeset 3402727


Ignore:
Timestamp:
11/25/2025 05:25:05 PM (3 months ago)
Author:
alttextai
Message:

Update to version 1.10.15 - Improved WPML translation processing

Location:
alttext-ai/trunk
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • alttext-ai/trunk/README.txt

    r3373984 r3402727  
    66Requires at least: 4.7
    77Tested up to: 6.8
    8 Stable tag: 1.10.14
     8Stable tag: 1.10.15
    99WC requires at least: 3.3
    1010WC tested up to: 10.1
     
    7070== Changelog ==
    7171
    72 = 1.10.14 - 2025-10-06 =
    73 * Fixed: ChatGPT prompts with special characters (β, →, ₂) now save correctly
    74 * Improved: Real-time error messages when ChatGPT prompts are invalid
    75 * Improved: Live character counter shows remaining space for ChatGPT prompts
     72= 1.10.15 - 2025-11-25 =
     73* Improved: WPML translations now process automatically when generating alt text for primary images
     74* Improved: Bulk generation prevents double-processing of WPML translated images
     75* Improved: Better error handling and tracking for multilingual alt text generation
    7676
    7777= 1.10.13 - 2025-10-06 =
  • alttext-ai/trunk/admin/class-atai-settings.php

    r3373984 r3402727  
    558558   * @param string $input The text of the GPT prompt.
    559559   *
    560    * @return string Returns the prompt string if valid, otherwise returns the old value with an error message.
     560   * @return string Returns the prompt string if valid, otherwise an empty string.
    561561   */
    562562  public function sanitize_gpt_prompt( $input ) {
    563     // Use mb_strlen to count characters, not bytes, for UTF-8 consistency with HTML maxlength
    564     // Empty strings are allowed (user wants to clear the prompt)
    565     if ( empty( trim( $input ) ) ) {
     563    if ( strlen($input) > 512 || strpos($input, "{{AltText}}") === false ) {
    566564      return '';
    567565    }
    568 
    569     // Get old value to preserve it if validation fails
    570     $old_value = get_option( 'atai_gpt_prompt', '' );
    571 
    572     // Check if prompt is too long (character count, not bytes)
    573     $char_count = mb_strlen( $input, 'UTF-8' );
    574     if ( $char_count > 512 ) {
    575       add_settings_error(
    576         'atai_gpt_prompt',
    577         'atai_gpt_prompt_too_long',
    578         sprintf(
    579           __( 'ChatGPT prompt is too long (%d characters). Maximum is 512 characters.', 'alttext-ai' ),
    580           $char_count
    581         ),
    582         'error'
    583       );
    584       return $old_value;
    585     }
    586 
    587     // Check if prompt contains the required {{AltText}} macro
    588     if ( strpos( $input, '{{AltText}}' ) === false ) {
    589       add_settings_error(
    590         'atai_gpt_prompt',
    591         'atai_gpt_prompt_missing_macro',
    592         __( 'ChatGPT prompt must include the {{AltText}} macro, which will be replaced with the generated alt text.', 'alttext-ai' ),
    593         'error'
    594       );
    595       return $old_value;
    596     }
    597 
    598     return sanitize_textarea_field( $input );
     566    else {
     567      return sanitize_textarea_field($input);
     568    }
    599569  }
    600570
  • alttext-ai/trunk/admin/js/admin.js

    r3373984 r3402727  
    18821882    setTimeout(extendMediaTemplate, 500);
    18831883  });
    1884 
    1885   /**
    1886    * Real-time ChatGPT prompt validation
    1887    */
    1888   document.addEventListener('DOMContentLoaded', () => {
    1889     const promptTextarea = document.getElementById('atai_gpt_prompt');
    1890     const errorsContainer = document.getElementById('atai_gpt_prompt_errors');
    1891     const missingMacroError = document.getElementById('atai_gpt_prompt_error_missing_macro');
    1892     const tooLongError = document.getElementById('atai_gpt_prompt_error_too_long');
    1893     const charCountSpan = document.getElementById('atai_gpt_prompt_char_count');
    1894     const charCounterSpan = document.getElementById('atai_gpt_prompt_char_counter');
    1895 
    1896     if (!promptTextarea || !errorsContainer) {
    1897       return; // Not on settings page
    1898     }
    1899 
    1900     function validatePrompt() {
    1901       const value = promptTextarea.value;
    1902       const charCount = value.length; // JavaScript .length counts UTF-16 code units, similar to mb_strlen
    1903       const hasMacro = value.includes('{{AltText}}');
    1904       const isTooLong = charCount > 512;
    1905       const isEmpty = value.trim() === '';
    1906 
    1907       // Update character counter
    1908       if (charCounterSpan) {
    1909         charCounterSpan.textContent = `${charCount}/512`;
    1910 
    1911         // Update counter color based on length
    1912         if (isTooLong) {
    1913           charCounterSpan.classList.remove('text-gray-400');
    1914           charCounterSpan.classList.add('text-red-500', 'font-medium');
    1915         } else if (charCount > 400) {
    1916           charCounterSpan.classList.remove('text-gray-400', 'text-red-500');
    1917           charCounterSpan.classList.add('text-amber-500', 'font-medium');
    1918         } else {
    1919           charCounterSpan.classList.remove('text-red-500', 'text-amber-500', 'font-medium');
    1920           charCounterSpan.classList.add('text-gray-400');
    1921         }
    1922       }
    1923 
    1924       // Reset state
    1925       let hasErrors = false;
    1926       missingMacroError.classList.add('hidden');
    1927       tooLongError.classList.add('hidden');
    1928 
    1929       // Check for missing macro (only if not empty)
    1930       if (!isEmpty && !hasMacro) {
    1931         missingMacroError.classList.remove('hidden');
    1932         hasErrors = true;
    1933       }
    1934 
    1935       // Check for length
    1936       if (isTooLong) {
    1937         charCountSpan.textContent = charCount;
    1938         tooLongError.classList.remove('hidden');
    1939         hasErrors = true;
    1940       }
    1941 
    1942       // Show/hide errors container
    1943       if (hasErrors) {
    1944         errorsContainer.classList.remove('hidden');
    1945       } else {
    1946         errorsContainer.classList.add('hidden');
    1947       }
    1948 
    1949       // Update textarea appearance
    1950       if (hasErrors) {
    1951         promptTextarea.classList.remove('ring-gray-300', 'focus:ring-primary-600');
    1952         promptTextarea.classList.add('ring-red-300', 'focus:ring-red-600');
    1953       } else {
    1954         promptTextarea.classList.remove('ring-red-300', 'focus:ring-red-600');
    1955         promptTextarea.classList.add('ring-gray-300', 'focus:ring-primary-600');
    1956       }
    1957     }
    1958 
    1959     // Validate on input (real-time)
    1960     promptTextarea.addEventListener('input', validatePrompt);
    1961 
    1962     // Validate on paste
    1963     promptTextarea.addEventListener('paste', () => {
    1964       // Use setTimeout to validate after paste content is inserted
    1965       setTimeout(validatePrompt, 0);
    1966     });
    1967 
    1968     // Initial validation on page load
    1969     validatePrompt();
    1970   });
    19711884})();
  • alttext-ai/trunk/admin/partials/settings.php

    r3373984 r3402727  
    503503                      placeholder="example: Rewrite the following text in the style of Shakespeare: {{AltText}}"
    504504                    ><?php echo esc_html ( get_option( 'atai_gpt_prompt' ) ); ?></textarea>
    505 
    506                     <!-- Real-time validation error container -->
    507                     <div id="atai_gpt_prompt_errors" class="mt-2 hidden">
    508                       <div id="atai_gpt_prompt_error_missing_macro" class="p-3 bg-red-50 border border-red-200 rounded-md text-red-700 text-sm hidden">
    509                         <strong><?php esc_html_e( 'Missing required macro:', 'alttext-ai' ); ?></strong>
    510                         <?php esc_html_e( 'Your prompt must include {{AltText}} which will be replaced with the generated alt text.', 'alttext-ai' ); ?>
    511                       </div>
    512                       <div id="atai_gpt_prompt_error_too_long" class="p-3 bg-red-50 border border-red-200 rounded-md text-red-700 text-sm hidden">
    513                         <strong><?php esc_html_e( 'Prompt too long:', 'alttext-ai' ); ?></strong>
    514                         <span id="atai_gpt_prompt_char_count">0</span> <?php esc_html_e( 'characters. Maximum is 512 characters.', 'alttext-ai' ); ?>
    515                       </div>
    516                     </div>
    517505                  </div>
    518506                  <p class="mt-1 text-gray-500">
    519507                    <?php esc_html_e( 'Your prompt MUST include the macro {{AltText}}, which will be substituted with the generated alt text, then sent to ChatGPT.', 'alttext-ai' ); ?>
    520                     <span id="atai_gpt_prompt_char_counter" class="float-right text-xs text-gray-400">0/512</span>
    521508                  </p>
    522509                </div>
  • alttext-ai/trunk/atai.php

    r3373984 r3402727  
    1616 * Plugin URI:        https://alttext.ai/product
    1717 * Description:       Automatically generate image alt text with AltText.ai.
    18  * Version:           1.10.14
     18 * Version:           1.10.15
    1919 * Author:            AltText.ai
    2020 * Author URI:        https://alttext.ai
     
    3434 * Current plugin version.
    3535 */
    36 define( 'ATAI_VERSION', '1.10.14' );
     36define( 'ATAI_VERSION', '1.10.15' );
    3737
    3838/**
  • alttext-ai/trunk/changelog.txt

    r3373984 r3402727  
    11*** AltText.ai Changelog ***
    22
    3 2025-10-06 - version 1.10.14
    4 * Fixed: ChatGPT prompts with special characters (β, →, ₂) now save correctly
    5 * Improved: Real-time error messages when ChatGPT prompts are invalid
    6 * Improved: Live character counter shows remaining space for ChatGPT prompts
     32025-11-25 - version 1.10.15
     4* Improved: WPML translations now process automatically when generating alt text for primary images
     5* Improved: Bulk generation prevents double-processing of WPML translated images
     6* Improved: Better error handling and tracking for multilingual alt text generation
    77
    882025-10-06 - version 1.10.13
  • alttext-ai/trunk/includes/class-atai-attachment.php

    r3371885 r3402727  
    10731073    }
    10741074
     1075    // Generate alt text for primary attachment
    10751076    $this->generate_alt( $attachment_id );
    10761077
    1077     // Generate alt text for WPML translated versions in their respective languages
     1078    // Process WPML translations if applicable
     1079    $this->process_wpml_translations( $attachment_id );
     1080  }
     1081
     1082  /**
     1083   * Process WPML translations for an attachment.
     1084   * Returns success/skipped counts and processed IDs for double-processing prevention.
     1085   *
     1086   * @since 1.11.0
     1087   */
     1088  private function process_wpml_translations( $attachment_id, $options = array() ) {
     1089    $results = array(
     1090      'success'       => 0,
     1091      'skipped'       => 0,
     1092      'processed_ids' => array(),
     1093    );
     1094
    10781095    if ( ! ATAI_Utility::has_wpml() ) {
    1079       return;
     1096      return $results;
    10801097    }
    10811098
    10821099    $active_languages = apply_filters( 'wpml_active_languages', NULL );
    1083 
    1084     // Guard against WPML returning null/false
    10851100    if ( empty( $active_languages ) || ! is_array( $active_languages ) ) {
    1086       return;
    1087     }
    1088 
    1089     $language_codes = array_keys( $active_languages );
    1090 
    1091     foreach ( $language_codes as $lang ) {
    1092       $translated_attachment_id = apply_filters( 'wpml_object_id', $attachment_id, 'attachment', FALSE, $lang );
    1093 
    1094       // Ensure translated attachment exists, is different, is actually an attachment, and not trashed
    1095       if ( ! $translated_attachment_id || $translated_attachment_id === $attachment_id ) {
     1101      return $results;
     1102    }
     1103
     1104    foreach ( array_keys( $active_languages ) as $lang ) {
     1105      $translated_id = apply_filters( 'wpml_object_id', $attachment_id, 'attachment', FALSE, $lang );
     1106
     1107      // Skip source and non-existent translations
     1108      if ( ! $translated_id || (int) $translated_id === (int) $attachment_id ) {
    10961109        continue;
    10971110      }
    10981111
    1099       $translated_post_type = get_post_type( $translated_attachment_id );
    1100       $translated_post_status = get_post_status( $translated_attachment_id );
    1101 
    1102       if ( 'attachment' !== $translated_post_type || 'trash' === $translated_post_status ) {
     1112      // Skip invalid or trashed
     1113      if ( get_post_type( $translated_id ) !== 'attachment' || get_post_status( $translated_id ) === 'trash' ) {
     1114        $results['skipped']++;
     1115        $results['processed_ids'][ $translated_id ] = 'skipped';
    11031116        continue;
    11041117      }
    11051118
    1106       // Pass language explicitly to avoid timing issues with WPML metadata
    1107       // Note: force_lang setting is enforced inside generate_alt() if enabled
    1108       $this->generate_alt( $translated_attachment_id, null, array( 'lang' => $lang ) );
    1109     }
     1119      $response = $this->generate_alt( $translated_id, null, array_merge( $options, array( 'lang' => $lang ) ) );
     1120
     1121      if ( $this->is_generation_error( $response ) ) {
     1122        $results['skipped']++;
     1123        $results['processed_ids'][ $translated_id ] = 'error';
     1124      } else {
     1125        $results['success']++;
     1126        $results['processed_ids'][ $translated_id ] = 'success';
     1127      }
     1128    }
     1129
     1130    return $results;
     1131  }
     1132
     1133  /**
     1134   * Check if a generate_alt response is an error.
     1135   *
     1136   * @since 1.11.0
     1137   */
     1138  private function is_generation_error( $response ) {
     1139    if ( is_wp_error( $response ) ) {
     1140      return true;
     1141    }
     1142    if ( ! is_string( $response ) || $response === '' ) {
     1143      return true;
     1144    }
     1145    return (
     1146      0 === strpos( $response, 'error_' ) ||
     1147      0 === strpos( $response, 'invalid_' ) ||
     1148      in_array( $response, array( 'insufficient_credits', 'url_access_error' ), true )
     1149    );
    11101150  }
    11111151
     
    11561196    $images_successful = $images_skipped = $loop_count = 0;
    11571197    $processed_ids = array(); // Track processed IDs for bulk-select cleanup
     1198    $wpml_processed_ids = array(); // Track WPML translation IDs to prevent double-processing
    11581199   
    11591200   
     
    12941335
    12951336    foreach ( $images_to_update as $image ) {
    1296       $attachment_id = ( $mode === 'bulk-select' ) ? $image : $image->post_id;
    1297      
     1337      $attachment_id = absint( ( $mode === 'bulk-select' ) ? $image : $image->post_id );
     1338
    12981339      if ( defined( 'ATAI_BULK_DEBUG' ) ) {
    12991340        ATAI_Utility::log_error( sprintf("BulkGenerate: Attachment ID %d", $attachment_id) );
    13001341      }
    1301      
     1342
     1343      // Skip if already processed as WPML translation (prevents double-processing)
     1344      if ( in_array( $attachment_id, $wpml_processed_ids, true ) ) {
     1345        // Don't increment images_skipped to avoid double-counting in stats
     1346        $skip_reasons['wpml_already_processed'] = ($skip_reasons['wpml_already_processed'] ?? 0) + 1;
     1347        $last_post_id = $attachment_id;
     1348
     1349        if ( $mode === 'bulk-select' ) {
     1350          $processed_ids[] = $attachment_id;
     1351        }
     1352
     1353        if ( ++$loop_count >= $query_limit ) {
     1354          break;
     1355        }
     1356        continue;
     1357      }
     1358
    13021359      // Skip if attachment is not eligible
    13031360      if ( ! $this->is_attachment_eligible( $attachment_id, 'bulk' ) ) {
     
    13691426      if ( is_string( $response ) && $response !== '' && ! $is_error_code ) {
    13701427        $images_successful++;
     1428
     1429        // Process WPML translations for successfully generated primary images
     1430        // Note: Translation stats are NOT added to main counters to keep success_count
     1431        // aligned with process_count (primary attachments only)
     1432        $wpml_results = $this->process_wpml_translations( $attachment_id, array(
     1433          'keywords'          => $keywords,
     1434          'negative_keywords' => $negative_keywords,
     1435        ) );
     1436
     1437        // Track all WPML translation IDs to prevent double-processing later in the loop
     1438        if ( ! empty( $wpml_results['processed_ids'] ) ) {
     1439          $wpml_processed_ids = array_merge( $wpml_processed_ids, array_keys( $wpml_results['processed_ids'] ) );
     1440        }
    13711441      } else {
    13721442        // API call failed - track the reason
     
    14581528    $this->generate_alt( $attachment_id );
    14591529
     1530    // Process WPML translations
     1531    $this->process_wpml_translations( $attachment_id );
     1532
    14601533    // Redirect back to edit page
    14611534    wp_safe_redirect( wp_get_referer() );
     
    15011574
    15021575    if ( ! is_array( $response ) && $response !== false ) {
     1576      // Process WPML translations for successfully generated primary image
     1577      $wpml_results = $this->process_wpml_translations( $attachment_id, array(
     1578        'keywords' => $keywords,
     1579      ) );
     1580
    15031581      wp_send_json( array(
    15041582        'status' => 'success',
    15051583        'alt_text' => $response,
     1584        'wpml_success' => $wpml_results['success'],
    15061585      ) );
    15071586    }
Note: See TracChangeset for help on using the changeset viewer.