admin_url('admin-ajax.php'), 'test_nonce' => wp_create_nonce('aitrfoel_test_api'), 'clear_cache_nonce' => wp_create_nonce('aitrfoel_clear_cache'), 'glossary_nonce' => wp_create_nonce('aitrfoel_glossary_nonce'), 'inventory_nonce' => wp_create_nonce('aitrfoel_inventory_nonce'), 'tm_nonce' => wp_create_nonce('aitrfoel_tm_nonce'), 'clear_cache_confirm' => __('Are you sure you want to clear all cached translations? This cannot be undone.', 'ai-translator-for-elementor-and-polylang'), 'clear_tm_confirm' => __('Are you sure you want to clear all Translation Memory? Cached phrases will need to be re-translated on future runs.', 'ai-translator-for-elementor-and-polylang'), 'clearing_text' => __('Clearing...', 'ai-translator-for-elementor-and-polylang'), 'clear_cache_text' => __('Clear Translation Cache', 'ai-translator-for-elementor-and-polylang'), 'recalculating_text' => __('Recalculating...', 'ai-translator-for-elementor-and-polylang'), 'recalculate_stats_text' => __('Recalculate Stats', 'ai-translator-for-elementor-and-polylang'), ]); } /** * Get word limit for plan * Based on Freemius plan IDs and token limits: * - Free: 40K tokens ≈ 7,200 words * - Starter (45513): 100K tokens ≈ 18,000 words * - Agency (45514): 500K tokens ≈ 89,000 words * - Enterprise (45515): 2M tokens ≈ 355,000 words */ public static function get_word_limit_for_plan($plan_name) { $limits = [ 'Free' => 7200, // 40,000 tokens 'free' => 7200, // lowercase variant 'Starter' => 18000, // 100,000 tokens (Plan ID: 45513) 'starter' => 18000, // lowercase variant 'Agency' => 89000, // 500,000 tokens (Plan ID: 45514) 'agency' => 89000, // lowercase variant 'Enterprise' => 355000, // 2,000,000 tokens (Plan ID: 45515) 'enterprise' => 355000 // lowercase variant ]; return isset($limits[$plan_name]) ? $limits[$plan_name] : $limits['Free']; } /** * Sanitize the custom string-translations setting. * Form submits parallel rows under ['rows'][i]['source'] and ['rows'][i]['trans'][lang]. * Stored as: [ ['source'=>..., 'translations'=>['en'=>...,'de'=>...]], ... ]. * * @param mixed $input * @return array */ public static function sanitize_string_translations($input) { $out = []; if (!is_array($input)) { return $out; } // Normalize to a flat list of rows. This runs both on the settings-form // submission (shape: ['rows'][i]['source'], ['rows'][i]['trans'][lang]) // AND on direct update_option() calls from the auto-translate tool // (shape: [ ['source'=>..., 'translations'=>[lang=>txt]], ... ]), because // register_setting's sanitize filter fires on both. $rows = []; if (isset($input['rows']) && is_array($input['rows'])) { foreach ($input['rows'] as $row) { if (!is_array($row)) { continue; } $rows[] = [ 'source' => isset($row['source']) ? $row['source'] : '', 'translations' => (!empty($row['trans']) && is_array($row['trans'])) ? $row['trans'] : [], ]; } } else { foreach ($input as $row) { if (!is_array($row)) { continue; } $rows[] = [ 'source' => isset($row['source']) ? $row['source'] : '', 'translations' => (!empty($row['translations']) && is_array($row['translations'])) ? $row['translations'] : [], ]; } } foreach ($rows as $row) { $source = trim(wp_kses_post((string) $row['source'])); if ($source === '') { continue; } $translations = []; foreach ((array) $row['translations'] as $lang => $txt) { $txt = trim(wp_kses_post((string) $txt)); if ($txt !== '') { $translations[sanitize_key($lang)] = $txt; } } if (!empty($translations)) { $out[] = ['source' => $source, 'translations' => $translations]; } } return $out; } public static function register_settings() { // License Settings (hosted OpenAI service) register_setting('aitrfoel_settings_group', 'aitrfoel_license_key', [ 'sanitize_callback' => 'sanitize_text_field' ]); // Cache Settings register_setting('aitrfoel_settings_group', 'aitrfoel_cache_enabled', [ 'sanitize_callback' => 'boolval', 'default' => true ]); register_setting('aitrfoel_settings_group', 'aitrfoel_cache_duration', [ 'sanitize_callback' => 'intval', 'default' => 30 ]); // Elementor async (submit→poll) translation. Opt-in / experimental: when enabled, // Elementor pages translate via the proxy async job + browser polling instead of // the synchronous path. Default OFF so existing behavior is unchanged. register_setting('aitrfoel_settings_group', 'aitrfoel_elementor_async_enabled', [ 'sanitize_callback' => 'boolval', 'default' => false ]); // Floating translation toolbar on WP List Screens (edit.php) register_setting('aitrfoel_settings_group', 'aitrfoel_list_toolbar_enabled', [ 'sanitize_callback' => 'boolval', 'default' => true ]); // Page Creation Settings register_setting('aitrfoel_settings_group', 'aitrfoel_translated_page_status', [ 'sanitize_callback' => 'sanitize_text_field', 'default' => 'publish' ]); // SEO Translation Settings register_setting('aitrfoel_settings_group', 'aitrfoel_seo_translation_enabled', [ 'sanitize_callback' => 'boolval', 'default' => true ]); register_setting('aitrfoel_settings_group', 'aitrfoel_seo_translate_og', [ 'sanitize_callback' => 'boolval', 'default' => true ]); register_setting('aitrfoel_settings_group', 'aitrfoel_seo_translate_twitter', [ 'sanitize_callback' => 'boolval', 'default' => true ]); // WooCommerce Settings register_setting('aitrfoel_settings_group', 'aitrfoel_woocommerce_support', [ 'sanitize_callback' => 'boolval', 'default' => true ]); // SureCart product translation toggle register_setting('aitrfoel_settings_group', 'aitrfoel_surecart_support', [ 'sanitize_callback' => 'boolval', 'default' => true ]); register_setting('aitrfoel_settings_group', 'aitrfoel_woo_translate_categories', [ 'sanitize_callback' => 'boolval', 'default' => true ]); register_setting('aitrfoel_settings_group', 'aitrfoel_woo_translate_tags', [ 'sanitize_callback' => 'boolval', 'default' => true ]); register_setting('aitrfoel_settings_group', 'aitrfoel_woo_translate_attributes', [ 'sanitize_callback' => 'boolval', 'default' => true ]); register_setting('aitrfoel_settings_group', 'aitrfoel_woo_translate_variations', [ 'sanitize_callback' => 'boolval', 'default' => true ]); register_setting('aitrfoel_settings_group', 'aitrfoel_woo_translate_gallery_alt', [ 'sanitize_callback' => 'boolval', 'default' => true ]); register_setting('aitrfoel_settings_group', 'aitrfoel_woo_product_fallback', [ 'sanitize_callback' => 'boolval', 'default' => true ]); // Taxonomy name translation (built-in language system) register_setting('aitrfoel_settings_group', 'aitrfoel_translate_taxonomy_names', [ 'sanitize_callback' => 'boolval', 'default' => true ]); // URL/slug translation for translated posts register_setting('aitrfoel_settings_group', 'aitrfoel_translate_url_slugs', [ 'sanitize_callback' => 'boolval', 'default' => true ]); // Preserve existing slug on retranslation register_setting('aitrfoel_settings_group', 'aitrfoel_preserve_slug_on_retranslate', [ 'sanitize_callback' => 'boolval', 'default' => true ]); // Localize absolute internal links (/fr/...) in content/widgets to the current language register_setting('aitrfoel_settings_group', 'aitrfoel_localize_internal_links', [ 'sanitize_callback' => 'boolval', 'default' => true ]); // Paths excluded from language routing (no prefix, no redirect) register_setting('aitrfoel_settings_group', 'aitrfoel_routing_excluded_paths', [ 'sanitize_callback' => 'sanitize_textarea_field', 'default' => '' ]); // Custom string translations (admin overrides for UI strings not in .mo files) register_setting('aitrfoel_settings_group', 'aitrfoel_string_translations', [ 'sanitize_callback' => [self::class, 'sanitize_string_translations'], 'default' => [] ]); // Default language URL prefix register_setting('aitrfoel_settings_group', 'aitrfoel_default_lang_prefix', [ 'sanitize_callback' => 'boolval', 'default' => false ]); // Theme Compatibility Settings register_setting('aitrfoel_settings_group', 'aitrfoel_custom_meta_fields', [ 'sanitize_callback' => 'sanitize_textarea_field', 'default' => '' ]); // Preserve Post Sorting Order register_setting('aitrfoel_settings_group', 'aitrfoel_preserve_post_order', [ 'sanitize_callback' => 'boolval', 'default' => true ]); // Synchronize Media, Galleries and Custom Fields register_setting('aitrfoel_settings_group', 'aitrfoel_copy_custom_media_fields', [ 'sanitize_callback' => 'boolval', 'default' => true ]); // Media Library Duplication (Optional attachment post creation per language) register_setting('aitrfoel_settings_group', 'aitrfoel_duplicate_media_attachments', [ 'sanitize_callback' => 'boolval', 'default' => false ]); // Brand Voice & Localization Tone (PRO Language Enhancer) register_setting('aitrfoel_settings_group', 'aitrfoel_brand_tone_preset', [ 'sanitize_callback' => function($value) { $allowed = ['hospitality', 'corporate', 'sales', 'casual', 'custom']; return in_array($value, $allowed, true) ? $value : 'corporate'; }, 'default' => 'corporate' ]); register_setting('aitrfoel_settings_group', 'aitrfoel_brand_tone_custom', [ 'sanitize_callback' => 'sanitize_textarea_field', 'default' => '' ]); register_setting('aitrfoel_settings_group', 'aitrfoel_formality_mode', [ 'sanitize_callback' => function($value) { $allowed = ['auto', 'formal', 'informal']; return in_array($value, $allowed, true) ? $value : 'auto'; }, 'default' => 'auto' ]); // Debug Settings register_setting('aitrfoel_settings_group', 'aitrfoel_debug_mode', [ 'sanitize_callback' => 'boolval', 'default' => false ]); // Language Switcher Settings register_setting('aitrfoel_settings_group', 'aitrfoel_switcher_enabled', [ 'sanitize_callback' => 'boolval', 'default' => true ]); register_setting('aitrfoel_settings_group', 'aitrfoel_switcher_style', [ 'sanitize_callback' => function($value) { $allowed = ['dropdown', 'list', 'flags', 'flags-text']; return in_array($value, $allowed, true) ? $value : 'flags-text'; }, 'default' => 'flags-text' ]); register_setting('aitrfoel_settings_group', 'aitrfoel_switcher_show_flags', [ 'sanitize_callback' => 'boolval', 'default' => true ]); register_setting('aitrfoel_settings_group', 'aitrfoel_switcher_theme', [ 'sanitize_callback' => function($value) { $allowed = ['default', 'minimal', 'light', 'pill']; return in_array($value, $allowed, true) ? $value : 'default'; }, 'default' => 'default' ]); register_setting('aitrfoel_settings_group', 'aitrfoel_switcher_dropdown_names', [ 'sanitize_callback' => 'boolval', 'default' => true ]); register_setting('aitrfoel_settings_group', 'aitrfoel_switcher_mobile_names', [ 'sanitize_callback' => 'boolval', 'default' => false ]); register_setting('aitrfoel_settings_group', 'aitrfoel_auto_translate_templates', [ 'sanitize_callback' => 'boolval', 'default' => false ]); register_setting('aitrfoel_settings_group', 'aitrfoel_extra_template_post_types', [ 'sanitize_callback' => function($value) { $slugs = preg_split('/[\s,]+/', (string) $value); $slugs = array_filter(array_map('sanitize_key', array_map('trim', $slugs))); return implode(', ', array_values(array_unique($slugs))); }, 'default' => '' ]); register_setting('aitrfoel_settings_group', 'aitrfoel_switcher_border_color', [ 'sanitize_callback' => 'sanitize_hex_color', 'default' => '' ]); register_setting('aitrfoel_settings_group', 'aitrfoel_switcher_flag_size', [ 'sanitize_callback' => function($value) { $v = absint($value); if ($v === 0) { return 0; } return max(8, min(128, $v)); }, 'default' => 0 ]); register_setting('aitrfoel_settings_group', 'aitrfoel_switcher_show_names', [ 'sanitize_callback' => 'boolval', 'default' => true ]); register_setting('aitrfoel_settings_group', 'aitrfoel_switcher_name_display', [ 'sanitize_callback' => function($value) { $allowed = ['native', 'english', 'code', 'code_lower']; return in_array($value, $allowed, true) ? $value : 'native'; }, 'default' => 'native' ]); register_setting('aitrfoel_settings_group', 'aitrfoel_switcher_custom_names', [ 'sanitize_callback' => function($value) { if (!is_array($value)) return []; return array_map('sanitize_text_field', $value); }, 'default' => [] ]); register_setting('aitrfoel_settings_group', 'aitrfoel_switcher_custom_css', [ 'sanitize_callback' => 'wp_strip_all_tags', 'default' => '' ]); // Nav menu switcher settings register_setting('aitrfoel_settings_group', 'aitrfoel_switcher_hide_untranslated', [ 'sanitize_callback' => 'boolval', 'default' => false ]); register_setting('aitrfoel_settings_group', 'aitrfoel_nav_menu_switcher', [ 'sanitize_callback' => 'boolval', 'default' => false ]); register_setting('aitrfoel_settings_group', 'aitrfoel_nav_menu_location', [ 'sanitize_callback' => 'sanitize_text_field', 'default' => '' ]); // Per-language menu assignments register_setting('aitrfoel_settings_group', 'aitrfoel_menu_assignments', [ 'sanitize_callback' => [self::class, 'sanitize_menu_assignments'], 'default' => [] ]); // Per-language widget visibility register_setting('aitrfoel_settings_group', 'aitrfoel_widget_languages', [ 'sanitize_callback' => [self::class, 'sanitize_widget_languages'], 'default' => [] ]); // Glossary settings register_setting('aitrfoel_settings_group', 'aitrfoel_glossary_terms', [ 'sanitize_callback' => [self::class, 'sanitize_glossary_terms'], 'default' => '' ]); // Exclude words settings register_setting('aitrfoel_settings_group', 'aitrfoel_exclude_words', [ 'sanitize_callback' => [self::class, 'sanitize_exclude_words'], 'default' => '' ]); // Translation Memory settings register_setting('aitrfoel_settings_group', 'aitrfoel_enable_translation_memory', [ 'sanitize_callback' => 'boolval', 'default' => true ]); // Background Auto-Sync settings register_setting('aitrfoel_settings_group', 'aitrfoel_enable_background_sync', [ 'sanitize_callback' => 'boolval', 'default' => true ]); // AI Translation Engine selection register_setting('aitrfoel_settings_group', 'aitrfoel_ai_engine', [ 'sanitize_callback' => function($val) { $allowed = ['auto', 'claude-3-5-sonnet', 'deepl', 'gpt-4o', 'gpt-4o-mini']; return in_array($val, $allowed, true) ? $val : 'auto'; }, 'default' => 'auto' ]); } /** * Get exclude words as array (synced from both Settings page and modal) */ public static function get_exclude_words() { // Check the modal-based setting first (stored by AJAX handler) $modal_settings = get_option('aitrfoel_translation_settings', []); $modal_words = isset($modal_settings['exclude_words']) ? $modal_settings['exclude_words'] : []; // Check the settings-page-based textarea $textarea_value = get_option('aitrfoel_exclude_words', ''); $textarea_words = array_filter(array_map('trim', explode("\n", $textarea_value))); // Merge both sources, remove duplicates return array_values(array_unique(array_merge($modal_words, $textarea_words))); } /** * Sanitize exclude words textarea and sync to modal option */ public static function sanitize_exclude_words($input) { $words = array_filter(array_map('sanitize_text_field', explode("\n", $input))); // Sync to the AJAX-based modal option so both stay in sync $settings = get_option('aitrfoel_translation_settings', []); $settings['exclude_words'] = array_values($words); $settings['updated_at'] = time(); update_option('aitrfoel_translation_settings', $settings); return implode("\n", $words); } /** * Get site-wide translatable content inventory * * @param bool $force_recount * @return array */ public static function get_site_content_inventory($force_recount = false) { $transient_key = 'aitrfoel_site_inventory_stats_v2'; if (!$force_recount) { $cached = get_transient($transient_key); if (is_array($cached) && !empty($cached)) { return $cached; } } $detector = class_exists('AITRFOEL_Content_Detector') ? AITRFOEL_Content_Detector::get_instance() : null; $manager = class_exists('AITRFOEL_Multilingual_Manager') ? AITRFOEL_Multilingual_Manager::get_instance() : null; $default_lang = ($manager && $manager->is_active()) ? $manager->get_default_language() : ''; $target_languages = ($manager && $manager->is_active()) ? $manager->get_available_languages() : []; $target_lang_count = max(1, count($target_languages) - ($default_lang ? 1 : 0)); // Content group definitions with crisp vector SVG icons $post_type_groups = [ 'pages' => [ 'label' => __('Pages', 'ai-translator-for-elementor-and-polylang'), 'icon' => '', 'post_types' => ['page'], ], 'posts' => [ 'label' => __('Posts', 'ai-translator-for-elementor-and-polylang'), 'icon' => '', 'post_types' => ['post'], ], 'templates' => [ 'label' => __('Templates & Headers/Footers', 'ai-translator-for-elementor-and-polylang'), 'icon' => '', 'post_types' => ['elementor_library', 'elementor-hf', 'jet-theme-core'], ], 'products' => [ 'label' => __('Products', 'ai-translator-for-elementor-and-polylang'), 'icon' => '', 'post_types' => ['product', 'sc_product'], ], ]; $breakdown = []; $total_items = 0; $total_source_words = 0; foreach ($post_type_groups as $group_key => $group) { $valid_types = array_filter($group['post_types'], 'post_type_exists'); if (empty($valid_types)) { continue; } $posts = get_posts([ 'post_type' => $valid_types, 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', ]); // Filter to only source-language posts if multilingual is active if ($manager && $manager->is_active() && $default_lang) { $posts = array_filter($posts, function($pid) use ($manager, $default_lang) { $lang = $manager->get_post_language($pid); if (!empty($lang)) { return $lang === $default_lang; } $translations = $manager->get_post_translations($pid); if (!empty($translations)) { foreach ($translations as $l => $id) { if ((int)$id === $pid && $l !== $default_lang) { return false; } } } return true; }); $posts = array_values($posts); } $group_words = 0; foreach ($posts as $pid) { if ($detector) { $group_words += $detector->get_cached_word_count($pid, $force_recount); } } $item_count = count($posts); if ($item_count > 0 || in_array($group_key, ['pages', 'posts'])) { $breakdown[$group_key] = [ 'label' => $group['label'], 'icon' => $group['icon'], 'count' => $item_count, 'words' => $group_words, ]; $total_items += $item_count; $total_source_words += $group_words; } } $estimated_total_demand = $total_source_words * $target_lang_count; $recommended_plan = self::get_recommended_plan_for_words($estimated_total_demand); $result = [ 'total_items' => $total_items, 'total_source_words' => $total_source_words, 'target_lang_count' => $target_lang_count, 'estimated_total_demand' => $estimated_total_demand, 'breakdown' => $breakdown, 'recommended_plan' => $recommended_plan, ]; set_transient($transient_key, $result, HOUR_IN_SECONDS); return $result; } /** * Get recommended plan based on word demand * * @param int $demand_words * @return array */ public static function get_recommended_plan_for_words($demand_words) { if ($demand_words <= 18000) { $plan_name = 'Starter'; $plan_limit = 18000; $plan_id = 45513; $plan_slug = 'starter'; } elseif ($demand_words <= 89000) { $plan_name = 'Agency'; $plan_limit = 89000; $plan_id = 45514; $plan_slug = 'agency'; } else { $plan_name = 'Enterprise'; $plan_limit = 355000; $plan_id = 45515; $plan_slug = 'enterprise'; } $upgrade_url = 'https://meeptech.com/products/ai-translator-for-elementor-polylang/?utm_source=wp-plugin&utm_medium=plugin-settings&utm_campaign=pro-upgrade&plan=' . $plan_slug . '#pricing'; try { $is_pro = (defined('AITRFOEL_VERSION_TYPE') && AITRFOEL_VERSION_TYPE === 'pro'); if ($is_pro && function_exists('aitrfoel_fs') && is_object(aitrfoel_fs()) && method_exists(aitrfoel_fs(), 'is_registered') && aitrfoel_fs()->is_registered()) { $upgrade_url = aitrfoel_fs()->get_upgrade_url(); } elseif (function_exists('eatp_fs') && is_object(eatp_fs()) && method_exists(eatp_fs(), 'is_registered') && eatp_fs()->is_registered()) { $upgrade_url = eatp_fs()->get_upgrade_url(); } } catch (\Throwable $e) { // Keep default meeptech pricing URL fallback } return [ 'name' => $plan_name, 'limit' => $plan_limit, 'limit_k' => round($plan_limit / 1000), 'slug' => $plan_slug, 'upgrade_url' => $upgrade_url, ]; } /** * AJAX endpoint to recalculate inventory stats on demand */ public static function refresh_inventory_stats_ajax() { try { check_ajax_referer('aitrfoel_inventory_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Insufficient permissions.', 'ai-translator-for-elementor-and-polylang')]); } $inventory = self::get_site_content_inventory(true); wp_send_json_success($inventory); } catch (\Throwable $e) { wp_send_json_error(['message' => $e->getMessage()]); } } /** * AJAX handler to clear Translation Memory */ public static function clear_translation_memory_ajax() { try { check_ajax_referer('aitrfoel_tm_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => __('Insufficient permissions.', 'ai-translator-for-elementor-and-polylang')]); } if (class_exists('AITRFOEL_Translation_Memory')) { AITRFOEL_Translation_Memory::get_instance()->clear_memory(); } wp_send_json_success([ 'message' => __('Translation Memory cleared successfully.', 'ai-translator-for-elementor-and-polylang'), 'stats' => [ 'total_entries' => 0, 'total_words_saved' => 0, 'total_hits' => 0, ], ]); } catch (\Throwable $e) { wp_send_json_error(['message' => $e->getMessage()]); } } public static function render_page() { $license_key = get_option('aitrfoel_license_key', ''); $license_status = 'Free'; if (function_exists('aitrfoel_fs') && aitrfoel_fs()->is_registered()) { $plan = aitrfoel_fs()->get_plan(); $license_status = $plan ? $plan->name : 'Free'; } // Get usage from server (not local cache) $words_used = 0; $word_limit = 7200; // Default free limit if (class_exists('AITRFOEL_License_Manager_Server')) { $license_manager = AITRFOEL_License_Manager_Server::get_instance(); $license_info = $license_manager->get_license_status(); $words_used = $license_info['words_used']; $word_limit = $license_info['word_limit']; } $usage_percent = null; if ($word_limit !== -1 && $word_limit > 0) { $usage_percent = (int) min(100, round(($words_used / $word_limit) * 100)); } $cache_enabled = get_option('aitrfoel_cache_enabled', true); $cache_duration = get_option('aitrfoel_cache_duration', 30); $translated_page_status = get_option('aitrfoel_translated_page_status', 'publish'); ?>
, name=, is_default= |