'; return 'data:image/svg+xml;base64,' . base64_encode($svg); } function aitmt_fallback_logo_url() { if (file_exists(AITMT_PLUGIN_DIR . 'assets/img/logo.png')) { return AITMT_PLUGIN_URL . 'assets/img/logo.png'; } return aitmt_fallback_embedded_logo_url(); } function aitmt_fallback_embedded_logo_url() { return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cmVjdCB4PSIzIiB5PSIzIiB3aWR0aD0iODgiIGhlaWdodD0iOTQiIHJ4PSI3IiBmaWxsPSIjZmZjOTI4Ii8+PHBhdGggZmlsbD0iIzAwMCIgZD0iTTE2IDg3YzAtOSA3LTE2IDE2LTE2aDR2MjZIMTZWODdabTE4LTYzaDUyYzYgMCAxMCA0IDEwIDEwdjEySDc2djUxSDUyVjQ2SDM0Yy02IDAtMTAtNC0xMC0xMHYtMmMwLTYgNC0xMCAxMC0xMFoiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNOTEgM2g2djM4aC02eiIvPjwvc3ZnPg=='; } function aitmt_fallback_asset_contents($relative_path) { $path = AITMT_PLUGIN_DIR . ltrim((string) $relative_path, '/'); return aitmt_fallback_read_local_file($path); } function aitmt_fallback_filesystem() { global $wp_filesystem; if (!function_exists('WP_Filesystem')) { require_once ABSPATH . 'wp-admin/includes/file.php'; } if (!$wp_filesystem) { WP_Filesystem(); } return $wp_filesystem; } function aitmt_fallback_read_local_file($path) { $filesystem = aitmt_fallback_filesystem(); $path = wp_normalize_path((string) $path); if (!$filesystem || !$filesystem->exists($path) || !$filesystem->is_readable($path)) { return ''; } $contents = $filesystem->get_contents($path); return is_string($contents) ? $contents : ''; } function aitmt_fallback_write_local_file($path, $contents) { $filesystem = aitmt_fallback_filesystem(); $path = wp_normalize_path((string) $path); if (!$filesystem) { return false; } return (bool) $filesystem->put_contents($path, (string) $contents, defined('FS_CHMOD_FILE') ? FS_CHMOD_FILE : 0644); } function aitmt_fallback_edition_label() { return __('Public Beta Build', 'ait-multilingual-translate'); } function aitmt_fallback_is_professional() { return false; } function aitmt_is_pro() { return (bool) apply_filters('aitmt_is_pro', aitmt_fallback_is_professional()); } function aitmt_has_feature($feature) { $feature = sanitize_key((string) $feature); $features = array( 'unlimited_languages', 'bulk_translate', 'translation_memory', 'advanced_woocommerce', 'seo_translation', 'slug_translation', 'export_import', 'priority_support', ); return (bool) apply_filters('aitmt_has_feature', true, $feature, $features); } function aitmt_fallback_admin_page_slugs() { return array( AITMT_PUBLIC_SLUG, 'ait-multilingual-translate-translations', 'ait-multilingual-translate-scanner', 'ait-multilingual-translate-usage', 'ait-multilingual-translate-report-bug', 'ait-multilingual-translate-feedback', 'ait-multilingual-translate-onboarding', 'ait-multilingual-translate-support', 'ait-multilingual-translate-debugger', ); } function aitmt_fallback_is_plugin_admin_screen($hook = '') { if ('nav-menus.php' === (string) $hook) { return true; } $screen = function_exists('get_current_screen') ? get_current_screen() : null; $screen_id = $screen && !empty($screen->id) ? (string) $screen->id : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin screen detection only reads the page slug. $page = isset($_GET['page']) ? sanitize_key(wp_unslash((string) $_GET['page'])) : ''; foreach (aitmt_fallback_admin_page_slugs() as $slug) { if ($slug === $page || false !== strpos((string) $hook, $slug) || false !== strpos($screen_id, $slug)) { return true; } } return false; } function aitmt_fallback_enqueue_admin_assets($hook) { if (!aitmt_fallback_is_plugin_admin_screen($hook)) { return; } wp_enqueue_style('aitmt-admin', AITMT_PLUGIN_URL . 'assets/css/admin.css', array(), AITMT_VERSION); wp_enqueue_script('aitmt-admin', AITMT_PLUGIN_URL . 'assets/js/admin.js', array(), AITMT_VERSION, true); wp_localize_script('aitmt-admin', 'AITMT_ADMIN', aitmt_fallback_admin_script_data()); $admin_css = aitmt_fallback_asset_contents('assets/css/admin.css'); wp_add_inline_style('aitmt-admin', $admin_css ? $admin_css : aitmt_fallback_admin_inline_css()); wp_add_inline_style('aitmt-admin', aitmt_fallback_admin_polish_css()); $admin_js = aitmt_fallback_asset_contents('assets/js/admin.js'); wp_add_inline_script('aitmt-admin', $admin_js ? $admin_js : aitmt_fallback_admin_inline_script(), 'after'); } function aitmt_fallback_admin_script_data() { $providers = array(); foreach (array('openai', 'gemini', 'grok', 'google_translate', 'deepl') as $provider) { $profile = aitmt_fallback_provider_pricing_profile($provider); $providers[$provider] = array( 'label' => aitmt_fallback_provider_label($provider), 'badge' => isset($profile['badge_text']) ? (string) $profile['badge_text'] : '', 'dismissed' => (bool) get_user_meta(get_current_user_id(), 'aitmt_provider_pricing_notice_' . $provider, true), 'recommendations' => isset($profile['recommendations']) ? array_values((array) $profile['recommendations']) : array(), ); } return array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aitmt_provider_notice'), 'providers' => $providers, 'modalTitle' => __('Provider Pricing Notice', 'ait-multilingual-translate'), 'modalBody' => __('This provider may require a paid API account. Before translating your website, configure a free tier if available, daily quota, monthly budget, and billing alerts. AIT Multilingual Translate minimizes API usage with Translation Memory, cache reuse, duplicate detection, and small batches.', 'ait-multilingual-translate'), 'continueText' => __('Continue', 'ait-multilingual-translate'), 'cancelText' => __('Cancel', 'ait-multilingual-translate'), ); } function aitmt_fallback_admin_inline_css() { return '.aitmt-admin-title{align-items:center;display:flex;gap:14px;margin:18px 0 12px}.aitmt-admin-title img{background:transparent;border-radius:8px;box-shadow:none;object-fit:contain}.aitmt-admin-title.is-dashboard{align-items:flex-start}.aitmt-admin-title.is-dashboard img{height:150px;width:150px}.aitmt-admin-title h1{margin:0;padding:0}.aitmt-admin-title p{color:#646970;margin:3px 0 0}.aitmt-admin-title .aitmt-admin-meta{color:#1d2327;font-size:12px}.aitmt-admin-tabs{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0 16px;max-width:1180px}.aitmt-admin-tabs a{background:#fff;border:1px solid #c3c4c7;border-radius:4px;color:#1d2327;padding:7px 10px;text-decoration:none}.aitmt-admin-tabs a:hover{border-color:#2271b1;color:#2271b1}.aitmt-fallback-card,.aitmt-wide-card{background:#fff;border:1px solid #dcdcde;border-radius:6px;margin:16px 0;padding:8px 20px 18px;width:calc(100% - 40px)}.aitmt-fallback-card h2,.aitmt-wide-card h2{border-bottom:1px solid #f0f0f1;margin:0 -20px 12px;padding:14px 20px}.aitmt-fallback-language-grid,.aitmt-language-list{border:1px solid #dcdcde;border-radius:6px;display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));max-height:360px;overflow:auto;padding:8px}.aitmt-actions-row,.aitmt-matrix-actions,.aitmt-matrix-filters,.aitmt-matrix-pagination,.aitmt-debug-actions{align-items:center;display:flex;flex-wrap:wrap;gap:8px;margin:12px 0}.aitmt-matrix-actions form,.aitmt-matrix-pagination form,.aitmt-debug-actions form{align-items:center;display:inline-flex;gap:8px;margin:0}.aitmt-matrix-actions .submit,.aitmt-matrix-filters .submit,.aitmt-matrix-pagination .submit,.aitmt-debug-actions .submit,.aitmt-debug-settings .submit{margin:0;padding:0}.aitmt-matrix-filters,.aitmt-debug-settings,.aitmt-debug-actions{background:#f6f7f7;border:1px solid #dcdcde;border-radius:6px;padding:10px 12px}.aitmt-matrix-filters label{display:flex;flex-direction:column;gap:4px;margin:0}.aitmt-debug-table th{width:260px}.aitmt-debug-result{background:#f6f7f7;border-left:4px solid #72aee6;margin:12px 0;padding:10px 12px}.aitmt-debug-result.is-bad{background:#fcf0f1;border-left-color:#d63638}.aitmt-debug-result.is-good{background:#edfaef;border-left-color:#00a32a}.aitmt-log-table code{white-space:pre-wrap;word-break:break-word}.aitmt-matrix-table textarea,.aitmt-translations-table textarea{min-height:76px;width:100%}.aitmt-matrix-table .source-cell{max-width:460px}.aitmt-matrix-pagination .button[aria-disabled=true]{opacity:.55;pointer-events:none}.aitmt-matrix-pagination .aitmt-page-number.is-current{background:#1d2327;border-color:#1d2327;color:#fff}.aitmt-export-import{border:1px solid #dcdcde;border-radius:6px;margin:12px 0;padding:10px 12px}.aitmt-export-import summary{cursor:pointer;font-weight:600}.aitmt-export-import-grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));margin-top:12px}'; } function aitmt_fallback_admin_polish_css() { return '.aitmt-admin-title img{display:block;height:34px;width:34px}.aitmt-admin-title.is-dashboard img{height:150px;width:150px}.toplevel_page_ait-multilingual-translate .wp-menu-image img{height:20px!important;object-fit:contain;padding-top:7px!important;width:20px!important}.aitmt-section-heading{font-size:15px;margin:10px 0 4px}.aitmt-mode-basic .aitmt-advanced-only,.aitmt-fallback-language-grid label.is-hidden,.aitmt-language-option.is-hidden{display:none!important}.aitmt-matrix-filters input[type=search],.aitmt-matrix-filters select,.aitmt-matrix-filters .button{height:34px;margin:0}.aitmt-matrix-filters>.button,.aitmt-matrix-filters>input[type=submit]{align-self:flex-end}.aitmt-segmented{display:inline-flex}.aitmt-segmented input{position:absolute;opacity:0}.aitmt-segmented span{background:#fff;border:1px solid #c3c4c7;display:inline-block;min-width:78px;padding:7px 12px;text-align:center}.aitmt-segmented input:checked+span{background:#2271b1;border-color:#2271b1;color:#fff}.aitmt-form-grid,.aitmt-onboarding-steps{display:grid;gap:14px 18px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}.aitmt-checkbox-row,.aitmt-notice-actions,.aitmt-support-buttons,.aitmt-finish-onboarding{align-items:center;display:flex;flex-wrap:wrap;gap:8px}'; } function aitmt_fallback_admin_inline_script() { return <<<'JS' (function(){if(window.AITMT_ADMIN_READY){return;}window.AITMT_ADMIN_READY=true;function initLanguageSearch(){var searches=Array.prototype.slice.call(document.querySelectorAll('.aitmt-language-search, .aitmt-fallback-language-search'));searches.forEach(function(search){var scope=search.closest?search.closest('td, .aitmt-wide-card, .aitmt-fallback-card, .form-table'):document;var options=Array.prototype.slice.call((scope||document).querySelectorAll('.aitmt-language-option, .aitmt-fallback-language-grid label'));if(!options.length){options=Array.prototype.slice.call(document.querySelectorAll('.aitmt-language-option, .aitmt-fallback-language-grid label'));}search.addEventListener('input',function(){var needle=search.value.trim().toLowerCase();options.forEach(function(option){var text=option.textContent.toLowerCase();option.classList.toggle('is-hidden',!!needle&&text.indexOf(needle)===-1);});});});}function initAdminMode(){var wrap=document.querySelector('.aitmt-admin-page');var radios=Array.prototype.slice.call(document.querySelectorAll('input[name="aitmt_options[admin_mode]"]'));if(!wrap||!radios.length){return;}function applyMode(){var checked=radios.filter(function(radio){return radio.checked;})[0];var mode=checked&&checked.value==='advanced'?'advanced':'basic';wrap.classList.toggle('aitmt-mode-basic',mode==='basic');wrap.classList.toggle('aitmt-mode-advanced',mode==='advanced');}radios.forEach(function(radio){radio.addEventListener('change',applyMode);});applyMode();}function initAdmin(){initLanguageSearch();initAdminMode();}if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',initAdmin);}else{initAdmin();}})(); JS; } function aitmt_fallback_frontend_inline_css() { return '.aitmt-fallback-switcher-wrap{box-sizing:border-box;padding:8px 16px;text-align:right;width:100%;z-index:99}.aitmt-fallback-switcher-header{clear:both}.aitmt-fallback-switcher-footer{clear:both;margin-top:10px}.aitmt-fallback-switcher,.aitmt-fallback-switcher-dropdown{font:inherit}.aitmt-fallback-switcher-dropdown{background:#fff;border:1px solid currentColor;border-radius:6px;min-height:36px;padding:4px 28px 4px 10px}.aitmt-fallback-switcher-list{align-items:center;display:inline-flex;flex-wrap:wrap;gap:6px;justify-content:flex-end}.aitmt-fallback-switcher-link{align-items:center;border:1px solid currentColor;border-radius:6px;display:inline-flex;gap:6px;line-height:1.2;min-height:32px;padding:5px 9px;text-decoration:none}.aitmt-fallback-switcher-link.is-current{font-weight:700;opacity:.75}.aitmt-fallback-language-code{font-size:.78em;opacity:.72}.aitmt-elementor-switcher .aitmt-fallback-switcher-wrap{display:inline-block;padding:0}.aitmt-inline-editor-toolbar{align-items:center;background:#1d2327;border-radius:6px;bottom:18px;box-shadow:0 8px 22px rgba(0,0,0,.22);box-sizing:border-box;color:#fff;display:flex;gap:8px;left:18px;max-width:calc(100vw - 36px);padding:8px;position:fixed;z-index:999999}.aitmt-inline-editor-brand{align-items:center;color:#fff;display:flex;gap:6px;font-size:12px;line-height:1.2;max-width:300px;text-decoration:none}.aitmt-inline-editor-brand:hover,.aitmt-inline-editor-brand:focus{color:#fff;text-decoration:underline}.aitmt-inline-editor-brand img{background:#fff;border-radius:4px;height:22px;width:22px}.aitmt-inline-editor-toolbar button{background:#2271b1;border:0;border-radius:4px;color:#fff;cursor:pointer;font:inherit;min-height:34px;padding:6px 10px}.aitmt-inline-editor-toolbar button.is-active{background:#00a32a}.aitmt-inline-editor-status{font-size:12px;max-width:340px}.aitmt-inline-editor-modal{align-items:center;background:rgba(0,0,0,.42);display:none;inset:0;justify-content:center;padding:20px;position:fixed;z-index:1000000}.aitmt-inline-editor-modal.is-open{display:flex}.aitmt-inline-editor-dialog{background:#fff;border-radius:8px;box-shadow:0 18px 50px rgba(0,0,0,.28);color:#1d2327;max-width:760px;padding:20px;width:min(760px,100%)}.aitmt-inline-editor-dialog h2{font-size:18px;line-height:1.3;margin:0 0 12px}.aitmt-inline-editor-textarea{border:1px solid #8c8f94;border-radius:6px;box-sizing:border-box;font:inherit;line-height:1.45;min-height:180px;padding:10px 12px;resize:vertical;width:100%}.aitmt-inline-editor-dialog-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}.aitmt-inline-editor-dialog-actions button{background:#2271b1;border:0;border-radius:4px;color:#fff;cursor:pointer;font:inherit;min-height:34px;padding:7px 13px}.aitmt-inline-editor-dialog-actions button.is-secondary{background:#f6f7f7;border:1px solid #c3c4c7;color:#1d2327}body.aitmt-editor-active .aitmt-editable{cursor:pointer;outline:2px dashed #2271b1;outline-offset:2px}body.aitmt-editor-active .aitmt-editable:hover{background:rgba(34,113,177,.14)}@media(max-width:640px){.aitmt-inline-editor-toolbar{align-items:flex-start;bottom:10px;flex-direction:column;left:10px;right:10px;max-width:none}}'; } function aitmt_fallback_plugin_folder() { return basename(dirname(AITMT_PLUGIN_FILE)); } function aitmt_fallback_is_update_safe_folder() { return AITMT_PUBLIC_SLUG === aitmt_fallback_plugin_folder(); } function aitmt_fallback_register_settings() { register_setting('aitmt_fallback_settings', 'aitmt_options', 'aitmt_fallback_sanitize'); aitmt_fallback_sync_cron_schedule(); } function aitmt_fallback_save_settings_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_save_settings'); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The settings array is sanitized by aitmt_fallback_sanitize() below. $raw_input = isset($_POST['aitmt_options']) ? wp_unslash($_POST['aitmt_options']) : array(); $input = is_array($raw_input) ? $raw_input : array(); $old_options = aitmt_fallback_options(); $options = aitmt_fallback_sanitize($input); update_option('aitmt_options', $options, false); aitmt_fallback_sync_cron_schedule(); if ( !isset($old_options['url_mode'], $options['url_mode']) || $old_options['url_mode'] !== $options['url_mode'] || !isset($old_options['hide_default_language'], $options['hide_default_language']) || $old_options['hide_default_language'] !== $options['hide_default_language'] || wp_json_encode($old_options['enabled_languages']) !== wp_json_encode($options['enabled_languages']) || $old_options['source_language'] !== $options['source_language'] ) { aitmt_fallback_register_rewrites(); flush_rewrite_rules(false); update_option('aitmt_rewrite_version', AITMT_VERSION, false); } wp_safe_redirect(add_query_arg('settings-updated', 'true', admin_url('admin.php?page=' . AITMT_PUBLIC_SLUG))); exit; } function aitmt_fallback_apply_safe_defaults_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_apply_safe_defaults'); $options = aitmt_fallback_options(); $options = array_merge($options, aitmt_fallback_safe_api_defaults()); update_option('aitmt_options', $options, false); aitmt_fallback_sync_cron_schedule(); wp_safe_redirect(add_query_arg('aitmt_safe_defaults', '1', admin_url('admin.php?page=' . AITMT_PUBLIC_SLUG . '#aitmt-provider'))); exit; } function aitmt_fallback_ajax_dismiss_provider_pricing_notice() { check_ajax_referer('aitmt_provider_notice', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied.', 'ait-multilingual-translate')), 403); } $provider = isset($_POST['provider']) ? sanitize_key(wp_unslash((string) $_POST['provider'])) : ''; if (!in_array($provider, array('openai', 'gemini', 'grok', 'google_translate', 'deepl'), true)) { wp_send_json_error(array('message' => __('Invalid provider.', 'ait-multilingual-translate')), 400); } update_user_meta(get_current_user_id(), 'aitmt_provider_pricing_notice_' . $provider, '1'); wp_send_json_success(array('provider' => $provider)); } function aitmt_fallback_register_rewrites() { $languages = array_values(array_unique(array_filter(array_map('aitmt_fallback_normalize_language', aitmt_fallback_enabled_languages())))); $language_pattern = !empty($languages) ? '(' . implode('|', array_map('preg_quote', $languages)) . ')' : '([a-z]{2,5})'; add_rewrite_tag('%aitmt_lang%', $language_pattern); add_rewrite_tag('%aitmt_path%', '(.+)'); add_rewrite_rule('^' . $language_pattern . '/?$', 'index.php?aitmt_lang=$matches[1]', 'top'); add_rewrite_rule('^' . $language_pattern . '/(.+)/?$', 'index.php?aitmt_lang=$matches[1]&aitmt_path=$matches[2]', 'top'); } function aitmt_fallback_activate() { aitmt_fallback_maybe_create_translation_table(); aitmt_fallback_register_rewrites(); flush_rewrite_rules(false); update_option('aitmt_rewrite_version', AITMT_VERSION, false); if (!get_option('aitmt_onboarding_completed')) { update_option('aitmt_onboarding_pending', '1', false); } } function aitmt_fallback_deactivate() { wp_clear_scheduled_hook('aitmt_fallback_process_queue_event'); flush_rewrite_rules(false); } function aitmt_fallback_maybe_flush_rewrites() { if (get_option('aitmt_rewrite_version') === AITMT_VERSION) { return; } aitmt_fallback_register_rewrites(); flush_rewrite_rules(false); update_option('aitmt_rewrite_version', AITMT_VERSION, false); } function aitmt_fallback_query_vars($vars) { $vars[] = 'aitmt_lang'; $vars[] = 'aitmt_path'; return $vars; } function aitmt_fallback_filter_language_request($query_vars) { $query_vars = is_array($query_vars) ? $query_vars : array(); if (!empty($GLOBALS['aitmt_request_language']) && !empty($GLOBALS['aitmt_stripped_relative_path'])) { $resolved_query_vars = aitmt_fallback_resolve_path_query_vars((string) $GLOBALS['aitmt_stripped_relative_path']); if (!empty($resolved_query_vars)) { unset($query_vars['pagename'], $query_vars['attachment'], $query_vars['page'], $query_vars['aitmt_lang'], $query_vars['aitmt_path']); return array_merge($query_vars, aitmt_fallback_normalize_resolved_query_vars($resolved_query_vars)); } } if (empty($query_vars['aitmt_lang'])) { return $query_vars; } $language = aitmt_fallback_normalize_language((string) $query_vars['aitmt_lang']); if (!in_array($language, aitmt_fallback_enabled_languages(), true)) { unset($query_vars['aitmt_lang'], $query_vars['aitmt_path']); return $query_vars; } $query_vars['aitmt_lang'] = $language; if (empty($query_vars['aitmt_path'])) { $front_page_id = 'page' === get_option('show_on_front') ? absint(get_option('page_on_front')) : 0; if ($front_page_id) { $query_vars['page_id'] = $front_page_id; } unset($query_vars['pagename'], $query_vars['name'], $query_vars['attachment']); return $query_vars; } $path = trim((string) $query_vars['aitmt_path'], '/'); if ('' === $path) { return $query_vars; } $resolved_query_vars = aitmt_fallback_resolve_path_query_vars($path); if (!empty($resolved_query_vars)) { unset($query_vars['pagename'], $query_vars['attachment'], $query_vars['page']); return array_merge($query_vars, aitmt_fallback_normalize_resolved_query_vars($resolved_query_vars)); } $query_vars['pagename'] = $path; return $query_vars; } function aitmt_fallback_resolve_path_query_vars($path) { $path = trim(rawurldecode((string) $path), '/'); $path_segments = aitmt_fallback_strip_language_segment(array_values(array_filter(explode('/', $path)))); $path = implode('/', $path_segments); if ('' === $path) { return array(); } $product_query_vars = aitmt_fallback_resolve_product_query_vars($path_segments); if (!empty($product_query_vars)) { return $product_query_vars; } $rewrite_query_vars = aitmt_fallback_resolve_rewrite_query_vars($path); if (!empty($rewrite_query_vars)) { return aitmt_fallback_normalize_resolved_query_vars($rewrite_query_vars); } $taxonomy_query_vars = aitmt_fallback_resolve_taxonomy_query_vars($path_segments); if (!empty($taxonomy_query_vars)) { return $taxonomy_query_vars; } $candidates = array( home_url(trailingslashit($path)), home_url($path), ); foreach (array_unique($candidates) as $candidate) { $post_id = url_to_postid($candidate); if ($post_id) { return aitmt_fallback_post_query_vars($post_id); } } $public_post_types = get_post_types(array('public' => true), 'names'); if (!empty($public_post_types)) { $post = get_page_by_path($path, OBJECT, $public_post_types); if ($post) { return aitmt_fallback_post_query_vars($post->ID); } } if (post_type_exists('product')) { $segments = array_values(array_filter(explode('/', $path))); $slug = end($segments); if ($slug) { $product = get_page_by_path($slug, OBJECT, 'product'); if ($product) { return aitmt_fallback_post_query_vars($product->ID); } } } return array(); } function aitmt_fallback_resolve_product_query_vars($segments) { $segments = array_values((array) $segments); if (count($segments) < 2 || !post_type_exists('product')) { return array(); } $first = sanitize_title((string) $segments[0]); $slug = sanitize_title((string) end($segments)); if (!$first || !$slug || in_array($first, aitmt_fallback_taxonomy_url_bases(), true)) { return array(); } $product = get_page_by_path($slug, OBJECT, 'product'); return $product ? aitmt_fallback_post_query_vars($product->ID) : array(); } function aitmt_fallback_normalize_resolved_query_vars($query_vars) { $query_vars = is_array($query_vars) ? $query_vars : array(); if (!empty($query_vars['product']) && post_type_exists('product')) { $product = get_page_by_path(sanitize_title((string) $query_vars['product']), OBJECT, 'product'); if ($product) { return aitmt_fallback_post_query_vars($product->ID); } unset($query_vars['taxonomy'], $query_vars['term'], $query_vars['product_cat'], $query_vars['product_tag']); } if (!empty($query_vars['name']) && !empty($query_vars['post_type']) && 'product' === $query_vars['post_type'] && post_type_exists('product')) { $product = get_page_by_path(sanitize_title((string) $query_vars['name']), OBJECT, 'product'); if ($product) { return aitmt_fallback_post_query_vars($product->ID); } } unset($query_vars['aitmt_lang'], $query_vars['aitmt_path']); return $query_vars; } function aitmt_fallback_taxonomy_url_bases() { $woocommerce_permalinks = function_exists('wc_get_permalink_structure') ? wc_get_permalink_structure() : (array) get_option('woocommerce_permalinks', array()); $bases = array('product-category', 'product-tag'); if (!empty($woocommerce_permalinks['category_base'])) { $bases[] = trim((string) $woocommerce_permalinks['category_base'], '/'); } if (!empty($woocommerce_permalinks['tag_base'])) { $bases[] = trim((string) $woocommerce_permalinks['tag_base'], '/'); } return array_values(array_filter(array_unique(array_map('sanitize_title', $bases)))); } function aitmt_fallback_resolve_rewrite_query_vars($path) { global $wp_rewrite; $path = trim(rawurldecode((string) $path), '/'); if ('' === $path || !$wp_rewrite || !method_exists($wp_rewrite, 'wp_rewrite_rules')) { return array(); } $rewrite_rules = $wp_rewrite->wp_rewrite_rules(); if (empty($rewrite_rules) || !is_array($rewrite_rules)) { return array(); } $decoded_path = rawurldecode($path); $candidates = array_unique(array( $path, trailingslashit($path), $decoded_path, trailingslashit($decoded_path), )); foreach ($rewrite_rules as $match => $query) { if (false !== strpos((string) $query, 'aitmt_lang')) { continue; } foreach ($candidates as $candidate) { if (!preg_match('#^' . $match . '#', $candidate, $matches)) { continue; } $query = preg_replace('!^.+\?!', '', (string) $query); $query = preg_replace_callback( '!\$matches\[([0-9]+)\]!', function ($found) use ($matches) { $index = isset($found[1]) ? absint($found[1]) : 0; return isset($matches[$index]) ? rawurlencode($matches[$index]) : ''; }, $query ); $query_vars = array(); wp_parse_str($query, $query_vars); if (empty($query_vars) || !is_array($query_vars)) { continue; } unset($query_vars['aitmt_lang'], $query_vars['aitmt_path']); return $query_vars; } } return array(); } function aitmt_fallback_resolve_taxonomy_query_vars($segments) { $segments = array_values((array) $segments); if (count($segments) < 2) { return array(); } $first = sanitize_title((string) $segments[0]); $last = sanitize_title((string) end($segments)); if (!$first || !$last) { return array(); } $woocommerce_permalinks = function_exists('wc_get_permalink_structure') ? wc_get_permalink_structure() : (array) get_option('woocommerce_permalinks', array()); $category_bases = array('product-category'); $tag_bases = array('product-tag'); if (!empty($woocommerce_permalinks['category_base'])) { $category_bases[] = trim((string) $woocommerce_permalinks['category_base'], '/'); } if (!empty($woocommerce_permalinks['tag_base'])) { $tag_bases[] = trim((string) $woocommerce_permalinks['tag_base'], '/'); } $category_bases = array_filter(array_unique(array_map('sanitize_title', $category_bases))); $tag_bases = array_filter(array_unique(array_map('sanitize_title', $tag_bases))); if (taxonomy_exists('product_cat') && in_array($first, $category_bases, true)) { $term = get_term_by('slug', $last, 'product_cat'); if ($term && !is_wp_error($term)) { return array( 'taxonomy' => 'product_cat', 'term' => $last, 'product_cat' => $last, ); } } if (taxonomy_exists('product_tag') && in_array($first, $tag_bases, true)) { $term = get_term_by('slug', $last, 'product_tag'); if ($term && !is_wp_error($term)) { return array( 'taxonomy' => 'product_tag', 'term' => $last, 'product_tag' => $last, ); } } return array(); } function aitmt_fallback_post_query_vars($post_id) { $post = get_post(absint($post_id)); if (!$post) { return array(); } if ('page' === $post->post_type) { return array('page_id' => (int) $post->ID); } $query_vars = array( 'post_type' => $post->post_type, 'name' => $post->post_name, ); $post_type_object = get_post_type_object($post->post_type); if ($post_type_object && !empty($post_type_object->query_var)) { $query_vars[(string) $post_type_object->query_var] = $post->post_name; } if ('post' === $post->post_type) { $query_vars['p'] = (int) $post->ID; } if ('product' === $post->post_type) { $query_vars['product'] = $post->post_name; } return $query_vars; } function aitmt_fallback_disable_language_canonical_redirect($redirect_url, $requested_url) { $language = aitmt_fallback_language_from_path(); if (!$language || !in_array($language, aitmt_fallback_enabled_languages(), true)) { return $redirect_url; } $path = (string) wp_parse_url(aitmt_fallback_request_uri(true), PHP_URL_PATH); $home_path = (string) wp_parse_url(home_url('/'), PHP_URL_PATH); $relative = trim(aitmt_fallback_strip_home_path($path, $home_path), '/'); $segments = aitmt_fallback_strip_language_segment(array_values(array_filter(explode('/', $relative)))); if (empty($segments)) { return $redirect_url; } return false; } function aitmt_fallback_redirect_conflicting_language_url() { if (is_admin() || wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST)) { return; } $request_uri = aitmt_fallback_request_uri(true); $path = (string) wp_parse_url($request_uri, PHP_URL_PATH); $home_path = (string) wp_parse_url(home_url('/'), PHP_URL_PATH); $relative = trim(aitmt_fallback_strip_home_path($path, $home_path), '/'); $segments = array_values(array_filter(explode('/', $relative))); $prefix_languages = aitmt_fallback_leading_language_segments($segments); $path_language = aitmt_fallback_language_from_path(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public language switching is a read-only GET action. $target_language = isset($_GET['lang']) ? aitmt_fallback_normalize_language(sanitize_key(wp_unslash((string) $_GET['lang']))) : ''; if ($target_language && in_array($target_language, aitmt_fallback_enabled_languages(), true) && $path_language && $path_language !== $target_language) { $clean_url = aitmt_fallback_clean_current_url_for_language($target_language, true); aitmt_fallback_redirect_if_url_changed($clean_url); return; } if (count($prefix_languages) > 1) { $clean_url = aitmt_fallback_clean_current_url_for_language((string) $prefix_languages[0], false); aitmt_fallback_redirect_if_url_changed($clean_url); return; } } function aitmt_fallback_redirect_if_url_changed($clean_url) { if (!$clean_url) { return; } $current_url = aitmt_fallback_current_url(); if ($current_url && untrailingslashit($current_url) === untrailingslashit($clean_url)) { return; } wp_safe_redirect($clean_url, 302); exit; } function aitmt_fallback_leading_language_segments($segments) { $segments = array_values((array) $segments); $all_languages = aitmt_fallback_enabled_languages(); $languages = array(); foreach ($segments as $segment) { $language = aitmt_fallback_normalize_language((string) $segment); if (!$language || !in_array($language, $all_languages, true)) { break; } $languages[] = $language; } return $languages; } function aitmt_fallback_clean_current_url_for_language($language, $force_query_language = false) { $language = aitmt_fallback_normalize_language($language); if (!$language || !in_array($language, aitmt_fallback_enabled_languages(), true)) { return ''; } $request_uri = aitmt_fallback_request_uri(true); $path = (string) wp_parse_url($request_uri, PHP_URL_PATH); $query = (string) wp_parse_url($request_uri, PHP_URL_QUERY); $query_args = array(); if ('' !== $query) { wp_parse_str($query, $query_args); } unset($query_args['lang']); $home_path = (string) wp_parse_url(home_url('/'), PHP_URL_PATH); $relative = trim(aitmt_fallback_strip_home_path($path, $home_path), '/'); $segments = aitmt_fallback_strip_language_segment(array_values(array_filter(explode('/', $relative)))); $options = aitmt_fallback_options(); $source = aitmt_fallback_source_language(); if ('directory' === $options['url_mode'] && !empty($segments) && ($language !== $source || '1' !== $options['hide_default_language'])) { array_unshift($segments, $language); $base_url = home_url(trailingslashit(implode('/', $segments))); } else { $base_url = home_url(empty($segments) ? '/' : trailingslashit(implode('/', $segments))); if ($force_query_language || $language !== $source || '1' !== $options['hide_default_language']) { $query_args['lang'] = $language; } } return add_query_arg($query_args, $base_url); } function aitmt_fallback_current_url() { $scheme = is_ssl() ? 'https://' : 'http://'; $host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field(wp_unslash((string) $_SERVER['HTTP_HOST'])) : ''; $request_uri = aitmt_fallback_request_uri(true); if (!$host || !$request_uri) { return ''; } return $scheme . $host . $request_uri; } function aitmt_fallback_capture_route_debug() { if (is_admin() || (function_exists('wp_doing_ajax') && wp_doing_ajax()) || !is_user_logged_in() || !current_user_can('manage_options')) { return; } $request_path = (string) wp_parse_url(aitmt_fallback_request_uri(true), PHP_URL_PATH); if (aitmt_fallback_is_untranslated_system_path($request_path) || aitmt_fallback_is_static_asset_path($request_path)) { return; } global $wp, $wp_query; $query_vars = array(); $interesting_keys = array( 'p', 'page_id', 'pagename', 'name', 'post_type', 'product', 'product_cat', 'product_tag', 'taxonomy', 'term', 'aitmt_lang', 'aitmt_path', ); foreach ($interesting_keys as $key) { if (isset($wp_query->query_vars[$key]) && '' !== (string) $wp_query->query_vars[$key]) { $query_vars[$key] = is_scalar($wp_query->query_vars[$key]) ? (string) $wp_query->query_vars[$key] : wp_json_encode($wp_query->query_vars[$key]); } } $route_debug = array( 'created_at' => current_time('mysql'), 'original_request_uri' => aitmt_fallback_request_uri(true), 'routed_request_uri' => aitmt_fallback_request_uri(false), 'path_language' => aitmt_fallback_language_from_path(), 'requested_language' => aitmt_fallback_requested_language(), 'current_language' => aitmt_fallback_current_language(), 'matched_rule' => isset($wp->matched_rule) ? (string) $wp->matched_rule : '', 'matched_query' => isset($wp->matched_query) ? (string) $wp->matched_query : '', 'query_vars' => $query_vars, 'queried_object_id' => get_queried_object_id(), 'is_404' => is_404() ? 'yes' : 'no', 'is_product' => function_exists('is_product') && is_product() ? 'yes' : 'no', 'is_shop' => function_exists('is_shop') && is_shop() ? 'yes' : 'no', 'is_product_category' => function_exists('is_product_category') && is_product_category() ? 'yes' : 'no', ); update_option('aitmt_last_route_debug', $route_debug, false); aitmt_fallback_log_event('route', (string) $route_debug['original_request_uri'], $route_debug); } function aitmt_fallback_redirect_language_home() { $language = get_query_var('aitmt_lang'); if (!$language || get_query_var('aitmt_path') || is_front_page()) { return; } $language = aitmt_fallback_normalize_language((string) $language); $source = aitmt_fallback_source_language(); if (!$language || $language === $source || !in_array($language, aitmt_fallback_enabled_languages(), true)) { return; } wp_safe_redirect(add_query_arg('lang', $language, home_url('/')), 302); exit; } function aitmt_fallback_plugin_links($links) { $url = admin_url('admin.php?page=' . AITMT_PUBLIC_SLUG); array_unshift($links, '' . esc_html__('Settings', 'ait-multilingual-translate') . ''); return $links; } function aitmt_fallback_admin_redirect_url_from_post($default_page = 'ait-multilingual-translate') { $allowed_pages = array( 'ait-multilingual-translate', 'ait-multilingual-translate-translations', 'ait-multilingual-translate-scanner', 'ait-multilingual-translate-debugger', 'ait-multilingual-translate-onboarding', 'ait-multilingual-translate-report-bug', 'ait-multilingual-translate-feedback', 'ait-multilingual-translate-support', ); // phpcs:disable WordPress.Security.NonceVerification.Missing -- Redirect metadata is only consumed after the calling admin-post handler verifies its nonce. $page = isset($_POST['redirect_page']) ? sanitize_key(wp_unslash((string) $_POST['redirect_page'])) : $default_page; if (!in_array($page, $allowed_pages, true)) { $page = $default_page; } $args = array('page' => $page); if ('ait-multilingual-translate-translations' === $page) { $per_page = isset($_POST['per_page']) ? absint($_POST['per_page']) : 25; $args['per_page'] = in_array($per_page, array(25, 50, 100), true) ? $per_page : 25; $args['paged'] = isset($_POST['paged']) ? max(1, absint($_POST['paged'])) : 1; $matrix_search = isset($_POST['matrix_search']) ? sanitize_text_field(wp_unslash((string) $_POST['matrix_search'])) : ''; $matrix_status = isset($_POST['matrix_status']) ? sanitize_key(wp_unslash((string) $_POST['matrix_status'])) : 'all'; if ('' !== $matrix_search) { $args['matrix_search'] = $matrix_search; } if (in_array($matrix_status, array('translated', 'untranslated'), true)) { $args['matrix_status'] = $matrix_status; } } // phpcs:enable WordPress.Security.NonceVerification.Missing return add_query_arg($args, admin_url('admin.php')); } function aitmt_fallback_maybe_redirect_onboarding() { if (!current_user_can('manage_options') || !is_admin() || wp_doing_ajax()) { return; } if ('1' !== get_option('aitmt_onboarding_pending', '0')) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Onboarding redirect checks a read-only admin page slug. $page = isset($_GET['page']) ? sanitize_key(wp_unslash((string) $_GET['page'])) : ''; if ('ait-multilingual-translate-onboarding' === $page) { return; } delete_option('aitmt_onboarding_pending'); wp_safe_redirect(admin_url('admin.php?page=ait-multilingual-translate-onboarding')); exit; } function aitmt_fallback_admin_mode() { $options = aitmt_fallback_options(); return 'advanced' === (string) $options['admin_mode'] ? 'advanced' : 'basic'; } function aitmt_fallback_support_development_url() { $options = aitmt_fallback_options(); $url = !empty($options['donation_paypal_url']) ? $options['donation_paypal_url'] : 'https://paypal.me/wpaitranslate'; return esc_url_raw($url); } function aitmt_fallback_support_development_button($class = 'button') { return sprintf( '%3$s', esc_attr($class), esc_url(aitmt_fallback_support_development_url()), esc_html__('Support Development', 'ait-multilingual-translate') ); } function aitmt_fallback_allowed_switcher_html() { $allowed = wp_kses_allowed_html('post'); $attrs = array( 'aria-current' => true, 'aria-hidden' => true, 'aria-label' => true, 'class' => true, 'data-aitmt-language-switcher' => true, 'data-aitmt-no-translate' => true, 'decoding' => true, 'href' => true, 'hreflang' => true, 'id' => true, 'lang' => true, 'loading' => true, 'rel' => true, 'role' => true, 'src' => true, 'target' => true, 'translate' => true, ); foreach (array('a', 'div', 'details', 'img', 'nav', 'span', 'summary') as $tag) { $allowed[$tag] = isset($allowed[$tag]) ? array_merge($allowed[$tag], $attrs) : $attrs; } return $allowed; } function aitmt_fallback_kses_switcher($html) { return wp_kses((string) $html, aitmt_fallback_allowed_switcher_html()); } function aitmt_fallback_support_development_block($compact = false) { ?>

AITMT_VERSION, 'plugin_edition' => aitmt_fallback_edition_label(), 'wp_version' => get_bloginfo('version'), 'php_version' => PHP_VERSION, 'woocommerce_version' => $wc_version, 'active_theme' => $theme->get('Name') . ' ' . $theme->get('Version'), 'active_plugins' => $active_plugins, 'enabled_languages' => aitmt_fallback_enabled_languages(), 'source_language' => aitmt_fallback_source_language(), 'current_language' => aitmt_fallback_current_language(), 'selected_provider' => aitmt_fallback_provider_label($options['provider']), 'queue_status' => array( 'queued_strings' => aitmt_fallback_queued_count(), 'saved_translations' => aitmt_fallback_translation_count(), 'batch_size' => (int) $options['max_segments_per_request'], 'background_queue' => '1' === $options['cron_enabled'] ? 'enabled' : 'disabled', ), 'route_status' => array_intersect_key($route, array_flip(array('created_at', 'original_request_uri', 'routed_request_uri', 'path_language', 'requested_language', 'current_language', 'matched_rule', 'matched_query', 'is_404', 'is_product', 'is_shop', 'is_product_category'))), ); if ($include_log) { $events = get_option('aitmt_debug_events', array()); $report['debug_events'] = is_array($events) ? array_slice($events, 0, 30) : array(); $path = aitmt_fallback_debug_log_path(); if (file_exists($path) && is_readable($path)) { $report['debug_file_tail'] = substr(aitmt_fallback_read_local_file($path), -12000); } } return aitmt_fallback_redact_sensitive_text(wp_json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } function aitmt_fallback_mail_public_beta_message($subject, $fields, $include_log) { $body = ''; foreach ($fields as $label => $value) { $value = (string) $value; if ('Email' !== (string) $label) { $value = aitmt_fallback_redact_sensitive_text($value); } $body .= $label . ":\n" . $value . "\n\n"; } $body .= "Technical report:\n" . aitmt_fallback_technical_report($include_log) . "\n"; return wp_mail('info@itdesign.biz', $subject, $body, array('Content-Type: text/plain; charset=UTF-8')); } function aitmt_fallback_submit_bug_report_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_submit_bug_report'); if (empty($_POST['aitmt_consent'])) { wp_safe_redirect(add_query_arg('aitmt_sent', 'consent', admin_url('admin.php?page=ait-multilingual-translate-report-bug'))); exit; } $fields = array( 'Name' => isset($_POST['name']) ? sanitize_text_field(wp_unslash((string) $_POST['name'])) : '', 'Email' => isset($_POST['email']) ? sanitize_email(wp_unslash((string) $_POST['email'])) : '', 'Website' => isset($_POST['website']) ? esc_url_raw(wp_unslash((string) $_POST['website'])) : home_url('/'), 'Problem type' => isset($_POST['problem_type']) ? sanitize_text_field(wp_unslash((string) $_POST['problem_type'])) : '', 'Short description' => isset($_POST['short_description']) ? sanitize_textarea_field(wp_unslash((string) $_POST['short_description'])) : '', 'Steps to reproduce' => isset($_POST['steps']) ? sanitize_textarea_field(wp_unslash((string) $_POST['steps'])) : '', ); $include_log = !empty($_POST['attach_log']); $sent = aitmt_fallback_mail_public_beta_message('[AIT Multilingual Translate Public Beta] Bug report', $fields, $include_log); wp_safe_redirect(add_query_arg('aitmt_sent', $sent ? '1' : '0', admin_url('admin.php?page=ait-multilingual-translate-report-bug'))); exit; } function aitmt_fallback_submit_feedback_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_submit_feedback'); if (empty($_POST['aitmt_consent'])) { wp_safe_redirect(add_query_arg('aitmt_sent', 'consent', admin_url('admin.php?page=ait-multilingual-translate-feedback'))); exit; } $fields = array( 'Name' => isset($_POST['name']) ? sanitize_text_field(wp_unslash((string) $_POST['name'])) : '', 'Email' => isset($_POST['email']) ? sanitize_email(wp_unslash((string) $_POST['email'])) : '', 'Type' => isset($_POST['feedback_type']) ? sanitize_text_field(wp_unslash((string) $_POST['feedback_type'])) : '', 'Message' => isset($_POST['message']) ? sanitize_textarea_field(wp_unslash((string) $_POST['message'])) : '', ); $sent = aitmt_fallback_mail_public_beta_message('[AIT Multilingual Translate Public Beta] Feedback', $fields, false); wp_safe_redirect(add_query_arg('aitmt_sent', $sent ? '1' : '0', admin_url('admin.php?page=ait-multilingual-translate-feedback'))); exit; } function aitmt_fallback_onboarding_save_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_onboarding_save'); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The settings array is sanitized by aitmt_fallback_sanitize() below. $raw_input = isset($_POST['aitmt_options']) ? wp_unslash($_POST['aitmt_options']) : array(); $input = is_array($raw_input) ? $raw_input : array(); $old_options = aitmt_fallback_options(); $options = aitmt_fallback_sanitize(wp_parse_args($input, $old_options)); update_option('aitmt_options', $options, false); aitmt_fallback_register_rewrites(); flush_rewrite_rules(false); update_option('aitmt_rewrite_version', AITMT_VERSION, false); wp_safe_redirect(add_query_arg('aitmt_saved', '1', admin_url('admin.php?page=ait-multilingual-translate-onboarding'))); exit; } function aitmt_fallback_onboarding_finish_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_onboarding_finish'); update_option('aitmt_onboarding_completed', current_time('mysql'), false); delete_option('aitmt_onboarding_pending'); wp_safe_redirect(admin_url('admin.php?page=' . AITMT_PUBLIC_SLUG)); exit; } function aitmt_fallback_sanitize($input) { $input = is_array($input) ? $input : array(); $languages = array(); if (!empty($input['enabled_languages'])) { $raw_languages = is_array($input['enabled_languages']) ? $input['enabled_languages'] : explode(',', (string) $input['enabled_languages']); foreach ($raw_languages as $language) { $language = strtolower(trim(str_replace('_', '-', (string) $language))); $language = sanitize_key(strtok($language, '-')); if ($language) { $languages[] = $language; } } } $source_language = empty($input['source_language']) ? '' : strtolower((string) $input['source_language']); $source_language = 'auto' === $source_language ? '' : sanitize_key($source_language); $url_mode = isset($input['url_mode']) && 'query' === $input['url_mode'] ? 'query' : 'directory'; $admin_mode = isset($input['admin_mode']) && 'advanced' === sanitize_key((string) $input['admin_mode']) ? 'advanced' : 'basic'; $allowed_providers = array('openai', 'gemini', 'grok', 'google_translate', 'deepl'); $provider = isset($input['provider']) ? sanitize_key((string) $input['provider']) : 'openai'; $provider = in_array($provider, $allowed_providers, true) ? $provider : 'openai'; $selector_style = isset($input['selector_style']) && 'list' === $input['selector_style'] ? 'list' : 'dropdown'; $max_segments = isset($input['max_segments_per_request']) ? absint($input['max_segments_per_request']) : 5; $max_segments = min(100, max(1, $max_segments)); $queue_lock_ttl = isset($input['queue_lock_ttl']) ? absint($input['queue_lock_ttl']) : 600; $queue_lock_ttl = min(DAY_IN_SECONDS, max(60, $queue_lock_ttl)); $processing_ttl = isset($input['processing_ttl']) ? absint($input['processing_ttl']) : 900; $processing_ttl = min(DAY_IN_SECONDS, max(120, $processing_ttl)); $retry_limit = isset($input['retry_limit']) ? absint($input['retry_limit']) : 3; $retry_limit = min(10, max(1, $retry_limit)); $daily_limit = isset($input['quota_daily_chars']) ? absint($input['quota_daily_chars']) : 5000; $monthly_limit = isset($input['quota_monthly_chars']) ? absint($input['quota_monthly_chars']) : 50000; $max_chars_per_request = isset($input['max_chars_per_request']) ? absint($input['max_chars_per_request']) : 2000; $estimated_cost_limit = isset($input['estimated_cost_limit']) ? (float) str_replace(',', '.', sanitize_text_field((string) $input['estimated_cost_limit'])) : 0.02; $estimated_cost_limit = max(0, $estimated_cost_limit); $translation_temperature = isset($input['translation_temperature']) ? (float) str_replace(',', '.', sanitize_text_field((string) $input['translation_temperature'])) : 0.1; $translation_temperature = max(0, min(1, $translation_temperature)); $quality_mode = isset($input['quality_mode']) ? sanitize_key((string) $input['quality_mode']) : 'cheap'; $quality_mode = array_key_exists($quality_mode, aitmt_fallback_quality_mode_options()) ? $quality_mode : 'cheap'; $deepl_plan = isset($input['deepl_plan']) && 'pro' === $input['deepl_plan'] ? 'pro' : 'free'; $translation_mode = isset($input['translation_mode']) ? sanitize_key((string) $input['translation_mode']) : 'neutral'; $translation_mode = array_key_exists($translation_mode, aitmt_fallback_translation_mode_options()) ? $translation_mode : 'neutral'; $custom_instruction = empty($input['custom_translation_instruction']) ? '' : sanitize_textarea_field((string) $input['custom_translation_instruction']); $custom_instruction = function_exists('mb_substr') ? mb_substr($custom_instruction, 0, 500, 'UTF-8') : substr($custom_instruction, 0, 500); return array( 'source_language' => $source_language, 'enabled_languages' => array_values(array_unique($languages)), 'admin_mode' => $admin_mode, 'provider' => $provider, 'openai_api_key' => empty($input['openai_api_key']) ? '' : sanitize_text_field((string) $input['openai_api_key']), 'openai_model' => empty($input['openai_model']) ? 'gpt-4o-mini' : sanitize_text_field((string) $input['openai_model']), 'gemini_api_key' => empty($input['gemini_api_key']) ? '' : sanitize_text_field((string) $input['gemini_api_key']), 'gemini_model' => empty($input['gemini_model']) ? 'gemini-2.5-flash' : sanitize_text_field((string) $input['gemini_model']), 'grok_api_key' => empty($input['grok_api_key']) ? '' : sanitize_text_field((string) $input['grok_api_key']), 'grok_model' => empty($input['grok_model']) ? 'grok-3-mini' : sanitize_text_field((string) $input['grok_model']), 'google_translate_api_key' => empty($input['google_translate_api_key']) ? '' : sanitize_text_field((string) $input['google_translate_api_key']), 'deepl_api_key' => empty($input['deepl_api_key']) ? '' : sanitize_text_field((string) $input['deepl_api_key']), 'deepl_plan' => $deepl_plan, 'quota_daily_chars' => $daily_limit, 'quota_monthly_chars' => $monthly_limit, 'max_chars_per_request' => $max_chars_per_request, 'estimated_cost_limit' => $estimated_cost_limit, 'translation_temperature' => $translation_temperature, 'quality_mode' => $quality_mode, 'translation_mode' => $translation_mode, 'custom_translation_instruction' => $custom_instruction, 'url_mode' => $url_mode, 'hide_default_language' => empty($input['hide_default_language']) ? '0' : '1', 'auto_translate' => empty($input['auto_translate']) ? '0' : '1', 'translate_on_page_load' => empty($input['translate_on_page_load']) ? '0' : '1', 'queue_missing' => empty($input['queue_missing']) ? '0' : '1', 'scan_on_save' => empty($input['scan_on_save']) ? '0' : '1', 'cron_enabled' => empty($input['cron_enabled']) ? '0' : '1', 'draft_mode' => empty($input['draft_mode']) ? '0' : '1', 'translate_attributes' => empty($input['translate_attributes']) ? '0' : '1', 'max_segments_per_request' => $max_segments, 'selector_style' => $selector_style, 'selector_show_flags' => empty($input['selector_show_flags']) ? '0' : '1', 'selector_show_names' => empty($input['selector_show_names']) ? '0' : '1', 'selector_show_codes' => empty($input['selector_show_codes']) ? '0' : '1', 'selector_custom_css' => empty($input['selector_custom_css']) ? '' : substr(wp_strip_all_tags((string) $input['selector_custom_css']), 0, 5000), 'selector_header' => empty($input['selector_header']) ? '0' : '1', 'selector_footer' => empty($input['selector_footer']) ? '0' : '1', 'frontend_editor' => empty($input['frontend_editor']) ? '0' : '1', 'donation_coffee_url' => empty($input['donation_coffee_url']) ? '' : esc_url_raw((string) $input['donation_coffee_url']), 'donation_paypal_url' => empty($input['donation_paypal_url']) ? '' : esc_url_raw((string) $input['donation_paypal_url']), 'queue_lock_ttl' => $queue_lock_ttl, 'processing_ttl' => $processing_ttl, 'retry_limit' => $retry_limit, ); } function aitmt_fallback_languages() { return array( 'af' => 'Afrikaans', 'sq' => 'Albanian', 'am' => 'Amharic', 'ar' => 'Arabic', 'hy' => 'Armenian', 'az' => 'Azerbaijani', 'eu' => 'Basque', 'be' => 'Belarusian', 'bn' => 'Bengali', 'bs' => 'Bosnian', 'bg' => 'Bulgarian', 'ca' => 'Catalan', 'ceb' => 'Cebuano', 'zh' => 'Chinese', 'co' => 'Corsican', 'hr' => 'Croatian', 'cs' => 'Czech', 'da' => 'Danish', 'nl' => 'Dutch', 'en' => 'English', 'eo' => 'Esperanto', 'et' => 'Estonian', 'fi' => 'Finnish', 'fr' => 'French', 'fy' => 'Frisian', 'gl' => 'Galician', 'ka' => 'Georgian', 'de' => 'German', 'el' => 'Greek', 'gu' => 'Gujarati', 'ht' => 'Haitian Creole', 'ha' => 'Hausa', 'haw' => 'Hawaiian', 'he' => 'Hebrew', 'hi' => 'Hindi', 'hmn' => 'Hmong', 'hu' => 'Hungarian', 'is' => 'Icelandic', 'ig' => 'Igbo', 'id' => 'Indonesian', 'ga' => 'Irish', 'it' => 'Italian', 'ja' => 'Japanese', 'jv' => 'Javanese', 'kn' => 'Kannada', 'kk' => 'Kazakh', 'km' => 'Khmer', 'ko' => 'Korean', 'ku' => 'Kurdish', 'ky' => 'Kyrgyz', 'lo' => 'Lao', 'la' => 'Latin', 'lv' => 'Latvian', 'lt' => 'Lithuanian', 'lb' => 'Luxembourgish', 'mk' => 'Macedonian', 'mg' => 'Malagasy', 'ms' => 'Malay', 'ml' => 'Malayalam', 'mt' => 'Maltese', 'mi' => 'Maori', 'mr' => 'Marathi', 'mn' => 'Mongolian', 'my' => 'Myanmar', 'ne' => 'Nepali', 'no' => 'Norwegian', 'ny' => 'Nyanja', 'ps' => 'Pashto', 'fa' => 'Persian', 'pl' => 'Polish', 'pt' => 'Portuguese', 'pa' => 'Punjabi', 'ro' => 'Romanian', 'ru' => 'Russian', 'sm' => 'Samoan', 'gd' => 'Scottish Gaelic', 'sr' => 'Serbian', 'st' => 'Sesotho', 'sn' => 'Shona', 'sd' => 'Sindhi', 'si' => 'Sinhala', 'sk' => 'Slovak', 'sl' => 'Slovenian', 'so' => 'Somali', 'es' => 'Spanish', 'su' => 'Sundanese', 'sw' => 'Swahili', 'sv' => 'Swedish', 'tl' => 'Tagalog', 'tg' => 'Tajik', 'ta' => 'Tamil', 'te' => 'Telugu', 'th' => 'Thai', 'tr' => 'Turkish', 'uk' => 'Ukrainian', 'ur' => 'Urdu', 'ug' => 'Uyghur', 'uz' => 'Uzbek', 'vi' => 'Vietnamese', 'cy' => 'Welsh', 'xh' => 'Xhosa', 'yi' => 'Yiddish', 'yo' => 'Yoruba', 'zu' => 'Zulu', ); } function aitmt_fallback_default_options() { return array( 'source_language' => '', 'enabled_languages' => array(), 'admin_mode' => 'basic', 'provider' => 'openai', 'openai_api_key' => '', 'openai_model' => 'gpt-4o-mini', 'gemini_api_key' => '', 'gemini_model' => 'gemini-2.5-flash', 'grok_api_key' => '', 'grok_model' => 'grok-3-mini', 'google_translate_api_key' => '', 'deepl_api_key' => '', 'deepl_plan' => 'free', 'quota_daily_chars' => 5000, 'quota_monthly_chars' => 50000, 'max_chars_per_request' => 2000, 'estimated_cost_limit' => 0.02, 'translation_temperature' => 0.1, 'quality_mode' => 'cheap', 'translation_mode' => 'neutral', 'custom_translation_instruction' => '', 'url_mode' => 'directory', 'hide_default_language' => '1', 'auto_translate' => '1', 'translate_on_page_load' => '0', 'queue_missing' => '0', 'scan_on_save' => '0', 'cron_enabled' => '0', 'draft_mode' => '0', 'translate_attributes' => '1', 'max_segments_per_request' => 5, 'selector_style' => 'dropdown', 'selector_show_flags' => '0', 'selector_show_names' => '1', 'selector_show_codes' => '0', 'selector_custom_css' => '', 'selector_header' => '0', 'selector_footer' => '0', 'frontend_editor' => '1', 'donation_coffee_url' => '', 'donation_paypal_url' => 'https://paypal.me/wpaitranslate', 'queue_lock_ttl' => 600, 'processing_ttl' => 900, 'retry_limit' => 3, ); } function aitmt_fallback_safe_api_defaults() { return array( 'quality_mode' => 'cheap', 'translation_temperature' => 0.1, 'quota_daily_chars' => 5000, 'quota_monthly_chars' => 50000, 'max_chars_per_request' => 2000, 'estimated_cost_limit' => 0.02, 'max_segments_per_request' => 5, 'queue_missing' => '0', 'scan_on_save' => '0', 'cron_enabled' => '0', 'translate_on_page_load' => '0', 'draft_mode' => '0', 'translate_attributes' => '1', ); } function aitmt_fallback_options() { $options = get_option('aitmt_options', array()); if (!is_array($options)) { $options = array(); } $options = wp_parse_args($options, aitmt_fallback_default_options()); if (!is_array($options['enabled_languages'])) { $options['enabled_languages'] = array_filter(array_map('trim', explode(',', (string) $options['enabled_languages']))); } return $options; } function aitmt_fallback_active_provider() { $options = aitmt_fallback_options(); $provider = isset($options['provider']) ? sanitize_key((string) $options['provider']) : 'openai'; return in_array($provider, array('openai', 'gemini', 'grok', 'google_translate', 'deepl'), true) ? $provider : 'openai'; } function aitmt_fallback_provider_label($provider = '') { $provider = $provider ? sanitize_key($provider) : aitmt_fallback_active_provider(); if ('gemini' === $provider) { return 'Gemini'; } if ('grok' === $provider) { return 'Grok / xAI'; } if ('google_translate' === $provider) { return 'Google Translate'; } if ('deepl' === $provider) { return 'DeepL'; } return 'OpenAI'; } function aitmt_fallback_provider_catalog() { return array( 'openai' => array( 'label' => 'OpenAI', 'status' => 'active', 'badges' => array('Tone of Voice', 'SEO Mode', 'Fast', 'Cheap', 'HTML Aware', 'Batch Support'), ), 'gemini' => array( 'label' => 'Gemini', 'status' => 'active', 'badges' => array('Tone of Voice', 'SEO Mode', 'Flash', 'Cheap', 'HTML Aware', 'Batch Support'), ), 'grok' => array( 'label' => 'Grok / xAI', 'status' => 'active', 'badges' => array('Tone of Voice', 'SEO Mode', 'HTML Aware', 'Batch Support'), ), 'google_translate' => array( 'label' => 'Google Translate', 'status' => 'active', 'badges' => array('Fast', 'Batch Support'), ), 'deepl' => array( 'label' => 'DeepL', 'status' => 'active', 'badges' => array('Fast', 'Batch Support'), ), 'claude' => array( 'label' => 'Claude', 'status' => 'planned', 'badges' => array('Tone of Voice', 'SEO Mode', 'HTML Aware'), ), 'yandex_translate' => array( 'label' => 'Yandex Translate', 'status' => 'planned', 'badges' => array('Fast'), ), 'yandexgpt' => array( 'label' => 'YandexGPT / Alice', 'status' => 'planned', 'badges' => array('Tone of Voice', 'SEO Mode'), ), 'more' => array( 'label' => 'More providers', 'status' => 'planned', 'badges' => array('Mistral', 'DeepSeek', 'Qwen', 'Moonshot AI', 'Baidu ERNIE', 'Cohere', 'Together AI', 'OpenRouter'), ), ); } function aitmt_fallback_provider_pricing_profiles() { return array( 'openai' => array( 'badge' => 'usage', 'badge_text' => __('Usage Based', 'ait-multilingual-translate'), 'summary' => __('OpenAI API usage is billed by your OpenAI account. Start with a small budget and the cheapest recommended translation model.', 'ait-multilingual-translate'), 'recommendations' => array( __('Monthly budget: $5 while testing', 'ait-multilingual-translate'), __('Soft alert: $3, hard limit: $5 where available', 'ait-multilingual-translate'), __('Use gpt-4o-mini with temperature 0.1', 'ait-multilingual-translate'), __('Keep Translation Memory, duplicate detection, and queue mode enabled', 'ait-multilingual-translate'), __('Use small batches of 3-5 strings until quota is confirmed', 'ait-multilingual-translate'), ), 'links' => array( 'docs' => array('label' => __('Documentation', 'ait-multilingual-translate'), 'url' => 'https://platform.openai.com/docs/'), 'pricing' => array('label' => __('Pricing', 'ait-multilingual-translate'), 'url' => 'https://openai.com/api/pricing/'), 'quota' => array('label' => __('Quota settings', 'ait-multilingual-translate'), 'url' => 'https://platform.openai.com/settings/organization/limits'), 'budget' => array('label' => __('Budget settings', 'ait-multilingual-translate'), 'url' => 'https://platform.openai.com/settings/organization/billing/overview'), ), ), 'gemini' => array( 'badge' => 'free', 'badge_text' => __('Free Tier Available', 'ait-multilingual-translate'), 'summary' => __('Gemini may offer a free tier, but rate limits can be low and paid billing may apply depending on your Google AI/GCP configuration.', 'ait-multilingual-translate'), 'recommendations' => array( __('Use the free tier where available', 'ait-multilingual-translate'), __('Use a Flash model for testing', 'ait-multilingual-translate'), __('Enable quota limits and billing alerts in Google tools', 'ait-multilingual-translate'), __('Keep batches small and process the queue manually first', 'ait-multilingual-translate'), ), 'links' => array( 'docs' => array('label' => __('Documentation', 'ait-multilingual-translate'), 'url' => 'https://ai.google.dev/gemini-api/docs'), 'pricing' => array('label' => __('Pricing', 'ait-multilingual-translate'), 'url' => 'https://ai.google.dev/gemini-api/docs/pricing'), 'quota' => array('label' => __('Quota settings', 'ait-multilingual-translate'), 'url' => 'https://ai.google.dev/gemini-api/docs/rate-limits'), 'budget' => array('label' => __('Budget settings', 'ait-multilingual-translate'), 'url' => 'https://cloud.google.com/billing/docs/how-to/budgets'), ), ), 'grok' => array( 'badge' => 'paid', 'badge_text' => __('Paid API', 'ait-multilingual-translate'), 'summary' => __('xAI API access usually requires an active paid account or credits. Use the lowest-cost model available to your account.', 'ait-multilingual-translate'), 'recommendations' => array( __('Confirm account credits before testing', 'ait-multilingual-translate'), __('Use the lowest-cost mini/lightweight model where available', 'ait-multilingual-translate'), __('Set usage alerts in xAI Console if available', 'ait-multilingual-translate'), __('Run one small manual queue batch before Translate All', 'ait-multilingual-translate'), ), 'links' => array( 'docs' => array('label' => __('Documentation', 'ait-multilingual-translate'), 'url' => 'https://docs.x.ai/docs'), 'pricing' => array('label' => __('Pricing', 'ait-multilingual-translate'), 'url' => 'https://x.ai/api'), 'quota' => array('label' => __('Quota settings', 'ait-multilingual-translate'), 'url' => 'https://console.x.ai/'), 'budget' => array('label' => __('Budget settings', 'ait-multilingual-translate'), 'url' => 'https://console.x.ai/'), ), ), 'google_translate' => array( 'badge' => 'usage', 'badge_text' => __('Usage Based', 'ait-multilingual-translate'), 'summary' => __('Google Cloud Translation API is billed by characters. Set Google Cloud quotas and billing alerts before bulk translation.', 'ait-multilingual-translate'), 'recommendations' => array( __('Enable Cloud Translation API only for the intended project', 'ait-multilingual-translate'), __('Set daily quota and request limits', 'ait-multilingual-translate'), __('Create billing alerts and budgets in Google Cloud Billing', 'ait-multilingual-translate'), __('Keep plugin monthly and per-request character limits enabled', 'ait-multilingual-translate'), ), 'links' => array( 'docs' => array('label' => __('Documentation', 'ait-multilingual-translate'), 'url' => 'https://cloud.google.com/translate/docs'), 'pricing' => array('label' => __('Pricing', 'ait-multilingual-translate'), 'url' => 'https://cloud.google.com/translate/pricing'), 'quota' => array('label' => __('Quota settings', 'ait-multilingual-translate'), 'url' => 'https://cloud.google.com/translate/quotas'), 'budget' => array('label' => __('Budget settings', 'ait-multilingual-translate'), 'url' => 'https://cloud.google.com/billing/docs/how-to/budgets'), ), ), 'deepl' => array( 'badge' => 'free', 'badge_text' => __('Free Tier Available', 'ait-multilingual-translate'), 'summary' => __('DeepL API can be used with a free endpoint when your key supports it. Monitor monthly character usage in your DeepL account.', 'ait-multilingual-translate'), 'recommendations' => array( __('Use DeepL Free endpoint when possible', 'ait-multilingual-translate'), __('Monitor monthly character usage in DeepL account', 'ait-multilingual-translate'), __('Use plugin character limits as an extra safety stop', 'ait-multilingual-translate'), __('Run small manual batches before translating the full queue', 'ait-multilingual-translate'), ), 'links' => array( 'docs' => array('label' => __('Documentation', 'ait-multilingual-translate'), 'url' => 'https://developers.deepl.com/docs'), 'pricing' => array('label' => __('Pricing', 'ait-multilingual-translate'), 'url' => 'https://www.deepl.com/pro-api'), 'quota' => array('label' => __('Quota settings', 'ait-multilingual-translate'), 'url' => 'https://www.deepl.com/account/usage'), 'budget' => array('label' => __('Budget settings', 'ait-multilingual-translate'), 'url' => 'https://www.deepl.com/account/usage'), ), ), ); } function aitmt_fallback_provider_pricing_profile($provider) { $provider = sanitize_key((string) $provider); $profiles = aitmt_fallback_provider_pricing_profiles(); return isset($profiles[$provider]) ? $profiles[$provider] : array( 'badge' => 'usage', 'badge_text' => __('Usage Based', 'ait-multilingual-translate'), 'summary' => __('Provider pricing depends on your provider account plan.', 'ait-multilingual-translate'), 'recommendations' => array(), 'links' => array(), ); } function aitmt_fallback_cost_safety_issues($options = array()) { $options = $options ? wp_parse_args($options, aitmt_fallback_default_options()) : aitmt_fallback_options(); $issues = array(); if (empty($options['quota_daily_chars'])) { $issues[] = __('No plugin-side daily character limit is configured.', 'ait-multilingual-translate'); } if (empty($options['quota_monthly_chars'])) { $issues[] = __('No plugin-side monthly character limit is configured.', 'ait-multilingual-translate'); } if (empty($options['max_chars_per_request'])) { $issues[] = __('No plugin-side per-request character limit is configured.', 'ait-multilingual-translate'); } if ((int) $options['max_segments_per_request'] > 5) { $issues[] = __('Batch size is higher than the safest free-tier recommendation of 3-5 strings.', 'ait-multilingual-translate'); } if ('1' === (string) $options['cron_enabled']) { $issues[] = __('Background queue processing is enabled. Use it only after confirming provider quotas.', 'ait-multilingual-translate'); } if ('1' === (string) $options['translate_on_page_load']) { $issues[] = __('Page-load translation is enabled. This can create API usage while visitors browse.', 'ait-multilingual-translate'); } if ('1' === (string) $options['scan_on_save']) { $issues[] = __('Auto scan on save is enabled. Disable it during imports or large WooCommerce edits.', 'ait-multilingual-translate'); } return $issues; } function aitmt_fallback_provider_monthly_cost_estimate($provider, $characters) { $provider = sanitize_key((string) $provider); $characters = max(0, absint($characters)); if ($characters < 1) { return __('Unknown (depends on your provider plan)', 'ait-multilingual-translate'); } if ('google_translate' === $provider) { return '$' . number_format(($characters / 1000000) * 20, 2); } if (in_array($provider, array('openai', 'gemini', 'grok'), true)) { $tokens = (int) ceil($characters / 4); return '$' . number_format(aitmt_fallback_estimate_provider_cost($provider, aitmt_fallback_provider_model($provider), $tokens, $tokens), 4); } return __('Unknown (depends on your provider plan)', 'ait-multilingual-translate'); } function aitmt_fallback_render_provider_safety_assistant($provider) { $provider = sanitize_key((string) $provider); $profile = aitmt_fallback_provider_pricing_profile($provider); ?>

0 ? $request_chars : 2000; $estimated_tokens = (int) ceil($chars_for_request / 4); $estimated_requests = $monthly_chars > 0 ? (int) ceil($monthly_chars / max(1, $chars_for_request)) : 0; /* translators: %s: Number of characters. */ $monthly_usage = $monthly_chars > 0 ? sprintf(_n('%s character', '%s characters', $monthly_chars, 'ait-multilingual-translate'), number_format_i18n($monthly_chars)) : __('Unlimited in plugin settings', 'ait-multilingual-translate'); /* translators: %s: Number of characters. */ $daily_usage = $daily_chars > 0 ? sprintf(_n('%s character', '%s characters', $daily_chars, 'ait-multilingual-translate'), number_format_i18n($daily_chars)) : __('Unlimited in plugin settings', 'ait-multilingual-translate'); $monthly_cost = $monthly_chars > 0 ? aitmt_fallback_provider_monthly_cost_estimate($provider, $monthly_chars) : __('Unknown (depends on your provider plan)', 'ait-multilingual-translate'); ?>

0 ? number_format_i18n($estimated_requests) : __('Unknown', 'ait-multilingual-translate')); ?>
__('Cheap', 'ait-multilingual-translate'), 'balanced' => __('Balanced', 'ait-multilingual-translate'), 'premium' => __('Premium', 'ait-multilingual-translate'), ); } function aitmt_fallback_quality_mode_label($mode = '') { $options = aitmt_fallback_options(); $mode = $mode ? sanitize_key($mode) : sanitize_key((string) $options['quality_mode']); $modes = aitmt_fallback_quality_mode_options(); return isset($modes[$mode]) ? $modes[$mode] : $modes['cheap']; } function aitmt_fallback_recommended_provider_model($provider, $quality = '') { $provider = sanitize_key((string) $provider); $quality = $quality ? sanitize_key((string) $quality) : sanitize_key((string) aitmt_fallback_options()['quality_mode']); $models = array( 'openai' => array( 'cheap' => 'gpt-4o-mini', 'balanced' => 'gpt-4o-mini', 'premium' => 'gpt-4o', ), 'gemini' => array( 'cheap' => 'gemini-2.5-flash', 'balanced' => 'gemini-2.5-flash', 'premium' => 'gemini-2.5-pro', ), 'grok' => array( 'cheap' => 'grok-3-mini', 'balanced' => 'grok-3-mini', 'premium' => 'grok-4', ), ); return isset($models[$provider][$quality]) ? $models[$provider][$quality] : ''; } function aitmt_fallback_translation_temperature() { $options = aitmt_fallback_options(); $temperature = isset($options['translation_temperature']) ? (float) $options['translation_temperature'] : 0.1; return max(0, min(1, $temperature)); } function aitmt_fallback_translation_mode_options() { return array( 'neutral' => array( 'label' => __('Neutral / Accurate', 'ait-multilingual-translate'), 'instruction' => 'Translate accurately and preserve the original meaning, formatting and tone.', ), 'seo' => array( 'label' => __('SEO Optimized', 'ait-multilingual-translate'), 'instruction' => 'Translate and adapt the text for SEO in the target language. Keep it natural, search-friendly and relevant. Do not add unrelated keywords.', ), 'marketing' => array( 'label' => __('Marketing', 'ait-multilingual-translate'), 'instruction' => 'Translate the text in a persuasive marketing style. Keep the message natural, clear and conversion-oriented.', ), 'ecommerce' => array( 'label' => __('eCommerce', 'ait-multilingual-translate'), 'instruction' => 'Translate the text for an eCommerce website. Keep product names, attributes, benefits and call-to-action phrases natural and commercially effective.', ), 'formal' => array( 'label' => __('Formal', 'ait-multilingual-translate'), 'instruction' => 'Translate using a formal and professional tone.', ), 'casual' => array( 'label' => __('Casual', 'ait-multilingual-translate'), 'instruction' => 'Translate using a natural and casual tone.', ), 'technical' => array( 'label' => __('Technical', 'ait-multilingual-translate'), 'instruction' => 'Translate using precise technical language. Preserve technical terms and product names.', ), 'legal' => array( 'label' => __('Legal', 'ait-multilingual-translate'), 'instruction' => 'Translate using a formal legal style. Preserve meaning carefully and avoid creative rewriting.', ), 'luxury' => array( 'label' => __('Luxury Brand', 'ait-multilingual-translate'), 'instruction' => 'Translate using an elegant premium brand tone. Keep the language refined, polished and natural.', ), 'friendly' => array( 'label' => __('Friendly', 'ait-multilingual-translate'), 'instruction' => 'Translate using a warm, friendly and approachable tone.', ), 'custom' => array( 'label' => __('Custom Prompt', 'ait-multilingual-translate'), 'instruction' => '', ), ); } function aitmt_fallback_translation_mode_label($mode = '') { $options = aitmt_fallback_options(); $mode = $mode ? sanitize_key($mode) : sanitize_key((string) $options['translation_mode']); $modes = aitmt_fallback_translation_mode_options(); return isset($modes[$mode]) ? $modes[$mode]['label'] : $modes['neutral']['label']; } function aitmt_fallback_translation_instruction() { $options = aitmt_fallback_options(); $mode = sanitize_key((string) $options['translation_mode']); $modes = aitmt_fallback_translation_mode_options(); if ('custom' === $mode && !empty($options['custom_translation_instruction'])) { $instruction = sanitize_textarea_field((string) $options['custom_translation_instruction']); return function_exists('mb_substr') ? mb_substr($instruction, 0, 500, 'UTF-8') : substr($instruction, 0, 500); } return isset($modes[$mode]) && !empty($modes[$mode]['instruction']) ? $modes[$mode]['instruction'] : $modes['neutral']['instruction']; } function aitmt_fallback_translate_with_provider($segments, $source_language, $target_language, $context = array()) { $provider = aitmt_fallback_active_provider(); $deduped = aitmt_fallback_dedupe_segments((array) $segments); $segments_for_provider = $deduped['segments']; $source_type = isset($context['source_type']) ? aitmt_fallback_normalize_source_type((string) $context['source_type']) : 'provider'; $source_type = '' === $source_type ? 'provider' : $source_type; $batch_id = isset($context['batch_id']) ? sanitize_key((string) $context['batch_id']) : ''; $cooldown = aitmt_fallback_provider_cooldown_remaining($provider); $quota_check = aitmt_fallback_provider_quota_check($provider, $segments_for_provider); if (empty($segments_for_provider)) { return array(); } if ($cooldown > 0) { $error = new WP_Error( 'aitmt_provider_cooldown', sprintf('Provider cooldown is active for %d more second(s) after a quota or rate-limit error.', $cooldown), array('status' => 429) ); aitmt_fallback_provider_request_log($provider, $source_language, $target_language, $segments_for_provider, 'blocked', $error->get_error_message(), $batch_id, $source_type); return $error; } if (is_wp_error($quota_check)) { aitmt_fallback_provider_request_log($provider, $source_language, $target_language, $segments_for_provider, 'blocked', $quota_check->get_error_message(), $batch_id, $source_type); return $quota_check; } if ('gemini' === $provider) { $result = aitmt_fallback_gemini_translate_batch($segments_for_provider, $source_language, $target_language); } elseif ('grok' === $provider) { $result = aitmt_fallback_grok_translate_batch($segments_for_provider, $source_language, $target_language); } elseif ('google_translate' === $provider) { $result = aitmt_fallback_google_translate_batch($segments_for_provider, $source_language, $target_language); } elseif ('deepl' === $provider) { $result = aitmt_fallback_deepl_translate_batch($segments_for_provider, $source_language, $target_language); } else { $result = aitmt_fallback_openai_translate_batch($segments_for_provider, $source_language, $target_language); } if (!is_wp_error($result)) { $result = aitmt_fallback_restore_deduped_translations((array) $result, $deduped['map']); aitmt_fallback_provider_request_log($provider, $source_language, $target_language, $segments_for_provider, 'success', '', $batch_id, $source_type); aitmt_fallback_provider_quota_record($provider, $segments_for_provider); aitmt_fallback_provider_stats_record( $provider, array( 'requests' => 1, 'input_tokens' => aitmt_fallback_estimate_tokens_for_segments($segments_for_provider), 'output_tokens' => aitmt_fallback_estimate_tokens_for_segments($result), 'duplicate_skipped' => $deduped['duplicate_skipped'], 'model' => aitmt_fallback_provider_model($provider), ) ); } else { aitmt_fallback_provider_request_log($provider, $source_language, $target_language, $segments_for_provider, 'error', $result->get_error_message(), $batch_id, $source_type); aitmt_fallback_provider_stats_record( $provider, array( 'requests' => 1, 'input_tokens' => aitmt_fallback_estimate_tokens_for_segments($segments_for_provider), 'output_tokens' => 0, 'duplicate_skipped' => $deduped['duplicate_skipped'], 'model' => aitmt_fallback_provider_model($provider), ) ); } return $result; } function aitmt_fallback_dedupe_segments($segments) { $unique = array(); $lookup = array(); $map = array(); $duplicate_skipped = 0; foreach ((array) $segments as $hash => $text) { $hash = (string) $hash; $text = (string) $text; $dedupe_key = md5($text); if (isset($lookup[$dedupe_key])) { $map[$hash] = $lookup[$dedupe_key]; $duplicate_skipped++; continue; } $lookup[$dedupe_key] = $hash; $unique[$hash] = $text; } return array( 'segments' => $unique, 'map' => $map, 'duplicate_skipped' => $duplicate_skipped, ); } function aitmt_fallback_restore_deduped_translations($translations, $map) { foreach ((array) $map as $hash => $source_hash) { if (isset($translations[$source_hash])) { $translations[$hash] = $translations[$source_hash]; } } return $translations; } function aitmt_fallback_provider_char_count($segments) { $total = 0; foreach ((array) $segments as $text) { $text = (string) $text; $total += function_exists('mb_strlen') ? mb_strlen($text, 'UTF-8') : strlen($text); } return $total; } function aitmt_fallback_provider_usage() { $usage = get_option('aitmt_provider_usage', array()); return is_array($usage) ? $usage : array(); } function aitmt_fallback_provider_stats() { $stats = get_option('aitmt_provider_stats', array()); return is_array($stats) ? $stats : array(); } function aitmt_fallback_provider_stats_for($provider = '') { $provider = $provider ? sanitize_key($provider) : aitmt_fallback_active_provider(); $stats = aitmt_fallback_provider_stats(); return isset($stats[$provider]) && is_array($stats[$provider]) ? $stats[$provider] : array(); } function aitmt_fallback_provider_stats_record($provider, $data) { $provider = sanitize_key((string) $provider); if ('' === $provider) { return; } $stats = aitmt_fallback_provider_stats(); $current = isset($stats[$provider]) && is_array($stats[$provider]) ? $stats[$provider] : array(); $input_tokens = isset($data['input_tokens']) ? absint($data['input_tokens']) : 0; $output_tokens = isset($data['output_tokens']) ? absint($data['output_tokens']) : 0; $estimated_cost = aitmt_fallback_estimate_provider_cost($provider, isset($data['model']) ? (string) $data['model'] : '', $input_tokens, $output_tokens); $stats[$provider] = array( 'requests' => absint($current['requests'] ?? 0) + absint($data['requests'] ?? 0), 'input_tokens' => absint($current['input_tokens'] ?? 0) + $input_tokens, 'output_tokens' => absint($current['output_tokens'] ?? 0) + $output_tokens, 'estimated_cost' => (float) ($current['estimated_cost'] ?? 0) + $estimated_cost, 'cache_hits' => absint($current['cache_hits'] ?? 0) + absint($data['cache_hits'] ?? 0), 'duplicate_skipped' => absint($current['duplicate_skipped'] ?? 0) + absint($data['duplicate_skipped'] ?? 0), 'last_provider' => $provider, 'last_model' => isset($data['model']) ? sanitize_text_field((string) $data['model']) : (string) ($current['last_model'] ?? ''), 'updated_at' => current_time('mysql'), ); update_option('aitmt_provider_stats', $stats, false); } function aitmt_fallback_provider_stats_record_cache_hits($provider, $count) { $count = absint($count); if ($count < 1) { return; } aitmt_fallback_provider_stats_record( $provider, array( 'requests' => 0, 'input_tokens' => 0, 'output_tokens' => 0, 'cache_hits' => $count, 'duplicate_skipped' => 0, 'model' => aitmt_fallback_provider_model($provider), ) ); } function aitmt_fallback_estimate_tokens_for_segments($segments) { $chars = aitmt_fallback_provider_char_count((array) $segments); return (int) ceil($chars / 4); } function aitmt_fallback_estimate_provider_cost($provider, $model, $input_tokens, $output_tokens) { $provider = sanitize_key((string) $provider); $model = strtolower((string) $model); $rates = array( 'openai' => array('input' => 0.15, 'output' => 0.60), 'gemini' => array('input' => 0.10, 'output' => 0.40), 'grok' => array('input' => 0.30, 'output' => 0.50), ); if ('openai' === $provider && false !== strpos($model, 'gpt-4o') && false === strpos($model, 'mini')) { $rates[$provider] = array('input' => 2.50, 'output' => 10.00); } if (!isset($rates[$provider])) { return 0.0; } return (($input_tokens / 1000000) * $rates[$provider]['input']) + (($output_tokens / 1000000) * $rates[$provider]['output']); } function aitmt_fallback_provider_usage_key($provider, $period) { return sanitize_key($provider) . '_' . $period; } function aitmt_fallback_provider_chars_used($provider, $period) { $usage = aitmt_fallback_provider_usage(); $key = aitmt_fallback_provider_usage_key($provider, $period); return isset($usage[$key]) ? absint($usage[$key]) : 0; } function aitmt_fallback_provider_quota_check($provider, $segments) { $options = aitmt_fallback_options(); $chars = aitmt_fallback_provider_char_count($segments); $daily_limit = isset($options['quota_daily_chars']) ? absint($options['quota_daily_chars']) : 0; $monthly_limit = isset($options['quota_monthly_chars']) ? absint($options['quota_monthly_chars']) : 0; $request_char_limit = isset($options['max_chars_per_request']) ? absint($options['max_chars_per_request']) : 0; $cost_limit = isset($options['estimated_cost_limit']) ? (float) $options['estimated_cost_limit'] : 0.0; $day = gmdate('Ymd'); $month = gmdate('Ym'); if ($request_char_limit > 0 && $chars > $request_char_limit) { return new WP_Error( 'aitmt_request_char_limit', sprintf('Local request character limit reached for %s. This batch has %d characters and the configured limit is %d.', aitmt_fallback_provider_label($provider), $chars, $request_char_limit), array('status' => 429) ); } if ($cost_limit > 0) { $estimated_tokens = aitmt_fallback_estimate_tokens_for_segments($segments); $estimated_cost = aitmt_fallback_estimate_provider_cost($provider, aitmt_fallback_provider_model($provider), $estimated_tokens, $estimated_tokens); if ($estimated_cost > $cost_limit) { return new WP_Error( 'aitmt_estimated_cost_limit', sprintf('Estimated cost limit reached for %s. Estimated request cost is %.6f and the configured limit is %.6f.', aitmt_fallback_provider_label($provider), $estimated_cost, $cost_limit), array('status' => 429) ); } } if ($daily_limit > 0 && aitmt_fallback_provider_chars_used($provider, $day) + $chars > $daily_limit) { return new WP_Error( 'aitmt_daily_quota', sprintf('Local daily character quota reached for %s. Used %d of %d characters.', aitmt_fallback_provider_label($provider), aitmt_fallback_provider_chars_used($provider, $day), $daily_limit), array('status' => 429) ); } if ($monthly_limit > 0 && aitmt_fallback_provider_chars_used($provider, $month) + $chars > $monthly_limit) { return new WP_Error( 'aitmt_monthly_quota', sprintf('Local monthly character quota reached for %s. Used %d of %d characters.', aitmt_fallback_provider_label($provider), aitmt_fallback_provider_chars_used($provider, $month), $monthly_limit), array('status' => 429) ); } return true; } function aitmt_fallback_provider_quota_record($provider, $segments) { $chars = aitmt_fallback_provider_char_count($segments); if ($chars <= 0) { return; } $usage = aitmt_fallback_provider_usage(); $day_key = aitmt_fallback_provider_usage_key($provider, gmdate('Ymd')); $month_key = aitmt_fallback_provider_usage_key($provider, gmdate('Ym')); $usage[$day_key] = isset($usage[$day_key]) ? absint($usage[$day_key]) + $chars : $chars; $usage[$month_key] = isset($usage[$month_key]) ? absint($usage[$month_key]) + $chars : $chars; foreach ($usage as $key => $value) { if (false === strpos($key, sanitize_key($provider) . '_')) { continue; } $period = substr($key, strlen(sanitize_key($provider)) + 1); if (preg_match('/^\d{6}$/', $period) && $period < gmdate('Ym', strtotime('-13 months'))) { unset($usage[$key]); } if (preg_match('/^\d{8}$/', $period) && $period < gmdate('Ymd', strtotime('-45 days'))) { unset($usage[$key]); } } update_option('aitmt_provider_usage', $usage, false); } function aitmt_fallback_provider_request_log($provider, $source_language, $target_language, $segments, $status, $error = '', $batch_id = '', $source_type = 'provider') { $entry = array( 'created_at' => current_time('mysql'), 'provider' => sanitize_key((string) $provider), 'source_type' => sanitize_key((string) $source_type), 'source_language' => aitmt_fallback_normalize_language($source_language), 'target_language' => aitmt_fallback_normalize_language($target_language), 'characters' => aitmt_fallback_provider_char_count((array) $segments), 'batch_id' => sanitize_key((string) $batch_id), 'status' => sanitize_key((string) $status), 'error' => '' === $error ? '' : substr(wp_strip_all_tags((string) $error), 0, 300), ); $entries = get_option('aitmt_provider_request_log', array()); $entries = is_array($entries) ? $entries : array(); array_unshift($entries, $entry); $entries = array_slice($entries, 0, 500); update_option('aitmt_provider_request_log', $entries, false); } function aitmt_fallback_provider_request_log_entries() { $entries = get_option('aitmt_provider_request_log', array()); return is_array($entries) ? $entries : array(); } function aitmt_fallback_provider_request_log_summary() { $summary = array( 'today' => 0, 'month' => 0, 'success' => 0, 'blocked' => 0, 'error' => 0, ); $today = gmdate('Y-m-d'); $month = gmdate('Y-m'); foreach (aitmt_fallback_provider_request_log_entries() as $entry) { $created_at = isset($entry['created_at']) ? (string) $entry['created_at'] : ''; $characters = isset($entry['characters']) ? absint($entry['characters']) : 0; $status = isset($entry['status']) ? sanitize_key((string) $entry['status']) : ''; if (0 === strpos($created_at, $today)) { $summary['today'] += $characters; } if (0 === strpos($created_at, $month)) { $summary['month'] += $characters; } if (isset($summary[$status])) { $summary[$status]++; } } return $summary; } function aitmt_fallback_provider_cooldown_key($provider) { return 'aitmt_provider_cooldown_' . sanitize_key($provider); } function aitmt_fallback_provider_cooldown_remaining($provider = '') { $provider = $provider ? sanitize_key($provider) : aitmt_fallback_active_provider(); $until = (int) get_transient(aitmt_fallback_provider_cooldown_key($provider)); return max(0, $until - time()); } function aitmt_fallback_set_provider_cooldown($provider, $seconds) { $provider = $provider ? sanitize_key($provider) : aitmt_fallback_active_provider(); $seconds = max(60, min(DAY_IN_SECONDS, absint($seconds))); set_transient(aitmt_fallback_provider_cooldown_key($provider), time() + $seconds, $seconds); } function aitmt_fallback_normalize_language($language) { $language = strtolower(str_replace('_', '-', trim((string) $language))); $parts = explode('-', $language); return sanitize_key(isset($parts[0]) ? $parts[0] : $language); } function aitmt_fallback_source_language() { $options = aitmt_fallback_options(); if (!empty($options['source_language'])) { return aitmt_fallback_normalize_language($options['source_language']); } $locale = function_exists('determine_locale') ? determine_locale() : get_locale(); return aitmt_fallback_normalize_language($locale); } function aitmt_fallback_enabled_languages() { $options = aitmt_fallback_options(); $languages = array_merge(array(aitmt_fallback_source_language()), (array) $options['enabled_languages']); $languages = array_map('aitmt_fallback_normalize_language', $languages); return array_values(array_unique(array_filter($languages))); } function aitmt_fallback_gettext($translation, $text, $domain) { if ('ait-multilingual-translate' !== $domain || '' === $text) { return $translation; } $language = aitmt_fallback_source_language(); if ('en' === $language) { return $translation; } $dictionary = aitmt_fallback_plugin_dictionary($language); return isset($dictionary[$text]) ? $dictionary[$text] : $translation; } function aitmt_fallback_plugin_dictionary($language) { $language = aitmt_fallback_normalize_language($language); $ru = array( 'AI Translate' => 'AI Translate', 'AIT Multilingual Translate' => 'AIT Multilingual Translate', 'Dashboard' => 'Панель', 'Translations' => 'Переводы', 'Scanner' => 'Сканер', 'Debugger' => 'Отладчик', 'Settings' => 'Настройки', 'Languages' => 'Языки', 'AI Provider' => 'AI-провайдер', 'Switcher' => 'Селектор', 'Language Switcher' => 'Селектор языка', 'Source language' => 'Основной язык', 'Target languages' => 'Языки перевода', 'Provider' => 'Провайдер', 'OpenAI API key' => 'OpenAI API ключ', 'OpenAI model' => 'Модель OpenAI', 'Gemini API key' => 'Gemini API ключ', 'Gemini model' => 'Модель Gemini', 'Grok API key' => 'Grok API ключ', 'Grok model' => 'Модель Grok', 'Google Translate API key' => 'Google Translate API ключ', 'DeepL API key' => 'DeepL API ключ', 'DeepL plan' => 'Тариф DeepL', 'Quota control' => 'Контроль квоты', 'Daily character limit' => 'Дневной лимит символов', 'Monthly character limit' => 'Месячный лимит символов', 'Translation behavior' => 'Поведение перевода', 'Batch size' => 'Размер пакета', 'URLs and SEO' => 'URL и SEO', 'URL mode' => 'Режим URL', 'Style' => 'Стиль', 'Display parts' => 'Что показывать', 'Automatic placement' => 'Автоматическое размещение', 'Frontend editor' => 'Редактор на фронтенде', 'Selector custom CSS' => 'CSS селектора', 'Save settings' => 'Сохранить настройки', 'String Scanner' => 'Сканер строк', 'Translation Queue' => 'Очередь перевода', 'Translations Matrix' => 'Матрица переводов', 'Scan new strings' => 'Сканировать новые строки', 'Process queue' => 'Обработать очередь', 'Per page' => 'На странице', 'Previous' => 'Назад', 'Next' => 'Вперёд', 'Save visible translations' => 'Сохранить видимые переводы', 'Export / Import' => 'Экспорт / импорт', 'Export language' => 'Язык экспорта', 'Import language' => 'Язык импорта', 'Export translations' => 'Экспортировать переводы', 'Import translations' => 'Импортировать переводы', 'Select language' => 'Выберите язык', 'Dropdown' => 'Выпадающий список', 'List' => 'Список', 'Flags' => 'Флаги', 'Language names' => 'Названия языков', 'Language codes' => 'Коды языков', 'Status:' => 'Статус:', ); $de = array( 'Dashboard' => 'Dashboard', 'Translations' => 'Übersetzungen', 'Scanner' => 'Scanner', 'Debugger' => 'Debugger', 'Settings' => 'Einstellungen', 'Languages' => 'Sprachen', 'AI Provider' => 'KI-Anbieter', 'Switcher' => 'Sprachumschalter', 'Language Switcher' => 'Sprachumschalter', 'Source language' => 'Ausgangssprache', 'Target languages' => 'Zielsprachen', 'Provider' => 'Anbieter', 'Translation behavior' => 'Übersetzungsverhalten', 'Batch size' => 'Batch-Größe', 'URLs and SEO' => 'URLs und SEO', 'URL mode' => 'URL-Modus', 'Style' => 'Stil', 'Display parts' => 'Anzeigeelemente', 'Automatic placement' => 'Automatische Platzierung', 'Frontend editor' => 'Frontend-Editor', 'Selector custom CSS' => 'Eigenes CSS für den Umschalter', 'Save settings' => 'Einstellungen speichern', 'String Scanner' => 'String-Scanner', 'Translation Queue' => 'Übersetzungswarteschlange', 'Translations Matrix' => 'Übersetzungsmatrix', 'Scan new strings' => 'Neue Strings scannen', 'Process queue' => 'Warteschlange verarbeiten', 'Per page' => 'Pro Seite', 'Previous' => 'Zurück', 'Next' => 'Weiter', 'Save visible translations' => 'Sichtbare Übersetzungen speichern', 'Export / Import' => 'Export / Import', 'Export translations' => 'Übersetzungen exportieren', 'Import translations' => 'Übersetzungen importieren', 'Select language' => 'Sprache auswählen', 'Dropdown' => 'Dropdown', 'List' => 'Liste', 'Flags' => 'Flaggen', 'Language names' => 'Sprachnamen', 'Language codes' => 'Sprachcodes', 'Status:' => 'Status:', ); $ka = array( 'Dashboard' => 'მართვის პანელი', 'Translations' => 'თარგმანები', 'Scanner' => 'სკანერი', 'Debugger' => 'გამართვა', 'Settings' => 'პარამეტრები', 'Languages' => 'ენები', 'AI Provider' => 'AI პროვაიდერი', 'Switcher' => 'ენის გადამრთველი', 'Language Switcher' => 'ენის გადამრთველი', 'Source language' => 'საწყისი ენა', 'Target languages' => 'სამიზნე ენები', 'Provider' => 'პროვაიდერი', 'Translation behavior' => 'თარგმნის ქცევა', 'Batch size' => 'პაკეტის ზომა', 'URLs and SEO' => 'URL და SEO', 'URL mode' => 'URL რეჟიმი', 'Style' => 'სტილი', 'Display parts' => 'ჩვენების ნაწილები', 'Automatic placement' => 'ავტომატური განთავსება', 'Frontend editor' => 'ფრონტენდის რედაქტორი', 'Selector custom CSS' => 'სელექტორის CSS', 'Save settings' => 'პარამეტრების შენახვა', 'String Scanner' => 'სტრიქონების სკანერი', 'Translation Queue' => 'თარგმნის რიგი', 'Translations Matrix' => 'თარგმანების მატრიცა', 'Scan new strings' => 'ახალი სტრიქონების სკანირება', 'Process queue' => 'რიგის დამუშავება', 'Per page' => 'გვერდზე', 'Previous' => 'წინა', 'Next' => 'შემდეგი', 'Save visible translations' => 'ხილული თარგმანების შენახვა', 'Export / Import' => 'ექსპორტი / იმპორტი', 'Export translations' => 'თარგმანების ექსპორტი', 'Import translations' => 'თარგმანების იმპორტი', 'Select language' => 'ენის არჩევა', 'Dropdown' => 'ჩამოსაშლელი', 'List' => 'სია', 'Flags' => 'დროშები', 'Language names' => 'ენების სახელები', 'Language codes' => 'ენის კოდები', 'Status:' => 'სტატუსი:', ); if ('ru' === $language) { return $ru; } if ('de' === $language) { return $de; } if ('ka' === $language) { return $ka; } return array(); } function aitmt_fallback_current_language() { $enabled = aitmt_fallback_enabled_languages(); $source = aitmt_fallback_source_language(); $requested_language = aitmt_fallback_requested_language(); if ($requested_language && in_array($requested_language, $enabled, true)) { return $requested_language; } if (!empty($_COOKIE['aitmt_language'])) { $cookie_language = aitmt_fallback_normalize_language(sanitize_key(wp_unslash((string) $_COOKIE['aitmt_language']))); if ($cookie_language && in_array($cookie_language, $enabled, true)) { return $cookie_language; } } return $source; } function aitmt_fallback_requested_language() { $enabled = aitmt_fallback_enabled_languages(); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Public language switching is a read-only GET action. if (isset($_GET['lang'])) { $language = aitmt_fallback_normalize_language(sanitize_key(wp_unslash((string) $_GET['lang']))); if (in_array($language, $enabled, true)) { return $language; } } // phpcs:enable WordPress.Security.NonceVerification.Recommended if (!empty($GLOBALS['aitmt_request_language'])) { $language = aitmt_fallback_normalize_language((string) $GLOBALS['aitmt_request_language']); if (in_array($language, $enabled, true)) { return $language; } } $rewrite_language = get_query_var('aitmt_lang'); if ($rewrite_language) { $language = aitmt_fallback_normalize_language((string) $rewrite_language); if (in_array($language, $enabled, true)) { return $language; } } $path_language = aitmt_fallback_language_from_path(); if ($path_language && in_array($path_language, $enabled, true)) { return $path_language; } return ''; } function aitmt_fallback_remember_language() { if (is_admin() || wp_doing_ajax()) { return; } $language = aitmt_fallback_requested_language(); if (!$language || !in_array($language, aitmt_fallback_enabled_languages(), true)) { return; } $path = defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/'; $domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : ''; setcookie('aitmt_language', $language, time() + MONTH_IN_SECONDS, $path, $domain, is_ssl(), true); $_COOKIE['aitmt_language'] = $language; } function aitmt_fallback_language_from_path() { if (!empty($GLOBALS['aitmt_path_language'])) { return aitmt_fallback_normalize_language((string) $GLOBALS['aitmt_path_language']); } $request_uri = aitmt_fallback_request_uri(true); $path = (string) wp_parse_url($request_uri, PHP_URL_PATH); $home_path = (string) wp_parse_url(home_url('/'), PHP_URL_PATH); $relative = aitmt_fallback_strip_home_path($path, $home_path); $segments = array_values(array_filter(explode('/', trim($relative, '/')))); $language = isset($segments[0]) ? aitmt_fallback_normalize_language($segments[0]) : ''; return $language && in_array($language, aitmt_fallback_enabled_languages(), true) ? $language : ''; } function aitmt_fallback_language_url($language) { $language = aitmt_fallback_normalize_language($language); $options = aitmt_fallback_options(); $source = aitmt_fallback_source_language(); $current = aitmt_fallback_current_language(); $request_uri = aitmt_fallback_request_uri(true); $path = (string) wp_parse_url($request_uri, PHP_URL_PATH); $query = (string) wp_parse_url($request_uri, PHP_URL_QUERY); $query_args = array(); if ('' !== $query) { wp_parse_str($query, $query_args); } unset($query_args['lang']); if ('query' === $options['url_mode'] || !file_exists(AITMT_PLUGIN_DIR . 'includes/class-aitmt-activator.php')) { $relative = trim(aitmt_fallback_strip_home_path($path, (string) wp_parse_url(home_url('/'), PHP_URL_PATH)), '/'); $segments = aitmt_fallback_strip_language_segment(array_values(array_filter(explode('/', $relative)))); $base_url = home_url(empty($segments) ? '/' : trailingslashit(implode('/', $segments))); if ($language !== $source || '1' !== $options['hide_default_language'] || $current !== $source) { $query_args['lang'] = $language; } return add_query_arg($query_args, $base_url); } $home_path = (string) wp_parse_url(home_url('/'), PHP_URL_PATH); $relative = trim(aitmt_fallback_strip_home_path($path, $home_path), '/'); $segments = aitmt_fallback_strip_language_segment(array_values(array_filter(explode('/', $relative)))); if (empty($segments)) { if ($language !== $source || '1' !== $options['hide_default_language'] || $current !== $source) { $query_args['lang'] = $language; } return add_query_arg($query_args, home_url('/')); } if ($language !== $source || '1' !== $options['hide_default_language']) { array_unshift($segments, $language); } elseif ($current !== $source) { $query_args['lang'] = $language; } $new_path = implode('/', $segments); $url = home_url($new_path ? trailingslashit($new_path) : '/'); return add_query_arg($query_args, $url); } function aitmt_fallback_strip_language_segment($segments) { $segments = array_values((array) $segments); $all_languages = aitmt_fallback_enabled_languages(); while (isset($segments[0]) && in_array(aitmt_fallback_normalize_language($segments[0]), $all_languages, true)) { array_shift($segments); $segments = array_values($segments); } return array_values($segments); } function aitmt_fallback_apply_language_to_url($url, $language) { $url = trim(html_entity_decode((string) $url, ENT_QUOTES, 'UTF-8')); if ('' === $url || '#' === $url || 0 === strpos($url, '#')) { return $url; } if (preg_match('/^(mailto|tel|sms|javascript|data):/i', $url)) { return $url; } $language = aitmt_fallback_normalize_language($language); if (!$language || !in_array($language, aitmt_fallback_enabled_languages(), true)) { return $url; } $parts = wp_parse_url($url); if (false === $parts) { return $url; } $home_parts = wp_parse_url(home_url('/')); $home_host = isset($home_parts['host']) ? strtolower($home_parts['host']) : ''; $url_host = isset($parts['host']) ? strtolower($parts['host']) : ''; if ($url_host && $home_host && $url_host !== $home_host) { return $url; } $path = isset($parts['path']) ? $parts['path'] : '/'; if (isset($url[0]) && '?' === $url[0]) { $path = (string) wp_parse_url(aitmt_fallback_request_uri(true), PHP_URL_PATH); } if ('' === $path) { $path = '/'; } if (isset($parts['scheme']) && !in_array(strtolower($parts['scheme']), array('http', 'https'), true)) { return $url; } $home_path = isset($home_parts['path']) ? $home_parts['path'] : '/'; $relative = trim(aitmt_fallback_strip_home_path($path, $home_path), '/'); if (preg_match('/\.(jpg|jpeg|png|gif|webp|svg|css|js|json|xml|pdf|zip|rar|7z|mp4|mp3|woff|woff2|ttf|eot)$/i', $relative)) { return $url; } if (0 === strpos($relative, 'wp-admin') || 0 === strpos($relative, 'wp-login.php') || 0 === strpos($relative, 'wp-content') || 0 === strpos($relative, 'wp-includes')) { return $url; } $query_args = array(); if (!empty($parts['query'])) { wp_parse_str($parts['query'], $query_args); } unset($query_args['lang']); $fragment = empty($parts['fragment']) ? '' : '#' . $parts['fragment']; $options = aitmt_fallback_options(); $source = aitmt_fallback_source_language(); $segments = aitmt_fallback_strip_language_segment(array_values(array_filter(explode('/', $relative)))); if ('query' === $options['url_mode'] || empty($segments)) { $base_url = home_url(empty($segments) ? '/' : trailingslashit(implode('/', $segments))); if ($language !== $source || '1' !== $options['hide_default_language']) { $query_args['lang'] = $language; } return add_query_arg($query_args, $base_url) . $fragment; } if ($language !== $source || '1' !== $options['hide_default_language']) { array_unshift($segments, $language); } $translated_url = home_url(trailingslashit(implode('/', $segments))); return add_query_arg($query_args, $translated_url) . $fragment; } function aitmt_fallback_strip_home_path($path, $home_path) { $home_path = '/' . trim((string) $home_path, '/'); if ('/' === $home_path) { return '/' . ltrim((string) $path, '/'); } if (0 === strpos((string) $path, $home_path)) { return '/' . ltrim(substr((string) $path, strlen($home_path)), '/'); } return '/' . ltrim((string) $path, '/'); } function aitmt_fallback_shortcode($atts = array()) { $options = aitmt_fallback_options(); $atts = shortcode_atts( array( 'style' => $options['selector_style'], ), is_array($atts) ? $atts : array(), 'aitmt_language_switcher' ); return aitmt_fallback_render_switcher((string) $atts['style']); } function aitmt_fallback_render_header_switcher() { $options = aitmt_fallback_options(); if ('1' !== $options['selector_header']) { return; } echo '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Switcher HTML is generated internally with escaped URLs, labels, and attributes. echo wp_kses(aitmt_fallback_render_switcher($options['selector_style']), aitmt_fallback_allowed_switcher_html()); echo '
'; } function aitmt_fallback_render_footer_switcher() { $options = aitmt_fallback_options(); if ('1' !== $options['selector_footer']) { return; } echo ''; } function aitmt_fallback_boolish($value, $default = false) { if (null === $value) { return (bool) $default; } if (is_bool($value)) { return $value; } $value = strtolower(trim((string) $value)); if (in_array($value, array('1', 'true', 'yes', 'on'), true)) { return true; } if (in_array($value, array('0', 'false', 'no', 'off'), true)) { return false; } return (bool) $default; } function aitmt_fallback_switcher_default_display_args() { $options = aitmt_fallback_options(); return array( 'show_flags' => '1' === (string) $options['selector_show_flags'], 'show_names' => '1' === (string) $options['selector_show_names'], 'show_codes' => '1' === (string) $options['selector_show_codes'], 'show_current' => true, 'hide_current' => false, 'orientation' => 'horizontal', 'open_current_page' => true, ); } function aitmt_fallback_normalize_switcher_display_args($args = array()) { $args = is_array($args) ? $args : array(); $display = wp_parse_args($args, aitmt_fallback_switcher_default_display_args()); $display['show_flags'] = aitmt_fallback_boolish($display['show_flags']); $display['show_names'] = aitmt_fallback_boolish($display['show_names']); $display['show_codes'] = aitmt_fallback_boolish($display['show_codes']); $display['show_current'] = aitmt_fallback_boolish($display['show_current'], true); $display['hide_current'] = aitmt_fallback_boolish($display['hide_current']); $display['open_current_page'] = aitmt_fallback_boolish($display['open_current_page'], true); $display['orientation'] = in_array((string) $display['orientation'], array('horizontal', 'vertical'), true) ? (string) $display['orientation'] : 'horizontal'; if (!$display['show_flags'] && !$display['show_names'] && !$display['show_codes']) { $display['show_codes'] = true; } return $display; } function aitmt_fallback_switcher_display_for_layout($layout, $display) { $display = aitmt_fallback_normalize_switcher_display_args($display); if ('flags_only' === $layout) { $display['show_flags'] = true; $display['show_names'] = false; $display['show_codes'] = false; } elseif ('flags_name' === $layout) { $display['show_flags'] = true; $display['show_names'] = true; } elseif ('name_only' === $layout) { $display['show_flags'] = false; $display['show_names'] = true; $display['show_codes'] = false; } return $display; } function aitmt_fallback_render_switcher($style = '', $display_args = array()) { $languages = aitmt_fallback_enabled_languages(); if (count($languages) < 2) { return ''; } $options = aitmt_fallback_options(); $style = sanitize_key($style ? $style : $options['selector_style']); $style = in_array($style, array('dropdown', 'list', 'buttons', 'flags_only', 'flags_name', 'name_only'), true) ? $style : 'dropdown'; $display_args = aitmt_fallback_switcher_display_for_layout($style, $display_args); $current = aitmt_fallback_current_language(); $visible_languages = array(); foreach ($languages as $language) { if ($language === $current && ($display_args['hide_current'] || !$display_args['show_current'])) { continue; } $visible_languages[] = $language; } if (empty($visible_languages) && in_array($current, $languages, true)) { $visible_languages[] = $current; } if ('dropdown' !== $style) { $classes = array( 'aitmt-fallback-switcher', 'aitmt-fallback-switcher-list', 'aitmt-switcher-layout-' . $style, 'aitmt-switcher-orientation-' . $display_args['orientation'], ); $classes[] = 'notranslate'; $output = ''; return $output; } $id = 'aitmt-fallback-switcher-' . wp_rand(1000, 999999); $output = '
'; $output .= ''; $output .= aitmt_fallback_language_html_for_display($current, $display_args); $output .= ''; $output .= '
'; foreach ($visible_languages as $language) { $classes = array('aitmt-fallback-switcher-dropdown-link'); if ($language === $current) { $classes[] = 'is-current'; } $output .= sprintf( '%6$s', esc_attr(implode(' ', $classes)), esc_url(aitmt_fallback_language_url($language)), esc_attr($language), $language === $current ? 'aria-current="true"' : '', esc_attr(aitmt_fallback_language_accessible_label($language)), aitmt_fallback_language_html_for_display($language, $display_args) ); } $output .= '
'; return $output; } function aitmt_fallback_register_widget() { if (class_exists('WP_Widget') && class_exists('AITMT_Fallback_Switcher_Widget')) { register_widget('AITMT_Fallback_Switcher_Widget'); } } if (class_exists('WP_Widget') && !class_exists('AITMT_Fallback_Switcher_Widget')) { class AITMT_Fallback_Switcher_Widget extends WP_Widget { public function __construct() { parent::__construct( 'aitmt_fallback_switcher', __('AIT Multilingual Translate Switcher', 'ait-multilingual-translate'), array('description' => __('Display the AIT Multilingual Translate language switcher.', 'ait-multilingual-translate')) ); } public function widget($args, $instance) { $style = isset($instance['style']) && 'list' === $instance['style'] ? 'list' : 'dropdown'; $switcher = aitmt_fallback_render_switcher($style); if ('' === $switcher) { return; } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Widget wrapper is provided by WordPress/theme sidebars. echo isset($args['before_widget']) ? wp_kses_post($args['before_widget']) : ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Switcher HTML is generated internally with escaped URLs, labels, and attributes. echo $switcher; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Widget wrapper is provided by WordPress/theme sidebars. echo isset($args['after_widget']) ? wp_kses_post($args['after_widget']) : ''; } public function form($instance) { $style = isset($instance['style']) && 'list' === $instance['style'] ? 'list' : 'dropdown'; ?>

isset($new_instance['style']) && 'list' === $new_instance['style'] ? 'list' : 'dropdown', ); } } } function aitmt_fallback_register_elementor_widget_legacy() { if (!did_action('elementor/widgets/register') && class_exists('\Elementor\Plugin')) { aitmt_fallback_register_elementor_widget(\Elementor\Plugin::instance()->widgets_manager); } } function aitmt_fallback_register_elementor_widget($widgets_manager) { if (!$widgets_manager || !class_exists('\Elementor\Widget_Base') || !class_exists('\Elementor\Controls_Manager')) { return; } if (!class_exists('AITMT_Fallback_Elementor_Switcher_Widget')) { class AITMT_Fallback_Elementor_Switcher_Widget extends \Elementor\Widget_Base { public function get_name() { return 'aitmt_language_switcher'; } public function get_title() { return __('AI Translate Language Switcher', 'ait-multilingual-translate'); } public function get_icon() { return 'eicon-globe'; } public function get_categories() { return array('general'); } public function get_keywords() { return array('language', 'translate', 'translation', 'ai', 'wp ai'); } protected function register_controls() { $this->start_controls_section( 'aitmt_content', array('label' => __('Language Switcher', 'ait-multilingual-translate')) ); $this->add_control( 'layout_type', array( 'label' => __('Layout type', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::SELECT, 'default' => 'dropdown', 'options' => array( 'list' => __('List', 'ait-multilingual-translate'), 'dropdown' => __('Dropdown', 'ait-multilingual-translate'), 'buttons' => __('Buttons', 'ait-multilingual-translate'), 'flags_only' => __('Flags only', 'ait-multilingual-translate'), 'flags_name' => __('Flags + language name', 'ait-multilingual-translate'), 'name_only' => __('Language name only', 'ait-multilingual-translate'), ), ) ); $this->add_control( 'show_flags', array( 'label' => __('Show flags', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => __('Yes', 'ait-multilingual-translate'), 'label_off' => __('No', 'ait-multilingual-translate'), 'return_value' => 'yes', 'default' => '', ) ); $this->add_control( 'show_language_name', array( 'label' => __('Show language name', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => __('Yes', 'ait-multilingual-translate'), 'label_off' => __('No', 'ait-multilingual-translate'), 'return_value' => 'yes', 'default' => 'yes', ) ); $this->add_control( 'show_language_code', array( 'label' => __('Show language code', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => __('Yes', 'ait-multilingual-translate'), 'label_off' => __('No', 'ait-multilingual-translate'), 'return_value' => 'yes', 'default' => '', ) ); $this->add_control( 'show_current_language', array( 'label' => __('Show current language', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => __('Yes', 'ait-multilingual-translate'), 'label_off' => __('No', 'ait-multilingual-translate'), 'return_value' => 'yes', 'default' => 'yes', ) ); $this->add_control( 'hide_current_language', array( 'label' => __('Hide current language', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => __('Yes', 'ait-multilingual-translate'), 'label_off' => __('No', 'ait-multilingual-translate'), 'return_value' => 'yes', 'default' => '', ) ); $this->add_control( 'open_current_page', array( 'label' => __('Open links in current page', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::SWITCHER, 'label_on' => __('Yes', 'ait-multilingual-translate'), 'label_off' => __('No', 'ait-multilingual-translate'), 'return_value' => 'yes', 'default' => 'yes', ) ); $this->add_control( 'orientation', array( 'label' => __('Orientation', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::SELECT, 'default' => 'horizontal', 'options' => array( 'horizontal' => __('Horizontal', 'ait-multilingual-translate'), 'vertical' => __('Vertical', 'ait-multilingual-translate'), ), ) ); $this->add_responsive_control( 'align', array( 'label' => __('Alignment', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::CHOOSE, 'default' => 'left', 'options' => array( 'left' => array('title' => __('Left', 'ait-multilingual-translate'), 'icon' => 'eicon-text-align-left'), 'center' => array('title' => __('Center', 'ait-multilingual-translate'), 'icon' => 'eicon-text-align-center'), 'right' => array('title' => __('Right', 'ait-multilingual-translate'), 'icon' => 'eicon-text-align-right'), ), 'selectors' => array( '{{WRAPPER}} .aitmt-elementor-switcher' => 'text-align: {{VALUE}};', ), ) ); $this->end_controls_section(); $this->start_controls_section( 'aitmt_style', array( 'label' => __('Switcher Style', 'ait-multilingual-translate'), 'tab' => \Elementor\Controls_Manager::TAB_STYLE, ) ); if (class_exists('\Elementor\Group_Control_Typography')) { $this->add_group_control( \Elementor\Group_Control_Typography::get_type(), array( 'name' => 'typography', 'selector' => '{{WRAPPER}} .aitmt-fallback-switcher, {{WRAPPER}} .aitmt-fallback-switcher-link', ) ); } $this->add_control( 'text_color', array( 'label' => __('Text color', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .aitmt-fallback-switcher, {{WRAPPER}} .aitmt-fallback-switcher-link' => 'color: {{VALUE}};', ), ) ); $this->add_control( 'hover_color', array( 'label' => __('Hover color', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .aitmt-fallback-switcher-link:hover, {{WRAPPER}} .aitmt-fallback-switcher-link:focus, {{WRAPPER}} .aitmt-fallback-switcher-dropdown:hover, {{WRAPPER}} .aitmt-fallback-switcher-dropdown:focus' => 'color: {{VALUE}};', ), ) ); $this->add_control( 'active_language_color', array( 'label' => __('Active language color', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .aitmt-fallback-switcher-link.is-current' => 'color: {{VALUE}};', ), ) ); $this->add_control( 'background_color', array( 'label' => __('Background color', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .aitmt-fallback-switcher-dropdown, {{WRAPPER}} .aitmt-fallback-switcher-link' => 'background-color: {{VALUE}};', ), ) ); $this->add_control( 'border_color', array( 'label' => __('Border color', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .aitmt-fallback-switcher-dropdown, {{WRAPPER}} .aitmt-fallback-switcher-link' => 'border-color: {{VALUE}};', ), ) ); $this->add_control( 'dropdown_background', array( 'label' => __('Dropdown background', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::COLOR, 'selectors' => array( '{{WRAPPER}} .aitmt-fallback-switcher-dropdown' => 'background-color: {{VALUE}};', ), ) ); $this->add_responsive_control( 'border_radius', array( 'label' => __('Border radius', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::SLIDER, 'size_units' => array('px', '%'), 'range' => array( 'px' => array('min' => 0, 'max' => 40), '%' => array('min' => 0, 'max' => 50), ), 'selectors' => array( '{{WRAPPER}} .aitmt-fallback-switcher-dropdown, {{WRAPPER}} .aitmt-fallback-switcher-link' => 'border-radius: {{SIZE}}{{UNIT}};', ), ) ); $this->add_responsive_control( 'padding', array( 'label' => __('Padding', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::DIMENSIONS, 'size_units' => array('px', 'em', '%'), 'selectors' => array( '{{WRAPPER}} .aitmt-fallback-switcher-dropdown, {{WRAPPER}} .aitmt-fallback-switcher-link' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};', ), ) ); $this->add_responsive_control( 'gap', array( 'label' => __('Gap between items', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::SLIDER, 'size_units' => array('px'), 'range' => array('px' => array('min' => 0, 'max' => 40)), 'selectors' => array( '{{WRAPPER}} .aitmt-fallback-switcher-list' => 'gap: {{SIZE}}{{UNIT}};', ), ) ); $this->add_responsive_control( 'dropdown_width', array( 'label' => __('Dropdown width', 'ait-multilingual-translate'), 'type' => \Elementor\Controls_Manager::SLIDER, 'size_units' => array('px', '%', 'em'), 'range' => array( 'px' => array('min' => 80, 'max' => 520), '%' => array('min' => 10, 'max' => 100), 'em' => array('min' => 4, 'max' => 40), ), 'selectors' => array( '{{WRAPPER}} .aitmt-fallback-switcher-dropdown' => 'width: {{SIZE}}{{UNIT}};', ), ) ); if (class_exists('\Elementor\Group_Control_Box_Shadow')) { $this->add_group_control( \Elementor\Group_Control_Box_Shadow::get_type(), array( 'name' => 'dropdown_shadow', 'label' => __('Dropdown shadow', 'ait-multilingual-translate'), 'selector' => '{{WRAPPER}} .aitmt-fallback-switcher-dropdown', ) ); } $this->end_controls_section(); } protected function render() { $settings = $this->get_settings_for_display(); $layout = isset($settings['layout_type']) ? sanitize_key((string) $settings['layout_type']) : (isset($settings['style']) ? sanitize_key((string) $settings['style']) : 'dropdown'); $orientation = isset($settings['orientation']) && 'vertical' === $settings['orientation'] ? 'vertical' : 'horizontal'; $align = isset($settings['align']) ? sanitize_key((string) $settings['align']) : 'left'; $display = array( 'show_flags' => isset($settings['show_flags']) && 'yes' === $settings['show_flags'], 'show_names' => !isset($settings['show_language_name']) || 'yes' === $settings['show_language_name'], 'show_codes' => isset($settings['show_language_code']) && 'yes' === $settings['show_language_code'], 'show_current' => !isset($settings['show_current_language']) || 'yes' === $settings['show_current_language'], 'hide_current' => isset($settings['hide_current_language']) && 'yes' === $settings['hide_current_language'], 'open_current_page' => !isset($settings['open_current_page']) || 'yes' === $settings['open_current_page'], 'orientation' => $orientation, ); echo '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Switcher HTML is generated internally with escaped URLs, labels, and attributes. echo wp_kses(aitmt_fallback_render_switcher($layout, $display), aitmt_fallback_allowed_switcher_html()); echo '
'; } } } if (method_exists($widgets_manager, 'register')) { $widgets_manager->register(new AITMT_Fallback_Elementor_Switcher_Widget()); } elseif (method_exists($widgets_manager, 'register_widget_type')) { $widgets_manager->register_widget_type(new AITMT_Fallback_Elementor_Switcher_Widget()); } } function aitmt_fallback_add_nav_menu_metabox() { static $added = false; if ($added || !current_user_can('edit_theme_options')) { return; } $added = true; add_meta_box( 'aitmt-language-switcher-menu', __('Language Switcher', 'ait-multilingual-translate'), 'aitmt_fallback_nav_menu_metabox', 'nav-menus', 'side', 'high' ); } function aitmt_fallback_nav_menu_metabox() { $languages = aitmt_fallback_languages(); $enabled = aitmt_fallback_enabled_languages(); ?>

    $language) : ?>

  • classes)); if (!in_array('aitmt-menu-switcher', $classes, true)) { $expanded[] = $item; continue; } $display = aitmt_fallback_nav_menu_display_from_classes($classes); $mode = aitmt_fallback_nav_menu_mode_from_classes($classes); $languages = array(); if ('current' === $mode) { $languages = $enabled; $mode = 'all'; } elseif ('specific' === $mode) { $specific = aitmt_fallback_nav_menu_language_from_classes($classes); $languages = $specific && in_array($specific, $enabled, true) ? array($specific) : array(); } else { $languages = $enabled; } if (empty($languages)) { continue; } if ('all' === $mode && 'dropdown' === $display['menu_display']) { $parent = clone $item; $parent->title = aitmt_fallback_language_text_for_display($current, $display); $parent->url = '#'; $parent->classes = array_values(array_unique(array_merge($classes, array('menu-item-has-children', 'aitmt-menu-switcher-dropdown-toggle')))); $expanded[] = $parent; foreach ($languages as $language) { if ($language === $current && $display['hide_current']) { continue; } $expanded[] = aitmt_fallback_nav_menu_language_item($item, $language, $display, $sequence, (int) $parent->ID); $sequence++; } continue; } foreach ($languages as $language) { if ($language === $current && $display['hide_current'] && 'current' !== $mode) { continue; } $expanded[] = aitmt_fallback_nav_menu_language_item($item, $language, $display, $sequence, (int) $item->menu_item_parent); $sequence++; } } return $expanded; } function aitmt_fallback_nav_menu_display_from_classes($classes) { $display = array( 'show_flags' => in_array('aitmt-menu-show-flag', $classes, true), 'show_names' => in_array('aitmt-menu-show-name', $classes, true), 'show_codes' => in_array('aitmt-menu-show-code', $classes, true), 'hide_current' => in_array('aitmt-menu-hide-current', $classes, true), 'show_current' => !in_array('aitmt-menu-hide-current', $classes, true), 'orientation' => 'horizontal', 'open_current_page' => true, 'menu_display' => in_array('aitmt-menu-display-dropdown', $classes, true) ? 'dropdown' : 'list', ); if (!$display['show_flags'] && !$display['show_names'] && !$display['show_codes']) { $display['show_names'] = true; } return $display; } function aitmt_fallback_nav_menu_mode_from_classes($classes) { if (in_array('aitmt-menu-mode-current', $classes, true)) { return 'current'; } if (in_array('aitmt-menu-mode-specific', $classes, true)) { return 'specific'; } return 'all'; } function aitmt_fallback_nav_menu_language_from_classes($classes) { foreach ($classes as $class) { if ('aitmt-menu-language-item' === (string) $class) { continue; } if (0 === strpos((string) $class, 'aitmt-menu-language-')) { return aitmt_fallback_normalize_language(substr((string) $class, strlen('aitmt-menu-language-'))); } } return ''; } function aitmt_fallback_nav_menu_language_item($source_item, $language, $display, $sequence, $parent_id) { $item = clone $source_item; $current = aitmt_fallback_current_language(); $classes = array_diff(array_map('strval', (array) $source_item->classes), array('aitmt-menu-switcher', 'aitmt-menu-mode-current', 'aitmt-menu-mode-all', 'aitmt-menu-mode-specific', 'aitmt-menu-display-list', 'aitmt-menu-display-dropdown')); $item->ID = (int) $source_item->ID + 900000 + (int) $sequence; $item->db_id = 0; $item->object_id = 0; $item->menu_item_parent = (int) $parent_id; $item->title = aitmt_fallback_language_text_for_display($language, $display); $item->url = aitmt_fallback_language_url($language); $item->target = ''; $item->attr_title = ''; $item->description = ''; $item->classes = array_values(array_unique(array_merge($classes, array('aitmt-menu-language-item', 'aitmt-menu-language-' . sanitize_html_class($language))))); if ($language === $current) { $item->classes[] = 'aitmt-menu-current-language'; $item->classes[] = 'current-menu-item'; } return $item; } function aitmt_fallback_nav_menu_item_title($title, $item, $args = null, $depth = 0) { if (!is_object($item)) { return $title; } $classes = array_filter(array_map('strval', isset($item->classes) ? (array) $item->classes : array())); $is_language_item = in_array('aitmt-menu-language-item', $classes, true); $is_dropdown_toggle = in_array('aitmt-menu-switcher-dropdown-toggle', $classes, true); if (!$is_language_item && !$is_dropdown_toggle) { return $title; } $display = aitmt_fallback_nav_menu_display_from_classes($classes); $language = $is_dropdown_toggle ? aitmt_fallback_current_language() : aitmt_fallback_nav_menu_language_from_classes($classes); if (!$language) { return $title; } return aitmt_fallback_language_html_for_display($language, $display); } function aitmt_fallback_nav_menu_link_attributes($atts, $item, $args = null, $depth = 0) { if (!is_object($item)) { return $atts; } $classes = array_filter(array_map('strval', isset($item->classes) ? (array) $item->classes : array())); $is_language_item = in_array('aitmt-menu-language-item', $classes, true); $is_dropdown_toggle = in_array('aitmt-menu-switcher-dropdown-toggle', $classes, true); if (!$is_language_item && !$is_dropdown_toggle) { return $atts; } $existing_classes = isset($atts['class']) ? preg_split('/\s+/', trim((string) $atts['class'])) : array(); $existing_classes = is_array($existing_classes) ? array_filter($existing_classes) : array(); $existing_classes[] = $is_language_item ? 'aitmt-menu-language-link' : 'aitmt-menu-language-toggle'; $atts['class'] = implode(' ', array_values(array_unique($existing_classes))); $atts['data-aitmt-language-switcher'] = 'menu'; $atts['data-aitmt-no-translate'] = '1'; $atts['translate'] = 'no'; if ($is_language_item) { $language = aitmt_fallback_nav_menu_language_from_classes($classes); if ($language) { $atts['data-aitmt-language'] = $language; $atts['aria-label'] = aitmt_fallback_language_accessible_label($language); } } elseif ($is_dropdown_toggle) { $atts['aria-label'] = aitmt_fallback_language_accessible_label(aitmt_fallback_current_language()); $atts['aria-haspopup'] = 'true'; } return $atts; } function aitmt_fallback_language_text($language) { return aitmt_fallback_language_text_for_display($language); } function aitmt_fallback_language_text_for_display($language, $display_args = array()) { $display_args = aitmt_fallback_normalize_switcher_display_args($display_args); $parts = array(); if ($display_args['show_flags']) { $parts[] = aitmt_fallback_flag($language); } if ($display_args['show_names']) { $languages = aitmt_fallback_languages(); $parts[] = isset($languages[$language]) ? $languages[$language] : strtoupper($language); } if ($display_args['show_codes']) { $parts[] = strtoupper($language); } if (empty($parts)) { $parts[] = strtoupper($language); } return trim(implode(' ', $parts)); } function aitmt_fallback_language_html($language) { return aitmt_fallback_language_html_for_display($language); } function aitmt_fallback_language_html_for_display($language, $display_args = array()) { $display_args = aitmt_fallback_normalize_switcher_display_args($display_args); $parts = array(); if ($display_args['show_flags']) { $parts[] = aitmt_fallback_flag_html($language); } if ($display_args['show_names']) { $languages = aitmt_fallback_languages(); $label = isset($languages[$language]) ? $languages[$language] : strtoupper($language); $parts[] = '' . esc_html($label) . ''; } if ($display_args['show_codes']) { $parts[] = '' . esc_html(strtoupper($language)) . ''; } if (empty($parts)) { $parts[] = '' . esc_html(strtoupper($language)) . ''; } return implode(' ', $parts); } function aitmt_fallback_language_accessible_label($language) { $language = aitmt_fallback_normalize_language($language); $languages = aitmt_fallback_languages(); $label = isset($languages[$language]) ? $languages[$language] : strtoupper($language); return $label . ' (' . strtoupper($language) . ')'; } function aitmt_fallback_flag_html($language) { $url = aitmt_fallback_flag_url($language); if (!$url) { return ''; } return ''; } function aitmt_fallback_flag_url($language) { $country = strtolower(aitmt_fallback_flag_country($language)); $relative = 'assets/flags/flag-icons/4x3/' . sanitize_file_name($country) . '.svg'; if (!file_exists(AITMT_PLUGIN_DIR . $relative)) { return ''; } return AITMT_PLUGIN_URL . $relative; } function aitmt_fallback_flag($language) { $country = aitmt_fallback_flag_country($language); $entity = ''; foreach (str_split(strtoupper($country)) as $letter) { $entity .= '&#' . (127397 + ord($letter)) . ';'; } return html_entity_decode($entity, ENT_QUOTES, 'UTF-8'); } function aitmt_fallback_flag_country($language) { $language = aitmt_fallback_normalize_language($language); $map = array( 'af' => 'ZA', 'am' => 'ET', 'en' => 'US', 'ka' => 'GE', 'ru' => 'RU', 'de' => 'DE', 'fr' => 'FR', 'es' => 'ES', 'it' => 'IT', 'pt' => 'PT', 'zh' => 'CN', 'ja' => 'JP', 'ko' => 'KR', 'uk' => 'UA', 'tr' => 'TR', 'ar' => 'SA', 'he' => 'IL', 'sq' => 'AL', 'hy' => 'AM', 'az' => 'AZ', 'eu' => 'ES', 'be' => 'BY', 'bn' => 'BD', 'bs' => 'BA', 'bg' => 'BG', 'ca' => 'ES', 'ceb' => 'PH', 'co' => 'FR', 'hr' => 'HR', 'cs' => 'CZ', 'da' => 'DK', 'nl' => 'NL', 'eo' => 'UN', 'et' => 'EE', 'fi' => 'FI', 'fy' => 'NL', 'gl' => 'ES', 'el' => 'GR', 'gu' => 'IN', 'ht' => 'HT', 'ha' => 'NG', 'haw' => 'US', 'hi' => 'IN', 'hmn' => 'CN', 'hu' => 'HU', 'is' => 'IS', 'ig' => 'NG', 'id' => 'ID', 'ga' => 'IE', 'jv' => 'ID', 'kn' => 'IN', 'kk' => 'KZ', 'km' => 'KH', 'ku' => 'IQ', 'ky' => 'KG', 'lo' => 'LA', 'la' => 'VA', 'lv' => 'LV', 'lt' => 'LT', 'lb' => 'LU', 'mk' => 'MK', 'mg' => 'MG', 'ms' => 'MY', 'ml' => 'IN', 'mt' => 'MT', 'mi' => 'NZ', 'mr' => 'IN', 'mn' => 'MN', 'my' => 'MM', 'ne' => 'NP', 'no' => 'NO', 'ny' => 'MW', 'ps' => 'AF', 'fa' => 'IR', 'pa' => 'IN', 'ro' => 'RO', 'sm' => 'WS', 'gd' => 'GB-SCT', 'sr' => 'RS', 'st' => 'LS', 'sn' => 'ZW', 'sd' => 'PK', 'si' => 'LK', 'sk' => 'SK', 'sl' => 'SI', 'so' => 'SO', 'su' => 'ID', 'sw' => 'TZ', 'sv' => 'SE', 'tl' => 'PH', 'tg' => 'TJ', 'ta' => 'IN', 'te' => 'IN', 'th' => 'TH', 'ur' => 'PK', 'ug' => 'CN', 'uz' => 'UZ', 'vi' => 'VN', 'cy' => 'GB-WLS', 'xh' => 'ZA', 'yi' => 'IL', 'yo' => 'NG', 'zu' => 'ZA', ); return isset($map[$language]) ? $map[$language] : strtoupper(substr($language, 0, 2)); } function aitmt_fallback_inline_styles() { // Kept for backward compatibility with older builds. Assets are enqueued through wp_enqueue_scripts. } function aitmt_fallback_start_translation() { if (!aitmt_fallback_should_translate_request()) { return; } if (!empty($GLOBALS['aitmt_fallback_translation_buffer_level'])) { return; } $GLOBALS['aitmt_fallback_translation_buffer_level'] = ob_get_level() + 1; ob_start('aitmt_fallback_translate_html'); add_action('shutdown', 'aitmt_fallback_flush_translation_buffer', 0); } function aitmt_fallback_flush_translation_buffer() { $level = isset($GLOBALS['aitmt_fallback_translation_buffer_level']) ? (int) $GLOBALS['aitmt_fallback_translation_buffer_level'] : 0; if ($level <= 0) { return; } while (ob_get_level() >= $level) { ob_end_flush(); } unset($GLOBALS['aitmt_fallback_translation_buffer_level']); } function aitmt_fallback_should_translate_request() { $options = aitmt_fallback_options(); if (is_admin() || wp_doing_ajax() || is_feed() || is_robots() || is_trackback()) { return false; } if (defined('REST_REQUEST') && REST_REQUEST) { return false; } if (aitmt_fallback_is_unsafe_page_load_request()) { return false; } if ('1' !== $options['auto_translate'] && !aitmt_fallback_frontend_editor_enabled()) { return false; } return aitmt_fallback_current_language() !== aitmt_fallback_source_language(); } function aitmt_fallback_is_unsafe_page_load_request() { if (defined('DOING_CRON') && DOING_CRON) { return true; } if (function_exists('is_customize_preview') && is_customize_preview()) { return true; } if (function_exists('is_cart') && is_cart()) { return true; } if (function_exists('is_checkout') && is_checkout()) { return true; } $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash((string) $_SERVER['REQUEST_URI'])) : ''; $query_string = isset($_SERVER['QUERY_STRING']) ? sanitize_text_field(wp_unslash((string) $_SERVER['QUERY_STRING'])) : ''; $uri = strtolower($request_uri . '&' . $query_string); $blocked_needles = array( 'customize_', 'customize_changeset_uuid', 'customize_theme', 'elementor-preview', 'elementor_library', 'preview=true', 'preview_id=', 'preview_nonce=', 'add-to-cart=', 'remove_item=', 'removed_item=', 'wc-ajax=', 'orderby=', 'filter_', 'min_price=', 'max_price=', 'rating_filter=', '/cart', '/checkout', ); foreach ($blocked_needles as $needle) { if (false !== strpos($uri, $needle)) { return true; } } return false; } function aitmt_fallback_translate_html($html) { if ('' === trim((string) $html) || !class_exists('DOMDocument')) { return $html; } $target_language = aitmt_fallback_current_language(); $source_language = aitmt_fallback_source_language(); if ($target_language === $source_language) { return $html; } $dom = new DOMDocument('1.0', 'UTF-8'); $previous = libxml_use_internal_errors(true); $loaded = $dom->loadHTML('' . $html); libxml_clear_errors(); libxml_use_internal_errors($previous); if (!$loaded) { return $html; } foreach ($dom->childNodes as $child) { if (XML_PI_NODE === $child->nodeType) { $dom->removeChild($child); break; } } $text_nodes = array(); $attribute_nodes = array(); aitmt_fallback_collect_translation_nodes($dom, $text_nodes, $attribute_nodes); $segments = array(); foreach ($text_nodes as $item) { $segments[$item['hash']] = $item['source']; } $options = aitmt_fallback_options(); if ('1' === $options['translate_attributes']) { foreach ($attribute_nodes as $item) { $segments[$item['hash']] = $item['source']; } } if (empty($segments)) { aitmt_fallback_rewrite_dom_links($dom, $target_language); return $dom->saveHTML(); } $translations = aitmt_fallback_translate_segments($segments, $source_language, $target_language); $editor_mode = aitmt_fallback_frontend_editor_enabled(); if (empty($translations) && !$editor_mode) { aitmt_fallback_rewrite_dom_links($dom, $target_language); return $dom->saveHTML(); } foreach ($text_nodes as $item) { if (!$item['node']->parentNode) { continue; } if (!isset($translations[$item['hash']]) && !$editor_mode) { continue; } $replacement = isset($translations[$item['hash']]) ? $translations[$item['hash']] : $item['source']; aitmt_fallback_replace_text_node( $dom, $item['node'], $item['original'], $item['source'], $replacement, $item['hash'] ); } if ('1' === $options['translate_attributes']) { foreach ($attribute_nodes as $item) { if (!isset($translations[$item['hash']])) { continue; } $item['node']->setAttribute($item['attribute'], $translations[$item['hash']]); } } aitmt_fallback_rewrite_dom_links($dom, $target_language); return $dom->saveHTML(); } function aitmt_fallback_rewrite_dom_links($dom, $target_language) { if (!$dom instanceof DOMDocument) { return; } $target_language = aitmt_fallback_normalize_language($target_language); if (!$target_language || $target_language === aitmt_fallback_source_language()) { return; } $url_attributes = array('href', 'action', 'data-href', 'data-url', 'data-link', 'data-product-url', 'data-product-permalink', 'data-permalink', 'data-product_permalink'); foreach ($dom->getElementsByTagName('*') as $element) { if (!$element instanceof DOMElement) { continue; } $class = ' ' . $element->getAttribute('class') . ' '; if ( $element->hasAttribute('data-aitmt-no-translate') || $element->hasAttribute('data-aitmt-language-switcher') || false !== strpos($class, ' aitmt-fallback-switcher-link ') || false !== strpos($class, ' aitmt-fallback-switcher-dropdown-link ') || false !== strpos($class, ' aitmt-switcher-dropdown-link ') || false !== strpos($class, ' aitmt-switcher-link ') || false !== strpos($class, ' aitmt-editor-toolbar ') || false !== strpos($class, ' notranslate ') || aitmt_fallback_dom_has_class_in_ancestry($element, array('aitmt-menu-language-item', 'aitmt-menu-current-language', 'aitmt-menu-switcher-dropdown-toggle')) ) { continue; } foreach ($url_attributes as $attribute) { if (!$element->hasAttribute($attribute)) { continue; } $url = $element->getAttribute($attribute); $translated_url = aitmt_fallback_apply_language_to_url($url, $target_language); if ($translated_url !== $url) { $element->setAttribute($attribute, $translated_url); } } } } function aitmt_fallback_dom_has_class_in_ancestry($element, $classes) { if (!$element instanceof DOMElement) { return false; } $classes = array_filter(array_map('strval', (array) $classes)); if (empty($classes)) { return false; } $node = $element; while ($node instanceof DOMElement) { $node_classes = ' ' . $node->getAttribute('class') . ' '; foreach ($classes as $class) { if (false !== strpos($node_classes, ' ' . $class . ' ')) { return true; } } $node = $node->parentNode; } return false; } function aitmt_fallback_replace_text_node($dom, $node, $original, $source, $translation, $hash) { if (!$node->parentNode) { return; } $translation = aitmt_fallback_apply_original_spacing($original, $translation); if (aitmt_fallback_frontend_editor_enabled() && aitmt_fallback_can_wrap_for_editor($node)) { $span = $dom->createElement('span'); $span->setAttribute('class', 'aitmt-editable'); $span->setAttribute('data-aitmt-source-hash', $hash); $span->setAttribute('data-aitmt-source', base64_encode($source)); $span->appendChild($dom->createTextNode($translation)); $node->parentNode->replaceChild($span, $node); return; } $node->nodeValue = $translation; } function aitmt_fallback_frontend_editor_enabled() { $options = aitmt_fallback_options(); return '1' === $options['frontend_editor'] && current_user_can('manage_options') && '' !== aitmt_fallback_frontend_editor_target_language(); } function aitmt_fallback_frontend_editor_target_language() { $source_language = aitmt_fallback_source_language(); $current_language = aitmt_fallback_current_language(); if ($current_language && $current_language !== $source_language) { return $current_language; } return ''; } function aitmt_fallback_can_wrap_for_editor($node) { $current = $node->parentNode; while ($current instanceof DOMElement) { $tag = strtolower($current->tagName); if (in_array($tag, array('head', 'title', 'option', 'select', 'script', 'style'), true)) { return false; } $class = ' ' . $current->getAttribute('class') . ' '; if (false !== strpos($class, ' aitmt-editable ') || false !== strpos($class, ' aitmt-editor-toolbar ')) { return false; } $current = $current->parentNode; } return true; } function aitmt_fallback_enqueue_frontend_editor() { wp_enqueue_style('aitmt-frontend', AITMT_PLUGIN_URL . 'assets/css/frontend.css', array(), AITMT_VERSION); $frontend_css = aitmt_fallback_asset_contents('assets/css/frontend.css'); wp_add_inline_style('aitmt-frontend', $frontend_css ? $frontend_css : aitmt_fallback_frontend_inline_css()); $options = aitmt_fallback_options(); if (!empty($options['selector_custom_css'])) { wp_add_inline_style('aitmt-frontend', wp_strip_all_tags((string) $options['selector_custom_css'])); } if (!aitmt_fallback_frontend_editor_enabled()) { return; } wp_enqueue_script('aitmt-frontend-editor', AITMT_PLUGIN_URL . 'assets/js/frontend-editor.js', array(), AITMT_VERSION, true); wp_localize_script('aitmt-frontend-editor', 'AITMT_EDITOR', aitmt_fallback_frontend_editor_config()); $frontend_editor_js = aitmt_fallback_asset_contents('assets/js/frontend-editor.js'); wp_add_inline_script('aitmt-frontend-editor', $frontend_editor_js ? $frontend_editor_js : aitmt_fallback_frontend_editor_inline_script(), 'after'); } function aitmt_fallback_admin_bar_editor_node($wp_admin_bar) { if (!aitmt_fallback_frontend_editor_enabled() || !is_admin_bar_showing()) { return; } $wp_admin_bar->add_node(array( 'id' => 'aitmt-frontend-editor', 'title' => __('AI Translate Edit', 'ait-multilingual-translate'), 'href' => '#', 'meta' => array( 'class' => 'aitmt-adminbar-editor', 'onclick' => 'window.AITMT_INLINE_TOGGLE && window.AITMT_INLINE_TOGGLE(); return false;', ), )); } function aitmt_fallback_render_frontend_editor_inline() { // Deprecated. Frontend editor data is localized into assets/js/frontend-editor.js. } function aitmt_fallback_frontend_editor_config() { $target_language = aitmt_fallback_frontend_editor_target_language(); return array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aitmt_frontend_editor'), 'sourceLanguage' => aitmt_fallback_source_language(), 'targetLanguage' => $target_language, 'editLabel' => __('AI Translate edit', 'ait-multilingual-translate'), 'activeLabel' => __('Editing on', 'ait-multilingual-translate'), 'promptLabel' => __('Edit translation', 'ait-multilingual-translate'), 'saveLabel' => __('Save', 'ait-multilingual-translate'), 'cancelLabel' => __('Cancel', 'ait-multilingual-translate'), 'autoTranslateLabel' => __('Auto Translate', 'ait-multilingual-translate'), 'translatingLabel' => __('AI translating...', 'ait-multilingual-translate'), 'translationReadyLabel' => __('AI Translation Ready', 'ait-multilingual-translate'), 'translateFailedLabel' => __('Translation failed. Please try again.', 'ait-multilingual-translate'), 'savingLabel' => __('Saving...', 'ait-multilingual-translate'), 'savedLabel' => __('Saved', 'ait-multilingual-translate'), 'errorLabel' => __('Could not save translation', 'ait-multilingual-translate'), 'emptyLabel' => __('No editable translation text found yet. Scan/process the queue, or click source text that is highlighted for admins.', 'ait-multilingual-translate'), /* translators: %s: Target language code. */ 'targetNotice' => $target_language ? sprintf(__('Target: %s', 'ait-multilingual-translate'), strtoupper($target_language)) : '', 'logoUrl' => aitmt_fallback_logo_url(), 'brandLabel' => sprintf('AI Translate %s | %s | sotter IT Design | info@itdesign.biz', AITMT_VERSION, aitmt_fallback_edition_label()), 'siteUrl' => 'https://wp-ai.itdesign.biz', ); } function aitmt_fallback_frontend_editor_inline_script() { return <<<'JS' (function(){if(!window.AITMT_EDITOR||window.AITMT_INLINE_EDITOR_READY){return;}window.AITMT_INLINE_EDITOR_READY=true;var c=window.AITMT_EDITOR||{},active=false,current=null,toolbar=document.createElement('div'),brand=document.createElement('a'),img=document.createElement('img'),brandText=document.createElement('span'),button=document.createElement('button'),status=document.createElement('span'),modal=document.createElement('div'),dialog=document.createElement('div'),title=document.createElement('h2'),textarea=document.createElement('textarea'),actions=document.createElement('div'),save=document.createElement('button'),cancel=document.createElement('button');toolbar.className='aitmt-inline-editor-toolbar';toolbar.setAttribute('data-aitmt-no-translate','1');brand.className='aitmt-inline-editor-brand';brand.href=c.siteUrl||'#';brand.target='_blank';brand.rel='noopener noreferrer';if(c.logoUrl){img.src=c.logoUrl;img.alt='';brand.appendChild(img);}brandText.textContent=c.brandLabel||'AI Translate';brand.appendChild(brandText);button.type='button';button.textContent=c.editLabel||'AI Translate edit';status.className='aitmt-inline-editor-status';status.textContent=c.targetNotice||'';toolbar.appendChild(brand);toolbar.appendChild(button);toolbar.appendChild(status);modal.className='aitmt-inline-editor-modal';modal.setAttribute('data-aitmt-no-translate','1');modal.setAttribute('aria-hidden','true');dialog.className='aitmt-inline-editor-dialog';dialog.setAttribute('role','dialog');dialog.setAttribute('aria-modal','true');title.textContent=c.promptLabel||'Edit translation';textarea.className='aitmt-inline-editor-textarea';actions.className='aitmt-inline-editor-dialog-actions';save.type='button';save.textContent=c.saveLabel||'Save';cancel.type='button';cancel.textContent=c.cancelLabel||'Cancel';cancel.className='is-secondary';actions.appendChild(cancel);actions.appendChild(save);dialog.appendChild(title);dialog.appendChild(textarea);dialog.appendChild(actions);modal.appendChild(dialog);function mount(){if(document.body&&!document.querySelector('.aitmt-inline-editor-toolbar')){document.body.appendChild(toolbar);document.body.appendChild(modal);}}function setActive(next){active=typeof next==='boolean'?next:!active;document.body.classList.toggle('aitmt-editor-active',active);button.classList.toggle('is-active',active);status.textContent=active?(c.activeLabel||'Editing on'):(c.targetNotice||'');if(active&&!document.querySelector('.aitmt-editable')){status.textContent=c.emptyLabel||'No editable text found yet.';}}window.AITMT_INLINE_TOGGLE=function(){setActive();};button.addEventListener('click',function(){setActive();});document.addEventListener('click',function(e){var target=e.target.closest?e.target.closest('.aitmt-editable'):null;if(!active||!target){return;}e.preventDefault();e.stopPropagation();current=target;textarea.value=target.textContent;modal.classList.add('is-open');modal.setAttribute('aria-hidden','false');setTimeout(function(){textarea.focus();textarea.select();},20);},true);function close(){modal.classList.remove('is-open');modal.setAttribute('aria-hidden','true');current=null;textarea.value='';save.disabled=false;}modal.addEventListener('click',function(e){if(e.target===modal){close();}});cancel.addEventListener('click',close);document.addEventListener('keydown',function(e){if(e.key==='Escape'&&modal.classList.contains('is-open')){close();}});function decode(v){try{var b=window.atob(v),out='',i;for(i=0;itagName); $skip_tags = array('script', 'style', 'noscript', 'code', 'pre', 'textarea', 'svg', 'canvas', 'iframe', 'object'); if (in_array($tag, $skip_tags, true)) { return; } $class = ' ' . $node->getAttribute('class') . ' '; if ( 'wpadminbar' === $node->getAttribute('id') || false !== strpos($class, ' aitmt-fallback-switcher') || false !== strpos($class, ' aitmt-switcher') || false !== strpos($class, ' aitmt-menu-language-item') || false !== strpos($class, ' aitmt-menu-current-language') || false !== strpos($class, ' aitmt-menu-switcher-dropdown-toggle') || false !== strpos($class, ' aitmt-editor-toolbar') || false !== strpos($class, ' aitmt-editable') || false !== strpos($class, ' notranslate ') ) { return; } if ($node->hasAttribute('data-aitmt-no-translate') || $node->hasAttribute('data-aitmt-language-switcher') || ($node->hasAttribute('translate') && 'no' === strtolower($node->getAttribute('translate')))) { return; } aitmt_fallback_collect_translation_attributes($node, $attribute_nodes); } if (XML_TEXT_NODE === $node->nodeType) { $original = isset($node->nodeValue) ? $node->nodeValue : ''; $source = aitmt_fallback_normalize_text((string) $original); if (aitmt_fallback_is_translatable_text($source, true, array('source_type' => 'page_load', 'field_key' => 'dom_text'))) { $text_nodes[] = array( 'node' => $node, 'original' => (string) $original, 'source' => $source, 'hash' => aitmt_fallback_translation_hash($source), ); } } if (!$node->hasChildNodes()) { return; } foreach (iterator_to_array($node->childNodes) as $child) { aitmt_fallback_collect_translation_nodes($child, $text_nodes, $attribute_nodes); } } function aitmt_fallback_collect_translation_attributes($node, &$attribute_nodes) { $attributes = array('alt', 'title', 'placeholder', 'aria-label'); $tag = strtolower($node->tagName); if ('input' === $tag || 'button' === $tag) { $attributes[] = 'value'; } if ('meta' === $tag && $node->hasAttribute('content')) { $name = strtolower($node->getAttribute('name') ? $node->getAttribute('name') : $node->getAttribute('property')); if (in_array($name, array('description', 'og:title', 'og:description', 'twitter:title', 'twitter:description'), true)) { $attributes[] = 'content'; } } foreach (array_unique($attributes) as $attribute) { if (!$node->hasAttribute($attribute)) { continue; } $source = aitmt_fallback_normalize_text($node->getAttribute($attribute)); if (!aitmt_fallback_is_translatable_text($source, true, array('source_type' => 'page_load', 'field_key' => 'dom_attribute_' . $attribute))) { continue; } $attribute_nodes[] = array( 'node' => $node, 'attribute' => $attribute, 'source' => $source, 'hash' => aitmt_fallback_translation_hash($source), ); } } function aitmt_fallback_translate_segments($segments, $source_language, $target_language) { $clean_segments = array(); foreach ($segments as $hash => $text) { $text = aitmt_fallback_normalize_text((string) $text); if (aitmt_fallback_is_translatable_text($text, true, array('source_type' => 'page_load', 'field_key' => 'segment'))) { $clean_segments[$hash] = $text; } } if (empty($clean_segments)) { return array(); } $existing = aitmt_fallback_get_existing_translations($clean_segments, $source_language, $target_language); if (!empty($existing)) { aitmt_fallback_provider_stats_record_cache_hits(aitmt_fallback_active_provider(), count($existing)); } $missing = array_diff_key($clean_segments, $existing); if (empty($missing)) { return $existing; } $options = aitmt_fallback_options(); $limit = isset($options['max_segments_per_request']) ? absint($options['max_segments_per_request']) : 40; $missing = array_slice($missing, 0, max(1, min(100, $limit)), true); if ('1' === $options['queue_missing'] && !aitmt_fallback_is_unsafe_page_load_request()) { aitmt_fallback_enqueue_translation_batch($missing, $source_language, $target_language, 'page_load'); } if ('1' !== $options['translate_on_page_load']) { return $existing; } $translated = aitmt_fallback_translate_with_provider($missing, $source_language, $target_language, array('source_type' => 'page_load', 'batch_id' => aitmt_fallback_queue_batch_id('page'))); if (!is_wp_error($translated) && is_array($translated)) { $status = '1' === $options['draft_mode'] ? 'draft' : 'published'; aitmt_fallback_save_translation_batch($missing, $translated, $source_language, $target_language, $status); if ('draft' !== $status || current_user_can('manage_options')) { $existing = array_merge($existing, $translated); } } elseif (is_wp_error($translated)) { aitmt_fallback_log($translated->get_error_message()); } return $existing; } function aitmt_fallback_openai_translate_batch($segments, $source_language, $target_language) { $api_key = aitmt_fallback_provider_key('openai'); if (empty($api_key)) { return new WP_Error('aitmt_missing_openai_key', 'OpenAI API key is missing.'); } $languages = aitmt_fallback_languages(); $source_name = isset($languages[$source_language]) ? $languages[$source_language] : strtoupper($source_language); $target_name = isset($languages[$target_language]) ? $languages[$target_language] : strtoupper($target_language); $translation_instruction = aitmt_fallback_translation_instruction(); $items = array(); foreach ($segments as $hash => $text) { $items[] = array( 'id' => $hash, 'text' => $text, ); } $schema = array( 'type' => 'object', 'additionalProperties' => false, 'properties' => array( 'translations' => array( 'type' => 'array', 'items' => array( 'type' => 'object', 'additionalProperties' => false, 'properties' => array( 'id' => array('type' => 'string'), 'text' => array('type' => 'string'), ), 'required' => array('id', 'text'), ), ), ), 'required' => array('translations'), ); $body = array( 'model' => aitmt_fallback_provider_model('openai'), 'temperature' => aitmt_fallback_translation_temperature(), 'instructions' => sprintf( 'Translate to %s (%s). Preserve formatting. Return only translated text for each segment. %s Preserve placeholders, numbers, emails, URLs, shortcodes, HTML entities, and brand names. Return only valid JSON that matches the schema.', $target_name, strtoupper($target_language), $translation_instruction ), 'input' => wp_json_encode(array('segments' => $items), JSON_UNESCAPED_UNICODE), 'text' => array( 'format' => array( 'type' => 'json_schema', 'name' => 'aitmt_fallback_translate_batch', 'schema' => $schema, 'strict' => true, ), ), ); $response = wp_remote_post( 'https://api.openai.com/v1/responses', array( 'timeout' => 60, 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode($body), ) ); if (is_wp_error($response)) { return $response; } $status = (int) wp_remote_retrieve_response_code($response); $raw_body = (string) wp_remote_retrieve_body($response); $data = json_decode($raw_body, true); if ($status < 200 || $status >= 300) { $message = isset($data['error']['message']) ? $data['error']['message'] : 'OpenAI request failed.'; if (429 === $status) { aitmt_fallback_set_provider_cooldown('openai', HOUR_IN_SECONDS); } return new WP_Error('aitmt_openai_error', $message, array('status' => $status)); } $output_text = aitmt_fallback_extract_openai_output_text(is_array($data) ? $data : array()); $decoded = json_decode($output_text, true); if (!is_array($decoded)) { $decoded = json_decode(aitmt_fallback_strip_json_fence($output_text), true); } if (!is_array($decoded) || empty($decoded['translations']) || !is_array($decoded['translations'])) { return new WP_Error('aitmt_openai_parse_error', 'OpenAI returned an unexpected translation payload.'); } $translations = array(); foreach ($decoded['translations'] as $item) { if (empty($item['id']) || !array_key_exists('text', $item)) { continue; } $translations[(string) $item['id']] = (string) $item['text']; } return $translations; } function aitmt_fallback_extract_openai_output_text($data) { if (isset($data['output_text']) && is_string($data['output_text'])) { return $data['output_text']; } if (empty($data['output']) || !is_array($data['output'])) { return ''; } foreach ($data['output'] as $output) { if (!isset($output['content']) || !is_array($output['content'])) { continue; } foreach ($output['content'] as $content) { if (isset($content['text']) && is_string($content['text'])) { return $content['text']; } } } return ''; } function aitmt_fallback_strip_json_fence($text) { $text = trim((string) $text); $text = preg_replace('/^```(?:json)?\s*/i', '', $text); $text = preg_replace('/\s*```$/', '', (string) $text); return trim((string) $text); } function aitmt_fallback_extract_error_message($data, $fallback) { if (isset($data['error']['message']) && is_string($data['error']['message'])) { return $data['error']['message']; } if (isset($data['message']) && is_string($data['message'])) { return $data['message']; } if (isset($data['error']) && is_string($data['error'])) { return $data['error']; } return $fallback; } function aitmt_fallback_gemini_translate_batch($segments, $source_language, $target_language) { $api_key = aitmt_fallback_provider_key('gemini'); if (empty($api_key)) { return new WP_Error('aitmt_missing_gemini_key', 'Gemini API key is missing.'); } $languages = aitmt_fallback_languages(); $source_name = isset($languages[$source_language]) ? $languages[$source_language] : strtoupper($source_language); $target_name = isset($languages[$target_language]) ? $languages[$target_language] : strtoupper($target_language); $translation_instruction = aitmt_fallback_translation_instruction(); $items = array(); foreach ($segments as $hash => $text) { $items[] = array( 'id' => $hash, 'text' => $text, ); } $prompt = sprintf( "Translate to %s (%s). Preserve formatting. Return only translated text for each segment.\n%s\nPreserve placeholders, numbers, emails, URLs, shortcodes, HTML entities, and brand names.\nReturn only JSON in this exact shape: {\"translations\":[{\"id\":\"same id\",\"text\":\"translated text\"}]}.\n\nSegments:\n%s", $target_name, strtoupper($target_language), $translation_instruction, wp_json_encode($items, JSON_UNESCAPED_UNICODE) ); $body = array( 'contents' => array( array( 'role' => 'user', 'parts' => array( array('text' => $prompt), ), ), ), 'generationConfig' => array( 'temperature' => aitmt_fallback_translation_temperature(), 'responseMimeType' => 'application/json', ), ); $model = aitmt_fallback_provider_model('gemini'); $endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/' . rawurlencode($model) . ':generateContent'; $response = wp_remote_post( $endpoint, array( 'timeout' => 60, 'headers' => array( 'x-goog-api-key' => $api_key, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode($body), ) ); if (is_wp_error($response)) { return $response; } $status = (int) wp_remote_retrieve_response_code($response); $raw_body = (string) wp_remote_retrieve_body($response); $data = json_decode($raw_body, true); if ($status < 200 || $status >= 300) { $message = isset($data['error']['message']) ? $data['error']['message'] : 'Gemini request failed.'; if (429 === $status) { aitmt_fallback_set_provider_cooldown('gemini', HOUR_IN_SECONDS); } return new WP_Error('aitmt_gemini_error', $message, array('status' => $status)); } $output_text = aitmt_fallback_extract_gemini_output_text(is_array($data) ? $data : array()); $decoded = json_decode($output_text, true); if (!is_array($decoded)) { $decoded = json_decode(aitmt_fallback_strip_json_fence($output_text), true); } if (!is_array($decoded) || empty($decoded['translations']) || !is_array($decoded['translations'])) { return new WP_Error('aitmt_gemini_parse_error', 'Gemini returned an unexpected translation payload.'); } $translations = array(); foreach ($decoded['translations'] as $item) { if (empty($item['id']) || !array_key_exists('text', $item)) { continue; } $translations[(string) $item['id']] = (string) $item['text']; } return $translations; } function aitmt_fallback_extract_gemini_output_text($data) { if (empty($data['candidates']) || !is_array($data['candidates'])) { return ''; } foreach ($data['candidates'] as $candidate) { if (empty($candidate['content']['parts']) || !is_array($candidate['content']['parts'])) { continue; } foreach ($candidate['content']['parts'] as $part) { if (isset($part['text']) && is_string($part['text'])) { return $part['text']; } } } return ''; } function aitmt_fallback_google_translate_batch($segments, $source_language, $target_language) { $api_key = aitmt_fallback_provider_key('google_translate'); if (empty($api_key)) { return new WP_Error('aitmt_missing_google_translate_key', 'Google Translate API key is missing.'); } $hashes = array_keys($segments); $texts = array_values($segments); $endpoint = add_query_arg('key', $api_key, 'https://translation.googleapis.com/language/translate/v2'); $response = wp_remote_post( $endpoint, array( 'timeout' => 45, 'headers' => array( 'Content-Type' => 'application/json', 'Referer' => home_url('/'), ), 'body' => wp_json_encode(array( 'q' => $texts, 'source' => $source_language, 'target' => $target_language, 'format' => 'text', )), ) ); if (is_wp_error($response)) { return $response; } $status = (int) wp_remote_retrieve_response_code($response); $raw_body = (string) wp_remote_retrieve_body($response); $data = json_decode($raw_body, true); if ($status < 200 || $status >= 300) { $message = aitmt_fallback_extract_error_message(is_array($data) ? $data : array(), 'Google Translate request failed.'); if (429 === $status) { aitmt_fallback_set_provider_cooldown('google_translate', HOUR_IN_SECONDS); } return new WP_Error('aitmt_google_translate_error', $message, array('status' => $status)); } if (empty($data['data']['translations']) || !is_array($data['data']['translations'])) { return new WP_Error('aitmt_google_translate_parse_error', 'Google Translate returned an unexpected translation payload.'); } $translations = array(); foreach ($data['data']['translations'] as $index => $item) { if (!isset($hashes[$index]) || !isset($item['translatedText'])) { continue; } $translations[$hashes[$index]] = html_entity_decode((string) $item['translatedText'], ENT_QUOTES | ENT_HTML5, 'UTF-8'); } return $translations; } function aitmt_fallback_deepl_translate_batch($segments, $source_language, $target_language) { $api_key = aitmt_fallback_provider_key('deepl'); if (empty($api_key)) { return new WP_Error('aitmt_missing_deepl_key', 'DeepL API key is missing.'); } $options = aitmt_fallback_options(); $hashes = array_keys($segments); $texts = array_values($segments); $endpoint = ('pro' === $options['deepl_plan'] ? 'https://api.deepl.com' : 'https://api-free.deepl.com') . '/v2/translate'; $body_parts = array(); foreach ($texts as $text) { $body_parts[] = 'text=' . rawurlencode((string) $text); } $body_parts[] = 'target_lang=' . rawurlencode(aitmt_fallback_deepl_language_code($target_language, false)); $source_code = aitmt_fallback_deepl_language_code($source_language, true); if ($source_code) { $body_parts[] = 'source_lang=' . rawurlencode($source_code); } $response = wp_remote_post( $endpoint, array( 'timeout' => 45, 'headers' => array( 'Authorization' => 'DeepL-Auth-Key ' . $api_key, 'Content-Type' => 'application/x-www-form-urlencoded', ), 'body' => implode('&', $body_parts), ) ); if (is_wp_error($response)) { return $response; } $status = (int) wp_remote_retrieve_response_code($response); $raw_body = (string) wp_remote_retrieve_body($response); $data = json_decode($raw_body, true); if ($status < 200 || $status >= 300) { $message = aitmt_fallback_extract_error_message(is_array($data) ? $data : array(), 'DeepL request failed.'); if (429 === $status || 456 === $status) { aitmt_fallback_set_provider_cooldown('deepl', HOUR_IN_SECONDS); } return new WP_Error('aitmt_deepl_error', $message, array('status' => $status)); } if (empty($data['translations']) || !is_array($data['translations'])) { return new WP_Error('aitmt_deepl_parse_error', 'DeepL returned an unexpected translation payload.'); } $translations = array(); foreach ($data['translations'] as $index => $item) { if (!isset($hashes[$index]) || !isset($item['text'])) { continue; } $translations[$hashes[$index]] = (string) $item['text']; } return $translations; } function aitmt_fallback_deepl_language_code($language, $is_source = false) { $language = strtoupper(aitmt_fallback_normalize_language($language)); if ('EN' === $language && !$is_source) { return 'EN-US'; } return $language; } function aitmt_fallback_grok_translate_batch($segments, $source_language, $target_language) { $api_key = aitmt_fallback_provider_key('grok'); if (empty($api_key)) { return new WP_Error('aitmt_missing_grok_key', 'Grok API key is missing.'); } $languages = aitmt_fallback_languages(); $source_name = isset($languages[$source_language]) ? $languages[$source_language] : strtoupper($source_language); $target_name = isset($languages[$target_language]) ? $languages[$target_language] : strtoupper($target_language); $translation_instruction = aitmt_fallback_translation_instruction(); $items = array(); foreach ($segments as $hash => $text) { $items[] = array( 'id' => $hash, 'text' => $text, ); } $body = array( 'model' => aitmt_fallback_provider_model('grok'), 'temperature' => aitmt_fallback_translation_temperature(), 'input' => array( array( 'role' => 'system', 'content' => sprintf( 'Translate to %s (%s). Preserve formatting. Return only translated text for each segment. %s Preserve placeholders, numbers, emails, URLs, shortcodes, HTML entities, and brand names. Return only valid JSON.', $target_name, strtoupper($target_language), $translation_instruction ), ), array( 'role' => 'user', 'content' => 'Return exactly this JSON shape: {"translations":[{"id":"same id","text":"translated text"}]}. Segments: ' . wp_json_encode($items, JSON_UNESCAPED_UNICODE), ), ), ); $response = wp_remote_post( 'https://api.x.ai/v1/responses', array( 'timeout' => 60, 'headers' => array( 'Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode($body), ) ); if (is_wp_error($response)) { return $response; } $status = (int) wp_remote_retrieve_response_code($response); $raw_body = (string) wp_remote_retrieve_body($response); $data = json_decode($raw_body, true); if ($status < 200 || $status >= 300) { $message = aitmt_fallback_extract_error_message(is_array($data) ? $data : array(), 'Grok request failed.'); if (429 === $status) { aitmt_fallback_set_provider_cooldown('grok', HOUR_IN_SECONDS); } return new WP_Error('aitmt_grok_error', $message, array('status' => $status)); } $output_text = aitmt_fallback_extract_openai_output_text(is_array($data) ? $data : array()); $decoded = json_decode($output_text, true); if (!is_array($decoded)) { $decoded = json_decode(aitmt_fallback_strip_json_fence($output_text), true); } if (!is_array($decoded) || empty($decoded['translations']) || !is_array($decoded['translations'])) { return new WP_Error('aitmt_grok_parse_error', 'Grok returned an unexpected translation payload.'); } $translations = array(); foreach ($decoded['translations'] as $item) { if (empty($item['id']) || !array_key_exists('text', $item)) { continue; } $translations[(string) $item['id']] = (string) $item['text']; } return $translations; } function aitmt_fallback_translation_table() { global $wpdb; return $wpdb->prefix . 'aitmt_translations'; } function aitmt_fallback_translation_table_sql() { $table = aitmt_fallback_translation_table(); if (!preg_match('/^[A-Za-z0-9_]+$/', $table)) { return ''; } return esc_sql($table); } function aitmt_fallback_maybe_create_translation_table() { $schema_version = 5; if ((int) get_option('aitmt_fallback_schema_version', 0) >= $schema_version && get_option('aitmt_fallback_table_ready') && aitmt_fallback_translation_table_exists()) { return; } global $wpdb; $table = aitmt_fallback_translation_table_sql(); if ('' === $table) { return; } $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE {$table} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, source_hash char(64) NOT NULL, source_language varchar(16) NOT NULL, target_language varchar(16) NOT NULL, context varchar(80) NOT NULL DEFAULT 'html', source_text longtext NOT NULL, translated_text longtext NOT NULL, status varchar(20) NOT NULL DEFAULT 'published', provider varchar(40) NOT NULL DEFAULT 'openai', attempt_count int(10) unsigned NOT NULL DEFAULT 0, last_attempt_at datetime DEFAULT NULL, failed_reason text NOT NULL, batch_id varchar(64) NOT NULL DEFAULT '', source_type varchar(80) NOT NULL DEFAULT '', object_id bigint(20) unsigned NOT NULL DEFAULT 0, field_key varchar(120) NOT NULL DEFAULT '', created_at datetime NOT NULL, updated_at datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY aitmt_lookup (source_hash, source_language, target_language, context), KEY target_language (target_language), KEY status (status), KEY status_updated (status, updated_at), KEY batch_id (batch_id), KEY source_type (source_type), KEY object_id (object_id), KEY field_key (field_key) ) {$charset_collate};"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($sql); update_option('aitmt_fallback_table_ready', 1, false); update_option('aitmt_fallback_schema_version', $schema_version, false); } function aitmt_fallback_get_existing_translations($segments, $source_language, $target_language) { global $wpdb; if (empty($segments)) { return array(); } aitmt_fallback_maybe_create_translation_table(); $hashes = array_keys($segments); $placeholders = implode(',', array_fill(0, count($hashes), '%s')); $args = array_merge($hashes, array($source_language, $target_language, 'html')); $table = aitmt_fallback_translation_table_sql(); if ('' === $table) { return array(); } // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Dynamic IN placeholders and table name are controlled above; all values are prepared. $prepared = $wpdb->prepare( "SELECT source_hash, translated_text FROM {$table} WHERE source_hash IN ({$placeholders}) AND source_language = %s AND target_language = %s AND context = %s AND status IN ('published', 'manual', 'import', 'draft')", ...$args ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $rows = $wpdb->get_results($prepared, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Prepared immediately above. $map = array(); foreach ((array) $rows as $row) { $map[$row['source_hash']] = $row['translated_text']; } return $map; } function aitmt_fallback_save_translation_batch($source_segments, $translated_segments, $source_language, $target_language, $status) { foreach ($translated_segments as $hash => $translation) { if (!isset($source_segments[$hash])) { continue; } aitmt_fallback_save_translation($source_segments[$hash], (string) $translation, $source_language, $target_language, $status); } } function aitmt_fallback_save_translation($source_text, $translated_text, $source_language, $target_language, $status, $source_type = 'manual') { global $wpdb; $options = aitmt_fallback_options(); aitmt_fallback_maybe_create_translation_table(); $source_text = aitmt_fallback_normalize_text($source_text); $translated_text = trim((string) $translated_text); if ('' === $source_text || '' === $translated_text) { return false; } $now = current_time('mysql'); $result = $wpdb->replace( aitmt_fallback_translation_table(), array( 'source_hash' => aitmt_fallback_translation_hash($source_text), 'source_language' => aitmt_fallback_normalize_language($source_language), 'target_language' => aitmt_fallback_normalize_language($target_language), 'context' => 'html', 'source_text' => $source_text, 'translated_text' => $translated_text, 'status' => sanitize_key($status), 'provider' => aitmt_fallback_active_provider(), 'source_type' => aitmt_fallback_normalize_source_type($source_type) ?: 'manual', 'created_at' => $now, 'updated_at' => $now, ), array('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s') ); return false !== $result; } function aitmt_fallback_enqueue_translation_batch($source_segments, $source_language, $target_language, $source_type = 'scan', $object_id = 0, $field_key = '') { foreach ($source_segments as $hash => $source_text) { aitmt_fallback_enqueue_translation($source_text, $source_language, $target_language, $source_type, $object_id, $field_key); } } function aitmt_fallback_enqueue_translation($source_text, $source_language, $target_language, $source_type = 'scan', $object_id = 0, $field_key = '') { global $wpdb; aitmt_fallback_maybe_create_translation_table(); $source_type = aitmt_fallback_normalize_source_type($source_type); if ('' === $source_type) { aitmt_fallback_record_rejected_string('unknown_source_type', $source_text, array( 'source_type' => '', 'object_id' => absint($object_id), 'field_key' => $field_key, )); return false; } $source_text = aitmt_fallback_normalize_text($source_text); if (!aitmt_fallback_is_translatable_text($source_text, true, array( 'source_type' => $source_type, 'object_id' => absint($object_id), 'field_key' => $field_key, ))) { return false; } $now = current_time('mysql'); $table = aitmt_fallback_translation_table_sql(); if ('' === $table) { return false; } $hash = aitmt_fallback_translation_hash($source_text); $existing = $wpdb->get_var( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated by aitmt_fallback_translation_table_sql(); values are prepared below. $wpdb->prepare( "SELECT id FROM {$table} WHERE source_hash = %s AND source_language = %s AND target_language = %s AND context = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is controlled by aitmt_fallback_translation_table_sql(). $hash, aitmt_fallback_normalize_language($source_language), aitmt_fallback_normalize_language($target_language), 'html' ) ); if ($existing) { return false; } $result = $wpdb->insert( $table, array( 'source_hash' => $hash, 'source_language' => aitmt_fallback_normalize_language($source_language), 'target_language' => aitmt_fallback_normalize_language($target_language), 'context' => 'html', 'source_text' => $source_text, 'translated_text' => '', 'status' => 'queued', 'provider' => aitmt_fallback_active_provider(), 'source_type' => $source_type, 'object_id' => absint($object_id), 'field_key' => aitmt_fallback_normalize_field_key($field_key), 'created_at' => $now, 'updated_at' => $now, ), array('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s') ); return false !== $result; } function aitmt_fallback_queued_count() { global $wpdb; if (!aitmt_fallback_translation_table_exists()) { return 0; } $table = aitmt_fallback_translation_table_sql(); if ('' === $table) { return 0; } return (int) $wpdb->get_var( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated by aitmt_fallback_translation_table_sql(); value is prepared below. $wpdb->prepare( 'SELECT COUNT(*) FROM ' . $table . ' WHERE status = %s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Table name is controlled by aitmt_fallback_translation_table_sql(). 'queued' ) ); } function aitmt_fallback_queue_lock_ttl() { $options = aitmt_fallback_options(); return max(60, absint($options['queue_lock_ttl'] ?? 600)); } function aitmt_fallback_processing_ttl() { $options = aitmt_fallback_options(); return max(120, absint($options['processing_ttl'] ?? 900)); } function aitmt_fallback_retry_limit() { $options = aitmt_fallback_options(); return max(1, absint($options['retry_limit'] ?? 3)); } function aitmt_fallback_normalize_source_type($source_type) { $source_type = sanitize_key((string) $source_type); if ('' === $source_type || 'unknown' === $source_type) { return ''; } return substr($source_type, 0, 80); } function aitmt_fallback_normalize_field_key($field_key) { $field_key = sanitize_key((string) $field_key); return substr($field_key, 0, 120); } function aitmt_fallback_is_valid_queue_source_type($source_type) { return '' !== aitmt_fallback_normalize_source_type($source_type); } function aitmt_fallback_queue_batch_id($prefix = 'batch') { $prefix = sanitize_key((string) $prefix); $suffix = function_exists('wp_generate_uuid4') ? wp_generate_uuid4() : uniqid('', true); return substr($prefix . '-' . preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $suffix), 0, 64); } function aitmt_fallback_acquire_queue_lock($owner = 'queue') { $owner = sanitize_key((string) $owner); $token = aitmt_fallback_queue_batch_id($owner); $now = time(); $ttl = aitmt_fallback_queue_lock_ttl(); $lock = get_option('aitmt_queue_lock', array()); if (is_array($lock) && !empty($lock['expires']) && (int) $lock['expires'] > $now) { return new WP_Error( 'aitmt_queue_locked', sprintf('Translation queue is already running. Try again in %d second(s).', max(1, (int) $lock['expires'] - $now)), array('status' => 423) ); } if (!empty($lock)) { delete_option('aitmt_queue_lock'); } $next_lock = array( 'token' => $token, 'owner' => $owner, 'created_at' => current_time('mysql'), 'expires' => $now + $ttl, ); if (add_option('aitmt_queue_lock', $next_lock, '', false)) { return $token; } return new WP_Error('aitmt_queue_locked', 'Translation queue is already running.', array('status' => 423)); } function aitmt_fallback_release_queue_lock($token) { $lock = get_option('aitmt_queue_lock', array()); if (is_array($lock) && isset($lock['token']) && hash_equals((string) $lock['token'], (string) $token)) { delete_option('aitmt_queue_lock'); } } function aitmt_fallback_queue_lock_status() { $lock = get_option('aitmt_queue_lock', array()); if (!is_array($lock) || empty($lock['expires']) || (int) $lock['expires'] <= time()) { return array('active' => false, 'remaining' => 0, 'owner' => ''); } return array( 'active' => true, 'remaining' => max(1, (int) $lock['expires'] - time()), 'owner' => isset($lock['owner']) ? sanitize_key((string) $lock['owner']) : '', ); } function aitmt_fallback_datetime_minus($seconds) { return gmdate('Y-m-d H:i:s', time() - max(0, absint($seconds))); } function aitmt_fallback_reset_stale_processing_rows() { global $wpdb; $table = aitmt_fallback_translation_table_sql(); if ('' === $table || !aitmt_fallback_translation_table_exists()) { return array('requeued' => 0, 'failed' => 0); } $cutoff = aitmt_fallback_datetime_minus(aitmt_fallback_processing_ttl()); $retry_limit = aitmt_fallback_retry_limit(); $now = current_time('mysql'); $failed = (int) $wpdb->query( $wpdb->prepare( "UPDATE {$table} SET status = %s, failed_reason = %s, batch_id = %s, updated_at = %s WHERE status = %s AND last_attempt_at IS NOT NULL AND last_attempt_at < %s AND attempt_count >= %d", 'failed', 'Processing timed out after retry limit.', '', $now, 'processing', $cutoff, $retry_limit ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated by aitmt_fallback_translation_table_sql(); values are prepared above. $requeued = (int) $wpdb->query( $wpdb->prepare( "UPDATE {$table} SET status = %s, failed_reason = %s, batch_id = %s, updated_at = %s WHERE status = %s AND last_attempt_at IS NOT NULL AND last_attempt_at < %s AND attempt_count < %d", 'queued', 'Previous processing attempt timed out and was returned to queue.', '', $now, 'processing', $cutoff, $retry_limit ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated by aitmt_fallback_translation_table_sql(); values are prepared above. return array('requeued' => max(0, $requeued), 'failed' => max(0, $failed)); } function aitmt_fallback_mark_exhausted_queued_rows() { global $wpdb; $table = aitmt_fallback_translation_table_sql(); if ('' === $table || !aitmt_fallback_translation_table_exists()) { return 0; } $retry_limit = aitmt_fallback_retry_limit(); $now = current_time('mysql'); return (int) $wpdb->query( $wpdb->prepare( "UPDATE {$table} SET status = %s, failed_reason = %s, updated_at = %s WHERE status = %s AND attempt_count >= %d", 'failed', 'Retry limit reached before queue processing.', $now, 'queued', $retry_limit ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated by aitmt_fallback_translation_table_sql(); values are prepared above. } function aitmt_fallback_claim_queue_rows($table, $rows, $batch_id) { global $wpdb; $ids = array(); foreach ((array) $rows as $row) { $ids[] = absint($row['id'] ?? 0); } $ids = array_values(array_filter(array_unique($ids))); if (empty($ids)) { return array(); } $now = current_time('mysql'); $placeholders = implode(',', array_fill(0, count($ids), '%d')); $update_args = array_merge(array('processing', $now, $batch_id, '', $now, 'queued'), $ids); $wpdb->query( $wpdb->prepare( "UPDATE {$table} SET status = %s, attempt_count = attempt_count + 1, last_attempt_at = %s, batch_id = %s, failed_reason = %s, updated_at = %s WHERE status = %s AND id IN ({$placeholders})", ...$update_args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name and placeholders are controlled; values are prepared above. $select_args = array_merge(array($batch_id, 'processing'), $ids); return $wpdb->get_results( $wpdb->prepare( "SELECT id, source_hash, source_text, source_language, target_language, attempt_count, source_type, object_id, field_key FROM {$table} WHERE batch_id = %s AND status = %s AND id IN ({$placeholders}) ORDER BY id ASC", ...$select_args ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name and placeholders are controlled; values are prepared above. } function aitmt_fallback_mark_processing_rows_failed($table, $rows, $reason) { global $wpdb; $ids = array(); foreach ((array) $rows as $row) { $id = absint($row['id'] ?? 0); if ($id) { $ids[] = $id; } } $ids = array_values(array_filter(array_unique($ids))); if (empty($ids)) { return 0; } $reason = function_exists('mb_substr') ? mb_substr((string) $reason, 0, 500, 'UTF-8') : substr((string) $reason, 0, 500); $now = current_time('mysql'); $placeholders = implode(',', array_fill(0, count($ids), '%d')); $args = array_merge(array('failed', $reason, '', $now, 'processing'), $ids); return (int) $wpdb->query( $wpdb->prepare( "UPDATE {$table} SET status = %s, failed_reason = %s, batch_id = %s, updated_at = %s WHERE status = %s AND id IN ({$placeholders})", ...$args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name and placeholders are controlled; values are prepared above. } function aitmt_fallback_mark_processing_rows_after_failure($table, $rows, $reason) { global $wpdb; $retry_limit = aitmt_fallback_retry_limit(); $retry_ids = array(); $failed_ids = array(); foreach ((array) $rows as $row) { $id = absint($row['id'] ?? 0); if (!$id) { continue; } if (absint($row['attempt_count'] ?? 0) >= $retry_limit) { $failed_ids[] = $id; } else { $retry_ids[] = $id; } } $now = current_time('mysql'); $reason = substr(wp_strip_all_tags((string) $reason), 0, 500); if (!empty($failed_ids)) { $placeholders = implode(',', array_fill(0, count($failed_ids), '%d')); $args = array_merge(array('failed', $reason, '', $now, 'processing'), $failed_ids); $wpdb->query( $wpdb->prepare( "UPDATE {$table} SET status = %s, failed_reason = %s, batch_id = %s, updated_at = %s WHERE status = %s AND id IN ({$placeholders})", ...$args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name and placeholders are controlled; values are prepared above. } if (!empty($retry_ids)) { $placeholders = implode(',', array_fill(0, count($retry_ids), '%d')); $args = array_merge(array('queued', $reason, '', $now, 'processing'), $retry_ids); $wpdb->query( $wpdb->prepare( "UPDATE {$table} SET status = %s, failed_reason = %s, batch_id = %s, updated_at = %s WHERE status = %s AND id IN ({$placeholders})", ...$args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name and placeholders are controlled; values are prepared above. } } function aitmt_fallback_queue_estimate() { global $wpdb; aitmt_fallback_maybe_create_translation_table(); $estimate = array( 'queued' => 0, 'unknown_queued' => 0, 'technical_queued' => 0, 'blocked_characters' => 0, 'billable_jobs' => 0, 'unique_sources' => 0, 'target_languages' => 0, 'characters' => 0, 'chars_by_language' => array(), 'google_cost' => 0.0, ); $table = aitmt_fallback_translation_table_sql(); if ('' === $table || !aitmt_fallback_translation_table_exists()) { return $estimate; } $total_row = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(*) AS queued, SUM(CASE WHEN source_type = %s OR source_type = %s THEN 1 ELSE 0 END) AS unknown_queued FROM {$table} WHERE status = %s", '', 'unknown', 'queued' ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated; values are prepared above. if (is_array($total_row)) { $estimate['queued'] = absint($total_row['queued'] ?? 0); $estimate['unknown_queued'] = absint($total_row['unknown_queued'] ?? 0); } $technical_stats = aitmt_fallback_technical_queue_stats(); $estimate['technical_queued'] = absint($technical_stats['rows'] ?? 0); $estimate['blocked_characters'] = absint($technical_stats['characters'] ?? 0); $row = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(*) AS billable_jobs, COUNT(DISTINCT source_hash) AS unique_sources, COUNT(DISTINCT target_language) AS target_languages, COALESCE(SUM(characters), 0) AS characters FROM (SELECT source_hash, target_language, MAX(CHAR_LENGTH(source_text)) AS characters FROM {$table} WHERE status = %s AND source_type <> %s AND source_type <> %s AND CHAR_LENGTH(source_text) BETWEEN 3 AND 1200 AND source_text NOT LIKE %s AND source_text NOT LIKE %s GROUP BY source_hash, target_language) aitmt_jobs", 'queued', '', 'unknown', 'data:%', '%;base64,%' ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated; values are prepared above. if (is_array($row)) { $estimate['billable_jobs'] = absint($row['billable_jobs'] ?? 0); $estimate['unique_sources'] = absint($row['unique_sources'] ?? 0); $estimate['target_languages'] = absint($row['target_languages'] ?? 0); $estimate['characters'] = absint($row['characters'] ?? 0); $estimate['google_cost'] = aitmt_fallback_google_translation_cost_estimate($estimate['characters']); } $language_rows = $wpdb->get_results( $wpdb->prepare( "SELECT target_language, COUNT(*) AS jobs, COALESCE(SUM(characters), 0) AS characters FROM (SELECT target_language, source_hash, MAX(CHAR_LENGTH(source_text)) AS characters FROM {$table} WHERE status = %s AND source_type <> %s AND source_type <> %s AND CHAR_LENGTH(source_text) BETWEEN 3 AND 1200 AND source_text NOT LIKE %s AND source_text NOT LIKE %s GROUP BY target_language, source_hash) aitmt_language_jobs GROUP BY target_language ORDER BY characters DESC", 'queued', '', 'unknown', 'data:%', '%;base64,%' ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated; values are prepared above. foreach ((array) $language_rows as $language_row) { $language = aitmt_fallback_normalize_language($language_row['target_language'] ?? ''); if ('' === $language) { continue; } $estimate['chars_by_language'][$language] = array( 'jobs' => absint($language_row['jobs'] ?? 0), 'characters' => absint($language_row['characters'] ?? 0), ); } return $estimate; } function aitmt_fallback_google_translation_cost_estimate($characters) { $usd_per_million = (float) apply_filters('aitmt_google_translate_usd_per_million_chars', 20.0); return (max(0, absint($characters)) / 1000000) * $usd_per_million; } function aitmt_fallback_technical_queue_where_sql() { return "status = %s AND ( source_type = %s OR source_type = %s OR CHAR_LENGTH(source_text) < %d OR CHAR_LENGTH(source_text) > %d OR source_text LIKE %s OR source_text LIKE %s OR source_text REGEXP %s OR source_text REGEXP %s OR source_text REGEXP %s OR source_text REGEXP %s OR source_text REGEXP %s OR source_text REGEXP %s OR source_text REGEXP %s )"; } function aitmt_fallback_technical_queue_where_args($status = 'queued') { return array( sanitize_key((string) $status), '', 'unknown', 3, 1200, 'data:%', '%;base64,%', '^[[:space:]]*[\\{\\[]', '^(https?:)?//', '^[^[:space:]@]+@[^[:space:]@]+\\.[^[:space:]@]+$', '^[0-9a-fA-F]{16,}$', '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', '\\.(jpg|jpeg|png|gif|webp|svg|css|js|json|xml|pdf|zip|woff2?|ttf|eot)(\\?.*)?$', '^(elementor|wp|wc|woocommerce|css|js|data|aria|class|id|style|href|src|url|uuid|guid|nonce|token|key|settings?|layout|template|widget|container|section|column|heading|button|image|icon)[-_:.a-zA-Z0-9]*$', ); } function aitmt_fallback_technical_queue_stats() { global $wpdb; $stats = array('rows' => 0, 'characters' => 0); $table = aitmt_fallback_translation_table_sql(); if ('' === $table || !aitmt_fallback_translation_table_exists()) { return $stats; } $where = aitmt_fallback_technical_queue_where_sql(); $args = aitmt_fallback_technical_queue_where_args('queued'); $row = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(*) AS rows_count, COALESCE(SUM(CHAR_LENGTH(source_text)), 0) AS characters FROM {$table} WHERE {$where}", ...$args ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated; values are prepared above. if (is_array($row)) { $stats['rows'] = absint($row['rows_count'] ?? 0); $stats['characters'] = absint($row['characters'] ?? 0); } return $stats; } function aitmt_fallback_queue_source_stats() { global $wpdb; aitmt_fallback_maybe_create_translation_table(); $stats = array(); $table = aitmt_fallback_translation_table_sql(); if ('' === $table || !aitmt_fallback_translation_table_exists()) { return $stats; } $rows = $wpdb->get_results( "SELECT source_type, status, COUNT(*) AS total, COALESCE(SUM(CHAR_LENGTH(source_text)), 0) AS characters FROM {$table} GROUP BY source_type, status", ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Reporting query has no user input; table name is validated by aitmt_fallback_translation_table_sql(). foreach ((array) $rows as $row) { $source_type = sanitize_key((string) ($row['source_type'] ?? 'unknown')); $status = sanitize_key((string) ($row['status'] ?? 'unknown')); if (!isset($stats[$source_type])) { $stats[$source_type] = array( 'queued' => 0, 'processing' => 0, 'failed' => 0, 'published' => 0, 'draft' => 0, 'manual' => 0, 'import' => 0, 'queued_characters' => 0, 'processing_characters' => 0, 'failed_characters' => 0, 'total_characters' => 0, 'characters' => 0, ); } if (!isset($stats[$source_type][$status])) { $stats[$source_type][$status] = 0; } if (!isset($stats[$source_type][$status . '_characters'])) { $stats[$source_type][$status . '_characters'] = 0; } $characters = absint($row['characters'] ?? 0); $stats[$source_type][$status] += absint($row['total'] ?? 0); $stats[$source_type][$status . '_characters'] += $characters; $stats[$source_type]['total_characters'] += $characters; $stats[$source_type]['characters'] = $stats[$source_type]['queued_characters']; } return $stats; } function aitmt_fallback_queue_source_type_label($source_type) { $source_type = sanitize_key((string) $source_type); if ('' === $source_type) { return __('Legacy/missing source', 'ait-multilingual-translate'); } if ('unknown' === $source_type) { return __('Unknown source', 'ait-multilingual-translate'); } return $source_type; } function aitmt_fallback_queue_debug_report() { global $wpdb; aitmt_fallback_maybe_create_translation_table(); $report = array( 'longest' => array(), 'source_types' => array(), 'object_ids' => array(), 'field_keys' => array(), 'duplicate_hashes' => array('groups' => 0, 'rows' => 0), 'target_languages' => array(), 'skipped_reasons' => array(), 'rejected_samples' => array(), ); $table = aitmt_fallback_translation_table_sql(); if ('' === $table || !aitmt_fallback_translation_table_exists()) { return $report; } $report['longest'] = (array) $wpdb->get_results( $wpdb->prepare( "SELECT source_hash, target_language, source_type, object_id, field_key, CHAR_LENGTH(source_text) AS characters, LEFT(source_text, 180) AS sample FROM {$table} WHERE status = %s AND source_type <> %s AND source_type <> %s AND CHAR_LENGTH(source_text) BETWEEN 3 AND 1200 AND source_text NOT LIKE %s AND source_text NOT LIKE %s ORDER BY CHAR_LENGTH(source_text) DESC LIMIT %d", 'queued', '', 'unknown', 'data:%', '%;base64,%', 50 ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Diagnostic query has prepared values and a validated table name. $report['source_types'] = (array) $wpdb->get_results( $wpdb->prepare( "SELECT source_type, COUNT(*) AS rows_count, COUNT(DISTINCT source_hash) AS unique_sources, COALESCE(SUM(CHAR_LENGTH(source_text)), 0) AS characters FROM {$table} WHERE status = %s GROUP BY source_type ORDER BY rows_count DESC, characters DESC LIMIT %d", 'queued', 50 ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Diagnostic query has prepared values and a validated table name. $report['object_ids'] = (array) $wpdb->get_results( $wpdb->prepare( "SELECT object_id, COUNT(*) AS rows_count, COUNT(DISTINCT source_hash) AS unique_sources, COALESCE(SUM(CHAR_LENGTH(source_text)), 0) AS characters FROM {$table} WHERE status = %s GROUP BY object_id ORDER BY rows_count DESC, characters DESC LIMIT %d", 'queued', 50 ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Diagnostic query has prepared values and a validated table name. $report['field_keys'] = (array) $wpdb->get_results( $wpdb->prepare( "SELECT field_key, COUNT(*) AS rows_count, COUNT(DISTINCT source_hash) AS unique_sources, COALESCE(SUM(CHAR_LENGTH(source_text)), 0) AS characters FROM {$table} WHERE status = %s GROUP BY field_key ORDER BY rows_count DESC, characters DESC LIMIT %d", 'queued', 50 ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Diagnostic query has prepared values and a validated table name. $duplicate_row = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(*) AS groups_count, COALESCE(SUM(rows_count), 0) AS rows_count FROM (SELECT source_hash, target_language, COUNT(*) AS rows_count FROM {$table} WHERE status = %s GROUP BY source_hash, target_language HAVING COUNT(*) > 1) aitmt_duplicates", 'queued' ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Diagnostic query has prepared values and a validated table name. if (is_array($duplicate_row)) { $report['duplicate_hashes'] = array( 'groups' => absint($duplicate_row['groups_count'] ?? 0), 'rows' => absint($duplicate_row['rows_count'] ?? 0), ); } $report['target_languages'] = (array) $wpdb->get_results( $wpdb->prepare( "SELECT target_language, COUNT(*) AS rows_count, COUNT(DISTINCT source_hash) AS unique_sources, COALESCE(SUM(CHAR_LENGTH(source_text)), 0) AS characters FROM {$table} WHERE status = %s GROUP BY target_language ORDER BY rows_count DESC, characters DESC", 'queued' ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Diagnostic query has prepared values and a validated table name. $rejected_log = aitmt_fallback_rejected_strings_log(); $report['skipped_reasons'] = isset($rejected_log['reasons']) && is_array($rejected_log['reasons']) ? $rejected_log['reasons'] : array(); $report['rejected_samples'] = isset($rejected_log['samples']) && is_array($rejected_log['samples']) ? array_slice($rejected_log['samples'], 0, 50) : array(); return $report; } function aitmt_fallback_process_queue($limit = 25, $external_lock = '') { global $wpdb; aitmt_fallback_maybe_create_translation_table(); $lock = (string) $external_lock; $release_lock = false; if ('' === $lock) { $lock = aitmt_fallback_acquire_queue_lock('process_queue'); if (is_wp_error($lock)) { return array( 'ok' => false, 'message' => $lock->get_error_message(), 'processed' => 0, 'locked' => true, ); } $release_lock = true; } try { $limit = max(1, min(100, absint($limit))); $table = aitmt_fallback_translation_table_sql(); if ('' === $table) { return array( 'ok' => false, 'message' => 'Translation table is not available.', 'processed' => 0, ); } $stale = aitmt_fallback_reset_stale_processing_rows(); aitmt_fallback_mark_exhausted_queued_rows(); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT id, source_hash, source_text, source_language, target_language, source_type, object_id, field_key FROM {$table} WHERE status = %s AND attempt_count < %d AND source_type <> %s AND source_type <> %s ORDER BY updated_at ASC, id ASC LIMIT %d", 'queued', aitmt_fallback_retry_limit(), '', 'unknown', $limit ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated by aitmt_fallback_translation_table_sql(); values are prepared above. if (empty($rows)) { $estimate = aitmt_fallback_queue_estimate(); $message = (!empty($estimate['unknown_queued']) || !empty($estimate['technical_queued'])) ? sprintf('No safe billable queue items are ready. %1$d unknown and %2$d technical/non-translatable queued item(s) are blocked and will not be sent to the provider.', absint($estimate['unknown_queued']), absint($estimate['technical_queued'])) : 'Queue is empty.'; return array( 'ok' => true, 'message' => $message, 'processed' => 0, 'stale_requeued' => absint($stale['requeued'] ?? 0), 'stale_failed' => absint($stale['failed'] ?? 0), ); } $groups = array(); foreach ($rows as $row) { $key = $row['source_language'] . '|' . $row['target_language']; if (!isset($groups[$key])) { $groups[$key] = array( 'source_language' => $row['source_language'], 'target_language' => $row['target_language'], 'rows' => array(), ); } $groups[$key]['rows'][] = $row; } $options = aitmt_fallback_options(); $processed = 0; $routes = array(); $now = current_time('mysql'); $status = '1' === $options['draft_mode'] ? 'draft' : 'published'; foreach ($groups as $group) { $batch_id = aitmt_fallback_queue_batch_id('queue'); $claimed_rows = aitmt_fallback_claim_queue_rows($table, $group['rows'], $batch_id); if (empty($claimed_rows)) { continue; } $source_language = $group['source_language']; $target_language = $group['target_language']; $routes[] = $source_language . ' -> ' . $target_language; $segments = array(); $source_type = 'queue'; $invalid_rows = array(); $valid_claimed_rows = array(); foreach ($claimed_rows as $row) { if (!aitmt_fallback_is_valid_queue_source_type($row['source_type'] ?? '')) { aitmt_fallback_record_rejected_string('unknown_source_type', $row['source_text'] ?? '', array( 'source_type' => isset($row['source_type']) ? $row['source_type'] : '', 'object_id' => isset($row['object_id']) ? absint($row['object_id']) : 0, 'field_key' => isset($row['field_key']) ? $row['field_key'] : '', )); $invalid_rows[] = $row; continue; } if (!aitmt_fallback_is_translatable_text($row['source_text'] ?? '', true, array( 'source_type' => $row['source_type'] ?? '', 'object_id' => isset($row['object_id']) ? absint($row['object_id']) : 0, 'field_key' => isset($row['field_key']) ? $row['field_key'] : '', ))) { $invalid_rows[] = $row; continue; } $valid_claimed_rows[] = $row; $segments[$row['source_hash']] = $row['source_text']; if (!empty($row['source_type']) && 'unknown' !== $row['source_type']) { $source_type = sanitize_key((string) $row['source_type']); } } if (!empty($invalid_rows)) { aitmt_fallback_mark_processing_rows_failed($table, $invalid_rows, 'Skipped unsafe or unknown queue item before provider request.'); } $existing = aitmt_fallback_get_existing_translations($segments, $source_language, $target_language); $segments = array_diff_key($segments, $existing); if (empty($segments)) { foreach ($valid_claimed_rows as $row) { $wpdb->update( $table, array( 'status' => 'published', 'failed_reason' => '', 'batch_id' => '', 'updated_at' => $now, ), array('id' => (int) $row['id']), array('%s', '%s', '%s', '%s'), array('%d') ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated by aitmt_fallback_translation_table_sql(). } continue; } $translated = aitmt_fallback_translate_with_provider($segments, $source_language, $target_language, array( 'source_type' => $source_type, 'batch_id' => $batch_id, )); if (is_wp_error($translated)) { aitmt_fallback_log($translated->get_error_message()); aitmt_fallback_mark_processing_rows_after_failure($table, $valid_claimed_rows, $translated->get_error_message()); return array( 'ok' => false, 'message' => $translated->get_error_message(), 'processed' => $processed, 'source_language' => $source_language, 'target_language' => $target_language, 'routes' => implode(', ', $routes), 'batch_id' => $batch_id, ); } foreach ($valid_claimed_rows as $row) { if (!isset($segments[$row['source_hash']])) { continue; } if (!isset($translated[$row['source_hash']])) { aitmt_fallback_mark_processing_rows_after_failure($table, array($row), 'Provider did not return a translation for this string.'); continue; } $updated = $wpdb->update( $table, array( 'translated_text' => (string) $translated[$row['source_hash']], 'status' => $status, 'provider' => aitmt_fallback_active_provider(), 'failed_reason' => '', 'batch_id' => '', 'updated_at' => $now, ), array('id' => (int) $row['id']), array('%s', '%s', '%s', '%s', '%s', '%s'), array('%d') ); // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated by aitmt_fallback_translation_table_sql(). if (false !== $updated) { $processed++; } } } return array( 'ok' => true, 'message' => sprintf('Processed %d queued translation(s) across %d route(s).', $processed, count($groups)), 'processed' => $processed, 'routes' => implode(', ', $routes), 'stale_requeued' => absint($stale['requeued'] ?? 0), 'stale_failed' => absint($stale['failed'] ?? 0), ); } finally { if ($release_lock) { aitmt_fallback_release_queue_lock($lock); } } } function aitmt_fallback_enqueue_post_strings($post_id) { $post_id = absint($post_id); $post = get_post($post_id); if (!$post || 'publish' !== $post->post_status) { return array( 'strings' => 0, 'queued' => 0, ); } $source_language = aitmt_fallback_source_language(); $target_languages = array_values(array_diff(aitmt_fallback_enabled_languages(), array($source_language))); $source_type = sanitize_key((string) get_post_type($post_id)); $source_type = '' === $source_type ? 'post' : $source_type; $strings = array(); aitmt_fallback_add_scan_text($strings, get_the_title($post_id)); aitmt_fallback_add_scan_text($strings, get_post_field('post_excerpt', $post_id)); foreach (aitmt_fallback_extract_strings_from_html(get_post_field('post_content', $post_id)) as $text) { $strings[aitmt_fallback_translation_hash($text)] = $text; } foreach (aitmt_fallback_scan_post_meta($post_id) as $text) { $strings[aitmt_fallback_translation_hash($text)] = $text; } $queued = 0; foreach ($strings as $text) { foreach ($target_languages as $target_language) { if (aitmt_fallback_enqueue_translation($text, $source_language, $target_language, $source_type, $post_id, 'auto_save')) { $queued++; } } } return array( 'strings' => count($strings), 'queued' => $queued, ); } function aitmt_fallback_scan_saved_post($post_id, $post, $update) { $options = aitmt_fallback_options(); if ('1' !== $options['scan_on_save']) { return; } if (defined('WP_IMPORTING') && WP_IMPORTING) { return; } if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) { return; } if (!$post || 'publish' !== $post->post_status) { return; } $post_type = get_post_type($post_id); $post_types = get_post_types(array('public' => true), 'names'); if (!in_array($post_type, $post_types, true) || 'attachment' === $post_type) { return; } $result = aitmt_fallback_enqueue_post_strings($post_id); update_option('aitmt_last_auto_scan', array( 'post_id' => absint($post_id), 'post_type' => $post_type, 'strings' => (int) $result['strings'], 'queued' => (int) $result['queued'], 'created_at' => current_time('mysql'), ), false); } function aitmt_fallback_cron_schedules($schedules) { if (!isset($schedules['aitmt_five_minutes'])) { $schedules['aitmt_five_minutes'] = array( 'interval' => 5 * MINUTE_IN_SECONDS, 'display' => __('Every 5 minutes', 'ait-multilingual-translate'), ); } return $schedules; } function aitmt_fallback_sync_cron_schedule() { $options = aitmt_fallback_options(); $hook = 'aitmt_fallback_process_queue_event'; $scheduled = wp_next_scheduled($hook); if ('1' === $options['cron_enabled']) { if (!$scheduled) { $result = wp_schedule_event(time() + 120, 'aitmt_five_minutes', $hook, array(), true); if (is_wp_error($result)) { aitmt_fallback_set_last_error($result->get_error_message()); aitmt_fallback_log_debug_event('cron', 'Could not schedule translation queue.', array( 'code' => $result->get_error_code(), 'message' => $result->get_error_message(), )); } } return; } if ($scheduled) { wp_clear_scheduled_hook($hook); } } function aitmt_fallback_cron_process_queue() { $options = aitmt_fallback_options(); if ('1' !== $options['cron_enabled']) { return; } $limit = isset($options['max_segments_per_request']) ? absint($options['max_segments_per_request']) : 10; $limit = max(1, min(25, $limit)); $result = aitmt_fallback_process_queue($limit); update_option('aitmt_cron_queue_result', array_merge($result, array('created_at' => current_time('mysql'))), false); } function aitmt_fallback_normalize_text($text) { $text = html_entity_decode((string) $text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $text = preg_replace('/\s+/u', ' ', trim($text)); return null === $text ? '' : $text; } function aitmt_fallback_translation_hash($text) { return hash('sha256', aitmt_fallback_normalize_text($text)); } function aitmt_fallback_text_length($text) { return function_exists('mb_strlen') ? mb_strlen((string) $text, 'UTF-8') : strlen((string) $text); } function aitmt_fallback_non_translatable_reason($text, $field_key = '') { $text = trim((string) $text); $field_key = sanitize_key((string) $field_key); $length = aitmt_fallback_text_length($text); if ($length < 3) { return 'too_short'; } if ($length > 1200) { return 'too_long'; } if ('' !== $field_key && preg_match('/(^|_)(id|ids|uuid|guid|nonce|token|key|hash|class|classes|css|style|styles|script|scripts|url|href|src|srcset|icon|image|media|attachment|settings?|controls?|responsive|breakpoint|layout|template|widget_type|eltype|isinner)$/i', $field_key)) { return 'technical_field_key'; } if (preg_match('/^data:(?:image|text|application|font|video|audio)\//i', $text) || false !== stripos($text, ';base64,')) { return 'data_uri'; } if (preg_match('/^[A-Za-z0-9+\/]{160,}={0,2}$/', $text)) { return 'base64'; } if (function_exists('is_serialized') && is_serialized($text)) { return 'serialized_php'; } if (preg_match('/^[aObisC]:\d+:/', $text)) { return 'serialized_php'; } if ((preg_match('/^\{.*\}$/s', $text) || preg_match('/^\[.*\]$/s', $text)) && null !== json_decode($text, true)) { return 'json'; } if (preg_match('/^\{.*\}$/s', $text) || preg_match('/^\[.*\]$/s', $text)) { return 'settings_block'; } if (preg_match('/^(https?:)?\/\//i', $text) || preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $text)) { return 'url_or_email'; } if (preg_match('/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i', $text)) { return 'uuid'; } if (preg_match('/^[a-f0-9]{16,}$/i', $text) || preg_match('/^[a-z0-9_-]{24,}$/i', $text)) { return 'hash_or_token'; } if (preg_match('/^#[a-f0-9]{3,8}$/i', $text)) { return 'color_value'; } if (preg_match('/^[\d\s[:punct:]]+$/u', $text) || preg_match('/^[\d\s.,:;+\-\/\\\\%€$£¥₾]+$/u', $text)) { return 'number_or_punctuation'; } if (preg_match('/^(data-|aria-|class|id|style|href|src|srcset|alt|title|role|rel|target|width|height|sizes)[\w-]*\s*=/i', $text)) { return 'html_attribute'; } if (preg_match('/<\s*(script|style|svg|path|iframe|canvas)\b/i', $text) || preg_match('/\b(function|var|let|const|document|window|jQuery|console)\b|=>|;\s*$/', $text)) { return 'js_css_code'; } if (preg_match('/^[MmZzLlHhVvCcSsQqTtAa][\d\s,.\-]+$/', $text)) { return 'svg_path_data'; } if (preg_match('/^(elementor|wp|wc|woocommerce|css|js|data|aria|class|id|style|href|src|url|uuid|guid|nonce|token|key|settings?|layout|template|widget|container|section|column|heading|button|image|icon)[-_:.a-z0-9]*$/i', $text)) { return 'technical_keyword'; } if (preg_match('/^[.#]?[a-z][a-z0-9_.:-]{2,}(?:\s+[.#]?[a-z][a-z0-9_.:-]{2,}){0,8}$/i', $text) && preg_match('/[_.:#-]/', $text) && !preg_match('/[^\x00-\x7F]/', $text) && !preg_match('/\s(?:and|or|the|for|with|to|from|in|on|of|a|an)\s/i', ' ' . $text . ' ')) { return 'class_or_id'; } if (preg_match('/\.(jpg|jpeg|png|gif|webp|svg|css|js|json|xml|pdf|zip|woff2?|ttf|eot)(?:\?.*)?$/i', $text)) { return 'asset_or_file'; } if ('[' === substr($text, 0, 1) && '' === trim(strip_shortcodes($text))) { return 'shortcode_without_text'; } $letters = preg_match_all('/\p{L}/u', $text, $matches); $visible = preg_match_all('/[^\s]/u', $text, $visible_matches); if ($visible > 0 && ($letters / $visible) < 0.4) { return 'low_letter_ratio'; } return ''; } function aitmt_fallback_is_translatable_text($text, $record_rejection = false, $context = array()) { $context = is_array($context) ? $context : array(); $reason = aitmt_fallback_non_translatable_reason($text, isset($context['field_key']) ? $context['field_key'] : ''); if ('' !== $reason) { if ($record_rejection) { aitmt_fallback_record_rejected_string($reason, $text, $context); } return false; } return true; } function aitmt_fallback_record_rejected_string($reason, $text, $context = array()) { static $buffer = array(); static $registered = false; $reason = sanitize_key((string) $reason); if ('' === $reason) { $reason = 'rejected'; } $text = aitmt_fallback_normalize_text($text); $context = is_array($context) ? $context : array(); $buffer[] = array( 'reason' => $reason, 'source_type' => isset($context['source_type']) ? sanitize_key((string) $context['source_type']) : '', 'object_id' => isset($context['object_id']) ? absint($context['object_id']) : 0, 'field_key' => isset($context['field_key']) ? aitmt_fallback_normalize_field_key($context['field_key']) : '', 'characters' => aitmt_fallback_text_length($text), 'sample' => function_exists('mb_substr') ? mb_substr($text, 0, 220, 'UTF-8') : substr($text, 0, 220), ); if (!$registered) { $registered = true; add_action('shutdown', function () use (&$buffer) { aitmt_fallback_flush_rejected_string_log($buffer); $buffer = array(); }, 1); } } function aitmt_fallback_flush_rejected_string_log($entries) { if (empty($entries)) { return; } $log = get_option('aitmt_rejected_strings_log', array()); $log = is_array($log) ? $log : array(); $log['reasons'] = isset($log['reasons']) && is_array($log['reasons']) ? $log['reasons'] : array(); $log['samples'] = isset($log['samples']) && is_array($log['samples']) ? $log['samples'] : array(); foreach ((array) $entries as $entry) { $reason = sanitize_key((string) ($entry['reason'] ?? 'rejected')); if (!isset($log['reasons'][$reason])) { $log['reasons'][$reason] = array('count' => 0, 'characters' => 0); } $log['reasons'][$reason]['count']++; $log['reasons'][$reason]['characters'] += absint($entry['characters'] ?? 0); array_unshift($log['samples'], array_merge($entry, array('created_at' => current_time('mysql')))); } $log['samples'] = array_slice($log['samples'], 0, 50); update_option('aitmt_rejected_strings_log', $log, false); } function aitmt_fallback_rejected_strings_log() { $log = get_option('aitmt_rejected_strings_log', array()); return is_array($log) ? $log : array(); } function aitmt_fallback_is_translatable_text_legacy($text) { $text = trim((string) $text); $length = function_exists('mb_strlen') ? mb_strlen($text, 'UTF-8') : strlen($text); if ($length < 3) { return false; } if ($length > 1200) { return false; } if (preg_match('/^[\d\s[:punct:]]+$/u', $text)) { return false; } if (preg_match('/^[\d\s.,:;+\-\/\\\\%€$£¥₾]+$/u', $text)) { return false; } if (preg_match('/^\{.*\}$/s', $text) || preg_match('/^\[.*\]$/s', $text)) { return false; } if (preg_match('/^(https?:)?\/\//i', $text) || preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $text)) { return false; } if (preg_match('/^[a-f0-9]{16,}$/i', $text) || preg_match('/^[a-z0-9_-]{20,}$/i', $text)) { return false; } if (preg_match('/^#[a-f0-9]{3,8}$/i', $text)) { return false; } if (preg_match('/^(elementor|wp|wc|woocommerce|css|js|data|aria|class|id|style|href|src|url|uuid|guid|nonce|token|key|settings?|layout|template|widget|container|section|column|heading|button|image|icon)[-_:.a-z0-9]*$/i', $text)) { return false; } if (preg_match('/^[.#]?[a-z][a-z0-9_.:-]{2,}(?:\s+[.#]?[a-z][a-z0-9_.:-]{2,}){0,8}$/i', $text) && preg_match('/[_.:#-]/', $text) && !preg_match('/[^\x00-\x7F]/', $text) && !preg_match('/\s(?:and|or|the|for|with|to|from|in|on|of|a|an)\s/i', ' ' . $text . ' ')) { return false; } if (preg_match('/\.(jpg|jpeg|png|gif|webp|svg|css|js|json|xml|pdf|zip|woff2?|ttf|eot)$/i', $text)) { return false; } return true; } function aitmt_fallback_apply_original_spacing($original, $translation) { preg_match('/^\s*/u', (string) $original, $leading); preg_match('/\s*$/u', (string) $original, $trailing); return (isset($leading[0]) ? $leading[0] : '') . trim((string) $translation) . (isset($trailing[0]) ? $trailing[0] : ''); } function aitmt_fallback_log($message) { update_option('aitmt_last_error', current_time('mysql') . ' - ' . (string) $message, false); aitmt_fallback_log_event('error', (string) $message); do_action('aitmt_logged_error', (string) $message); } function aitmt_fallback_log_event($type, $message, $context = array()) { $extended = aitmt_fallback_debug_file_enabled(); $events = get_option('aitmt_debug_events', array()); $events = is_array($events) ? $events : array(); $context = is_array($context) ? $context : array(); $event = array( 'created_at' => current_time('mysql'), 'type' => sanitize_key((string) $type), 'message' => aitmt_fallback_mask_log_text(sanitize_text_field((string) $message)), 'context' => aitmt_fallback_shrink_log_context($context), ); if ($extended || 'error' === $event['type']) { array_unshift($events, $event); $events = array_slice($events, 0, 80); update_option('aitmt_debug_events', $events, false); } if ($extended) { aitmt_fallback_write_debug_log_file($event); } } function aitmt_fallback_shrink_log_context($context) { $clean = array(); foreach ((array) $context as $key => $value) { $key = sanitize_key((string) $key); if (is_array($value)) { $value = wp_json_encode($value); } elseif (is_bool($value)) { $value = $value ? 'true' : 'false'; } elseif (is_scalar($value)) { $value = (string) $value; } else { continue; } $value = aitmt_fallback_mask_log_text((string) $value); $clean[$key] = function_exists('mb_substr') ? mb_substr($value, 0, 900, 'UTF-8') : substr($value, 0, 900); } return $clean; } function aitmt_fallback_debug_file_enabled() { return '1' === get_option('aitmt_debug_file_enabled', '0'); } function aitmt_fallback_debug_log_dir() { $uploads = wp_upload_dir(null, false); if (empty($uploads['basedir']) || !empty($uploads['error'])) { return ''; } return trailingslashit($uploads['basedir']) . AITMT_PUBLIC_SLUG; } function aitmt_fallback_debug_log_path() { $dir = aitmt_fallback_debug_log_dir(); return $dir ? trailingslashit($dir) . 'debug.log' : ''; } function aitmt_fallback_ensure_debug_log_dir() { $dir = aitmt_fallback_debug_log_dir(); if ('' === $dir || !wp_mkdir_p($dir)) { return false; } $index = trailingslashit($dir) . 'index.php'; if (!file_exists($index)) { aitmt_fallback_write_local_file($index, " 1048576) { $existing = substr($existing, -524288); } return aitmt_fallback_write_local_file($path, $existing . $line . PHP_EOL); } function aitmt_fallback_mask_log_text($text) { $text = aitmt_fallback_redact_sensitive_text((string) $text); $patterns = array( '/sk-[A-Za-z0-9_\-]{12,}/', '/xai-[A-Za-z0-9_\-]{12,}/', '/AIzaSy[A-Za-z0-9_\-]{12,}/', '/sk-ant-[A-Za-z0-9_\-]{12,}/', ); return preg_replace($patterns, '[masked-api-key]', $text); } function aitmt_fallback_ajax_save_translation() { check_ajax_referer('aitmt_frontend_editor', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied.', 'ait-multilingual-translate')), 403); } $source_text = isset($_POST['sourceText']) ? sanitize_textarea_field(wp_unslash((string) $_POST['sourceText'])) : ''; $translated_text = isset($_POST['translatedText']) ? sanitize_textarea_field(wp_unslash((string) $_POST['translatedText'])) : ''; $source_language = isset($_POST['sourceLanguage']) ? aitmt_fallback_normalize_language(sanitize_key(wp_unslash((string) $_POST['sourceLanguage']))) : aitmt_fallback_source_language(); $target_language = isset($_POST['targetLanguage']) ? aitmt_fallback_normalize_language(sanitize_key(wp_unslash((string) $_POST['targetLanguage']))) : aitmt_fallback_current_language(); if ('' === $source_text || '' === $translated_text || '' === $target_language) { wp_send_json_error(array('message' => __('Missing translation data.', 'ait-multilingual-translate')), 400); } $saved = aitmt_fallback_save_translation($source_text, $translated_text, $source_language, $target_language, 'manual'); if (!$saved) { wp_send_json_error(array('message' => __('Translation was not saved.', 'ait-multilingual-translate')), 500); } wp_send_json_success(array('message' => __('Translation saved.', 'ait-multilingual-translate'))); } function aitmt_fallback_ajax_auto_translate_frontend() { check_ajax_referer('aitmt_frontend_editor', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied.', 'ait-multilingual-translate')), 403); } $source_text = isset($_POST['sourceText']) ? sanitize_textarea_field(wp_unslash((string) $_POST['sourceText'])) : ''; $source_language = isset($_POST['sourceLanguage']) ? aitmt_fallback_normalize_language(sanitize_key(wp_unslash((string) $_POST['sourceLanguage']))) : aitmt_fallback_source_language(); $target_language = isset($_POST['targetLanguage']) ? aitmt_fallback_normalize_language(sanitize_key(wp_unslash((string) $_POST['targetLanguage']))) : aitmt_fallback_current_language(); if ('' === $source_text || '' === $target_language) { wp_send_json_error(array('message' => __('Missing translation data.', 'ait-multilingual-translate')), 400); } if ($source_language === $target_language) { wp_send_json_error(array('message' => __('Source and target languages are the same.', 'ait-multilingual-translate')), 400); } if (!aitmt_fallback_is_translatable_text($source_text, true, array('source_type' => 'frontend_editor', 'field_key' => 'auto_translate'))) { wp_send_json_error(array('message' => __('This text looks technical or non-translatable and was not sent to the provider.', 'ait-multilingual-translate')), 400); } $provider = aitmt_fallback_active_provider(); if ('' === aitmt_fallback_provider_key($provider)) { wp_send_json_error(array('message' => __('No translation provider configured.', 'ait-multilingual-translate')), 400); } $hash = aitmt_fallback_translation_hash($source_text); $memory = aitmt_fallback_get_existing_translations(array($hash => $source_text), $source_language, $target_language); if (!empty($memory[$hash])) { aitmt_fallback_provider_stats_record_cache_hits($provider, 1); wp_send_json_success( array( 'translation' => (string) $memory[$hash], 'message' => __('Translation memory hit', 'ait-multilingual-translate'), 'provider' => aitmt_fallback_provider_label($provider), ) ); } $translated = aitmt_fallback_translate_with_provider(array($hash => $source_text), $source_language, $target_language, array('source_type' => 'frontend_editor', 'batch_id' => aitmt_fallback_queue_batch_id('front'))); if (is_wp_error($translated)) { $error_data = $translated->get_error_data(); $status = is_array($error_data) && !empty($error_data['status']) ? absint($error_data['status']) : 500; $message = aitmt_fallback_provider_error_message_for_editor($translated, $status); aitmt_fallback_log($translated->get_error_message()); wp_send_json_error(array('message' => $message), $status); } if (!is_array($translated) || !array_key_exists($hash, $translated) || '' === trim((string) $translated[$hash])) { wp_send_json_error(array('message' => __('Translation failed. Please try again.', 'ait-multilingual-translate')), 500); } wp_send_json_success( array( 'translation' => (string) $translated[$hash], /* translators: %s: Active translation provider label. */ 'message' => sprintf(__('Translated via %s', 'ait-multilingual-translate'), aitmt_fallback_provider_label($provider)), 'provider' => aitmt_fallback_provider_label($provider), ) ); } function aitmt_fallback_provider_error_message_for_editor($error, $status = 500) { if (!$error instanceof WP_Error) { return __('Translation failed. Please try again.', 'ait-multilingual-translate'); } $code = $error->get_error_code(); $message = strtolower($error->get_error_message()); if (false !== strpos($code, 'missing') || false !== strpos($message, 'api key is missing')) { return __('No translation provider configured.', 'ait-multilingual-translate'); } if (429 === (int) $status || false !== strpos($code, 'quota') || false !== strpos($code, 'cooldown') || false !== strpos($message, 'rate limit') || false !== strpos($message, 'quota')) { return __('Provider rate limit reached.', 'ait-multilingual-translate'); } return __('Translation failed. Please try again.', 'ait-multilingual-translate'); } function aitmt_fallback_add_scan_source_text(&$source_buckets, $source_type, $text, $object_id = 0, $field_key = '') { $source_type = aitmt_fallback_normalize_source_type($source_type); if ('' === $source_type) { aitmt_fallback_record_rejected_string('unknown_source_type', $text, array( 'source_type' => '', 'object_id' => absint($object_id), 'field_key' => $field_key, )); return; } $text = aitmt_fallback_normalize_text($text); if (!aitmt_fallback_is_translatable_text($text, true, array( 'source_type' => $source_type, 'object_id' => absint($object_id), 'field_key' => $field_key, ))) { return; } if (!isset($source_buckets[$source_type])) { $source_buckets[$source_type] = array(); } $source_buckets[$source_type][aitmt_fallback_translation_hash($text)] = array( 'text' => $text, 'object_id' => absint($object_id), 'field_key' => aitmt_fallback_normalize_field_key($field_key), ); } function aitmt_fallback_add_scan_source_html(&$source_buckets, $source_type, $html, $object_id = 0, $field_key = '') { foreach (aitmt_fallback_extract_strings_from_html($html) as $text) { aitmt_fallback_add_scan_source_text($source_buckets, $source_type, $text, $object_id, $field_key); } } function aitmt_fallback_scan_post_elementor_meta($post_id) { $strings = array(); foreach (array('_elementor_data', '_elementor_page_settings') as $meta_key) { foreach ((array) get_post_meta($post_id, $meta_key, false) as $raw_value) { if (!is_scalar($raw_value)) { continue; } $decoded = json_decode((string) $raw_value, true); if (is_array($decoded)) { aitmt_fallback_collect_elementor_scan_strings($strings, $decoded); } } } return array_values($strings); } function aitmt_fallback_scan_site_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_scan_site'); $result = aitmt_fallback_scan_site(); update_option('aitmt_scan_result', array_merge($result, array('created_at' => current_time('mysql'))), false); wp_safe_redirect(aitmt_fallback_admin_redirect_url_from_post('ait-multilingual-translate-scanner')); exit; } function aitmt_fallback_scan_site() { $source_language = aitmt_fallback_source_language(); $target_languages = array_values(array_diff(aitmt_fallback_enabled_languages(), array($source_language))); if (empty($target_languages)) { return array( 'ok' => false, 'message' => 'No target languages are selected.', 'strings' => 0, 'queued' => 0, ); } $source_buckets = array(); aitmt_fallback_add_scan_source_text($source_buckets, 'site', get_bloginfo('name'), 0, 'blogname'); aitmt_fallback_add_scan_source_text($source_buckets, 'site', get_bloginfo('description'), 0, 'description'); $post_types = get_post_types(array('public' => true), 'names'); unset($post_types['attachment']); $posts = get_posts(array( 'post_type' => array_values($post_types), 'post_status' => array('publish'), 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, )); foreach ((array) $posts as $post_id) { $source_type = sanitize_key((string) get_post_type($post_id)); $source_type = '' === $source_type ? 'post' : $source_type; aitmt_fallback_add_scan_source_text($source_buckets, $source_type, get_the_title($post_id), $post_id, 'post_title'); aitmt_fallback_add_scan_source_text($source_buckets, $source_type, get_post_field('post_excerpt', $post_id), $post_id, 'post_excerpt'); aitmt_fallback_add_scan_source_html($source_buckets, $source_type, get_post_field('post_content', $post_id), $post_id, 'post_content'); foreach (aitmt_fallback_scan_post_meta($post_id, true) as $text) { aitmt_fallback_add_scan_source_text($source_buckets, $source_type . '_meta', $text, $post_id, 'post_meta'); } foreach (aitmt_fallback_scan_post_elementor_meta($post_id) as $text) { aitmt_fallback_add_scan_source_text($source_buckets, 'elementor', $text, $post_id, 'elementor'); } } $taxonomies = get_taxonomies(array('public' => true), 'names'); foreach ((array) $taxonomies as $taxonomy) { $terms = get_terms(array('taxonomy' => $taxonomy, 'hide_empty' => false)); if (is_wp_error($terms)) { continue; } foreach ((array) $terms as $term) { aitmt_fallback_add_scan_source_text($source_buckets, 'taxonomy', $term->name, $term->term_id, 'term_name'); aitmt_fallback_add_scan_source_html($source_buckets, 'taxonomy', $term->description, $term->term_id, 'term_description'); } } foreach ((array) wp_get_nav_menus() as $menu) { $items = wp_get_nav_menu_items($menu->term_id); foreach ((array) $items as $item) { aitmt_fallback_add_scan_source_text($source_buckets, 'menu', $item->title, $item->ID, 'menu_title'); aitmt_fallback_add_scan_source_text($source_buckets, 'menu', $item->attr_title, $item->ID, 'menu_attr_title'); aitmt_fallback_add_scan_source_text($source_buckets, 'menu', $item->description, $item->ID, 'menu_description'); } } foreach (aitmt_fallback_scan_widget_options() as $text) { aitmt_fallback_add_scan_source_text($source_buckets, 'widget', $text, 0, 'widget'); } $unique_strings = array(); $queued = 0; foreach ($source_buckets as $source_type => $strings) { foreach ($strings as $hash => $item) { $text = is_array($item) ? (string) ($item['text'] ?? '') : (string) $item; $object_id = is_array($item) ? absint($item['object_id'] ?? 0) : 0; $field_key = is_array($item) ? aitmt_fallback_normalize_field_key($item['field_key'] ?? '') : ''; if ('' === $text) { continue; } $unique_strings[$hash] = $text; foreach ($target_languages as $target_language) { if (aitmt_fallback_enqueue_translation($text, $source_language, $target_language, $source_type, $object_id, $field_key)) { $queued++; } } } } return array( 'ok' => true, 'message' => sprintf('Scan finished. Found %d unique string(s), added %d new queue item(s).', count($unique_strings), $queued), 'strings' => count($unique_strings), 'queued' => $queued, 'targets' => implode(', ', $target_languages), ); } function aitmt_fallback_scan_post_meta($post_id, $exclude_elementor = false) { $allowed_private_keys = array( '_yoast_wpseo_title', '_yoast_wpseo_metadesc', '_aioseo_title', '_aioseo_description', 'rank_math_title', 'rank_math_description', '_elementor_data', '_elementor_page_settings', ); $meta = get_post_meta($post_id); $strings = array(); foreach ((array) $meta as $key => $values) { if ($exclude_elementor && in_array($key, array('_elementor_data', '_elementor_page_settings'), true)) { continue; } if (0 === strpos((string) $key, '_') && !in_array($key, $allowed_private_keys, true)) { continue; } foreach ((array) $values as $value) { if (!is_scalar($value) || is_serialized($value)) { continue; } $raw = (string) $value; $decoded = json_decode($raw, true); if (is_array($decoded)) { if (in_array($key, array('_elementor_data', '_elementor_page_settings'), true)) { aitmt_fallback_collect_elementor_scan_strings($strings, $decoded); } continue; } else { foreach (aitmt_fallback_extract_strings_from_html($raw) as $text) { $strings[aitmt_fallback_translation_hash($text)] = $text; } } } } return array_values($strings); } function aitmt_fallback_scan_widget_options() { global $wpdb; $strings = array(); $options_table = esc_sql($wpdb->options); $rows = $wpdb->get_col( $wpdb->prepare( "SELECT option_value FROM {$options_table} WHERE option_name LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Core options table name is controlled by WordPress. $wpdb->esc_like('widget_') . '%' ) ); foreach ((array) $rows as $raw_value) { $value = maybe_unserialize($raw_value); aitmt_fallback_collect_scalar_scan_strings($strings, $value); } return array_values($strings); } function aitmt_fallback_collect_elementor_scan_strings(&$strings, $value, $depth = 0, $current_key = '') { if ($depth > 8 || null === $value || is_bool($value)) { return; } if (is_array($value) || is_object($value)) { foreach ((array) $value as $key => $child) { aitmt_fallback_collect_elementor_scan_strings($strings, $child, $depth + 1, (string) $key); } return; } if (!is_scalar($value)) { return; } $allowed_keys = array( 'title', 'text', 'editor', 'description', 'subtitle', 'heading', 'caption', 'alt', 'label', 'placeholder', 'button_text', 'link_text', 'tab_title', 'accordion_title', 'item_title', 'testimonial_content', 'html', ); if (!in_array($current_key, $allowed_keys, true)) { return; } foreach (aitmt_fallback_extract_strings_from_html((string) $value) as $text) { $strings[aitmt_fallback_translation_hash($text)] = $text; } } function aitmt_fallback_collect_scalar_scan_strings(&$strings, $value, $depth = 0, $current_key = '') { if ($depth > 5 || null === $value || is_bool($value)) { return; } if (is_array($value) || is_object($value)) { foreach ((array) $value as $key => $child) { aitmt_fallback_collect_scalar_scan_strings($strings, $child, $depth + 1, (string) $key); } return; } if (!is_scalar($value)) { return; } $technical_key = sanitize_key((string) $current_key); if ('' !== $technical_key && preg_match('/(^|_)(id|ids|uuid|guid|hash|token|nonce|secret|password|key|class|classes|css|style|styles|color|icon|image|url|href|src|width|height|size|order|sort|type|taxonomy|term|object|instance|encoded|cache|transient)(_|$)/', $technical_key)) { return; } foreach (aitmt_fallback_extract_strings_from_html((string) $value) as $text) { $strings[aitmt_fallback_translation_hash($text)] = $text; } } function aitmt_fallback_extract_strings_from_html($html) { $html = (string) $html; $html = strip_shortcodes($html); $html = preg_replace('//s', ' ', $html); $html = preg_replace('/<(script|style|noscript)[^>]*>.*?<\/\1>/is', ' ', (string) $html); $html = preg_replace('/<\/(p|div|li|h[1-6]|td|th|blockquote|figcaption|button|a)>/i', "\n", (string) $html); $text = wp_strip_all_tags((string) $html, true); $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $lines = preg_split('/[\r\n]+/', (string) $text); $strings = array(); foreach ((array) $lines as $line) { $line = aitmt_fallback_normalize_text($line); if ('' === $line) { continue; } $length = function_exists('mb_strlen') ? mb_strlen($line, 'UTF-8') : strlen($line); $parts = $length > 320 ? preg_split('/(?<=[.!?;:])\s+/u', $line) : array($line); foreach ((array) $parts as $part) { aitmt_fallback_add_scan_text($strings, $part); } } return array_values($strings); } function aitmt_fallback_add_scan_text(&$strings, $text) { $text = aitmt_fallback_normalize_text($text); if (!aitmt_fallback_is_translatable_text($text, true, array('source_type' => 'scan', 'field_key' => 'html_text'))) { return; } if (preg_match('/^(https?:)?\/\//i', $text) || false !== strpos($text, '@')) { return; } $length = function_exists('mb_strlen') ? mb_strlen($text, 'UTF-8') : strlen($text); if ($length > 900) { return; } $strings[aitmt_fallback_translation_hash($text)] = $text; } function aitmt_fallback_save_manual_translation_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_save_manual_translation'); $id = isset($_POST['translation_id']) ? absint($_POST['translation_id']) : 0; $translated_text = isset($_POST['translated_text']) ? sanitize_textarea_field(wp_unslash((string) $_POST['translated_text'])) : ''; $source_text = isset($_POST['source_text']) ? sanitize_textarea_field(wp_unslash((string) $_POST['source_text'])) : ''; $source_language = isset($_POST['source_language']) ? aitmt_fallback_normalize_language(sanitize_key(wp_unslash((string) $_POST['source_language']))) : aitmt_fallback_source_language(); $target_language = isset($_POST['target_language']) ? aitmt_fallback_normalize_language(sanitize_key(wp_unslash((string) $_POST['target_language']))) : ''; if ($id && '' !== trim($translated_text) && aitmt_fallback_translation_table_exists()) { global $wpdb; $wpdb->update( aitmt_fallback_translation_table(), array( 'translated_text' => trim($translated_text), 'status' => 'manual', 'provider' => 'manual', 'updated_at' => current_time('mysql'), ), array('id' => $id), array('%s', '%s', '%s', '%s'), array('%d') ); } elseif ('' !== trim($source_text) && '' !== trim($translated_text) && '' !== $target_language) { aitmt_fallback_save_translation($source_text, $translated_text, $source_language, $target_language, 'manual'); } wp_safe_redirect(aitmt_fallback_admin_redirect_url_from_post('ait-multilingual-translate-translations')); exit; } function aitmt_fallback_save_translation_matrix_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_save_translation_matrix'); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Matrix fields are sanitized individually below before saving. $items = isset($_POST['aitmt_matrix']) && is_array($_POST['aitmt_matrix']) ? wp_unslash($_POST['aitmt_matrix']) : array(); $saved = 0; foreach ($items as $item) { if (!is_array($item)) { continue; } $id = isset($item['translation_id']) ? absint($item['translation_id']) : 0; $translated_text = isset($item['translated_text']) ? sanitize_textarea_field((string) $item['translated_text']) : ''; $source_text = isset($item['source_text']) ? sanitize_textarea_field((string) $item['source_text']) : ''; $source_language = isset($item['source_language']) ? aitmt_fallback_normalize_language((string) $item['source_language']) : aitmt_fallback_source_language(); $target_language = isset($item['target_language']) ? aitmt_fallback_normalize_language((string) $item['target_language']) : ''; if ('' === trim($translated_text)) { continue; } if ($id && aitmt_fallback_translation_table_exists()) { global $wpdb; $updated = $wpdb->update( aitmt_fallback_translation_table(), array( 'translated_text' => trim($translated_text), 'status' => 'manual', 'provider' => 'manual', 'updated_at' => current_time('mysql'), ), array('id' => $id), array('%s', '%s', '%s', '%s'), array('%d') ); if (false !== $updated) { $saved++; } } elseif ('' !== trim($source_text) && '' !== $target_language && aitmt_fallback_save_translation($source_text, $translated_text, $source_language, $target_language, 'manual')) { $saved++; } } update_option('aitmt_matrix_save_result', array( 'saved' => $saved, 'created_at' => current_time('mysql'), ), false); $per_page = isset($_POST['per_page']) ? max(25, min(100, absint($_POST['per_page']))) : 25; $paged = isset($_POST['paged']) ? max(1, absint($_POST['paged'])) : 1; wp_safe_redirect(add_query_arg(array( 'page' => 'ait-multilingual-translate-translations', 'per_page' => $per_page, 'paged' => $paged, ), admin_url('admin.php'))); exit; } function aitmt_fallback_export_translations_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_export_translations'); $language = isset($_POST['target_language']) ? aitmt_fallback_normalize_language(sanitize_key(wp_unslash((string) $_POST['target_language']))) : ''; $format = isset($_POST['export_format']) ? sanitize_key(wp_unslash((string) $_POST['export_format'])) : 'csv'; $format = in_array($format, array('csv', 'po', 'mo'), true) ? $format : 'csv'; if (!$language || !in_array($language, aitmt_fallback_enabled_languages(), true)) { wp_die(esc_html__('Invalid target language.', 'ait-multilingual-translate')); } $entries = aitmt_fallback_export_entries($language); $filename = 'ait-multilingual-translate-' . aitmt_fallback_source_language() . '-' . $language . '.' . $format; nocache_headers(); if ('csv' === $format) { header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); echo aitmt_fallback_csv_line(array('source_language', 'target_language', 'source_hash', 'source_text', 'translated_text', 'status', 'provider')); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Export endpoint intentionally streams CSV after nonce/capability checks. foreach ($entries as $entry) { echo aitmt_fallback_csv_line(array( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Export endpoint intentionally streams CSV after nonce/capability checks. $entry['source_language'], $entry['target_language'], $entry['source_hash'], $entry['source_text'], $entry['translated_text'], $entry['status'], $entry['provider'], )); } exit; } if ('mo' === $format) { header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="' . $filename . '"'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Export endpoint intentionally streams binary MO file content after nonce/capability checks. echo aitmt_fallback_build_mo($entries, $language); exit; } header('Content-Type: text/x-gettext-translation; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Export endpoint intentionally streams PO file content after nonce/capability checks. echo aitmt_fallback_build_po($entries, $language); exit; } function aitmt_fallback_import_translations_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_import_translations'); $uploaded_tmp_name = isset($_FILES['translation_file']['tmp_name']) ? sanitize_text_field(wp_unslash((string) $_FILES['translation_file']['tmp_name'])) : ''; if ('' === $uploaded_tmp_name || !is_uploaded_file($uploaded_tmp_name)) { wp_die(esc_html__('No import file uploaded.', 'ait-multilingual-translate')); } $language = isset($_POST['target_language']) ? aitmt_fallback_normalize_language(sanitize_key(wp_unslash((string) $_POST['target_language']))) : ''; $format = isset($_POST['import_format']) ? sanitize_key(wp_unslash((string) $_POST['import_format'])) : 'csv'; $format = in_array($format, array('csv', 'po', 'mo'), true) ? $format : 'csv'; if (!$language || !in_array($language, aitmt_fallback_enabled_languages(), true)) { wp_die(esc_html__('Invalid target language.', 'ait-multilingual-translate')); } $path = $uploaded_tmp_name; $source_language = aitmt_fallback_source_language(); if ('csv' === $format) { $entries = aitmt_fallback_parse_csv_import($path, $source_language, $language); } elseif ('mo' === $format) { $entries = aitmt_fallback_parse_mo(aitmt_fallback_read_local_file($path), $source_language, $language); } else { $entries = aitmt_fallback_parse_po(aitmt_fallback_read_local_file($path), $source_language, $language); } $saved = 0; foreach ($entries as $entry) { if (empty($entry['source_text']) || !array_key_exists('translated_text', $entry) || '' === trim((string) $entry['translated_text'])) { continue; } if (aitmt_fallback_save_translation($entry['source_text'], $entry['translated_text'], $source_language, $language, 'import')) { $saved++; } } update_option('aitmt_matrix_save_result', array( 'saved' => $saved, 'created_at' => current_time('mysql'), ), false); wp_safe_redirect(admin_url('admin.php?page=ait-multilingual-translate-translations')); exit; } function aitmt_fallback_export_entries($target_language) { global $wpdb; if (!aitmt_fallback_translation_table_exists()) { return array(); } $table = aitmt_fallback_translation_table_sql(); if ('' === $table) { return array(); } return (array) $wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated by aitmt_fallback_translation_table_sql(); values are prepared below. $wpdb->prepare( 'SELECT source_hash, source_language, target_language, source_text, translated_text, status, provider FROM ' . $table . ' WHERE source_language = %s AND target_language = %s ORDER BY source_text ASC', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Table name is controlled by aitmt_fallback_translation_table_sql(). aitmt_fallback_source_language(), $target_language ), ARRAY_A ); } function aitmt_fallback_csv_line($fields) { $escaped = array(); foreach ((array) $fields as $field) { $escaped[] = '"' . str_replace('"', '""', (string) $field) . '"'; } return implode(',', $escaped) . "\r\n"; } function aitmt_fallback_build_po($entries, $target_language) { $lines = array( 'msgid ""', 'msgstr ""', '"Project-Id-Version: AIT Multilingual Translate ' . AITMT_VERSION . '\n"', '"Language: ' . $target_language . '\n"', '"Content-Type: text/plain; charset=UTF-8\n"', '"Content-Transfer-Encoding: 8bit\n"', '', ); foreach ($entries as $entry) { $lines[] = '#. source_hash: ' . $entry['source_hash']; $lines[] = '#. status: ' . $entry['status'] . ', provider: ' . $entry['provider']; $lines[] = 'msgid ' . aitmt_fallback_po_quote($entry['source_text']); $lines[] = 'msgstr ' . aitmt_fallback_po_quote($entry['translated_text']); $lines[] = ''; } return implode("\n", $lines); } function aitmt_fallback_po_quote($text) { $text = str_replace(array('\\', '"', "\t", "\r", "\n"), array('\\\\', '\"', '\t', '', '\n'), (string) $text); return '"' . $text . '"'; } function aitmt_fallback_build_mo($entries, $target_language) { $pairs = array('' => "Project-Id-Version: AIT Multilingual Translate " . AITMT_VERSION . "\nLanguage: " . $target_language . "\nContent-Type: text/plain; charset=UTF-8\n"); foreach ($entries as $entry) { $pairs[(string) $entry['source_text']] = (string) $entry['translated_text']; } ksort($pairs, SORT_STRING); $count = count($pairs); $header_size = 28; $original_table_offset = $header_size; $translation_table_offset = $original_table_offset + ($count * 8); $string_offset = $translation_table_offset + ($count * 8); $original_table = ''; $translation_table = ''; $strings = ''; foreach ($pairs as $original => $translation) { $original_table .= pack('VV', strlen($original), $string_offset + strlen($strings)); $strings .= $original . "\0"; } foreach ($pairs as $original => $translation) { $translation_table .= pack('VV', strlen($translation), $string_offset + strlen($strings)); $strings .= $translation . "\0"; } return pack('V*', 0x950412de, 0, $count, $original_table_offset, $translation_table_offset, 0, 0) . $original_table . $translation_table . $strings; } function aitmt_fallback_parse_csv_import($path, $source_language, $target_language) { $entries = array(); $content = aitmt_fallback_read_local_file($path); if ('' === $content) { return $entries; } $lines = preg_split('/\r\n|\r|\n/', $content); if (empty($lines)) { return $entries; } $headers = str_getcsv((string) array_shift($lines)); if (empty($headers)) { return $entries; } $headers = array_map('sanitize_key', $headers); foreach ($lines as $line) { if ('' === trim((string) $line)) { continue; } $row = str_getcsv((string) $line); $item = array_combine($headers, array_pad($row, count($headers), '')); if (!$item) { continue; } $entries[] = array( 'source_language' => isset($item['source_language']) ? aitmt_fallback_normalize_language($item['source_language']) : $source_language, 'target_language' => $target_language, 'source_text' => isset($item['source_text']) ? (string) $item['source_text'] : '', 'translated_text' => isset($item['translated_text']) ? (string) $item['translated_text'] : '', ); } return $entries; } function aitmt_fallback_parse_po($content, $source_language, $target_language) { $entries = array(); $lines = preg_split('/\r\n|\r|\n/', (string) $content); $current = array('msgid' => '', 'msgstr' => ''); $field = ''; foreach ($lines as $line) { $trimmed = trim($line); if ('' === $trimmed) { if ('' !== $current['msgid'] && '' !== $current['msgstr']) { $entries[] = array( 'source_language' => $source_language, 'target_language' => $target_language, 'source_text' => $current['msgid'], 'translated_text' => $current['msgstr'], ); } $current = array('msgid' => '', 'msgstr' => ''); $field = ''; continue; } if (0 === strpos($trimmed, 'msgid ')) { $field = 'msgid'; $current[$field] = aitmt_fallback_po_unquote(substr($trimmed, 6)); continue; } if (0 === strpos($trimmed, 'msgstr ')) { $field = 'msgstr'; $current[$field] = aitmt_fallback_po_unquote(substr($trimmed, 7)); continue; } if ($field && isset($trimmed[0]) && '"' === $trimmed[0]) { $current[$field] .= aitmt_fallback_po_unquote($trimmed); } } if ('' !== $current['msgid'] && '' !== $current['msgstr']) { $entries[] = array( 'source_language' => $source_language, 'target_language' => $target_language, 'source_text' => $current['msgid'], 'translated_text' => $current['msgstr'], ); } return $entries; } function aitmt_fallback_po_unquote($text) { $text = trim((string) $text); if (strlen($text) >= 2 && '"' === $text[0] && '"' === substr($text, -1)) { $text = substr($text, 1, -1); } return stripcslashes($text); } function aitmt_fallback_parse_mo($content, $source_language, $target_language) { $entries = array(); $content = (string) $content; if (strlen($content) < 28) { return $entries; } $magic = unpack('V', substr($content, 0, 4)); $little_endian = isset($magic[1]) && 0x950412de === $magic[1]; $big_endian = isset($magic[1]) && 0xde120495 === $magic[1]; if (!$little_endian && !$big_endian) { return $entries; } $read = function ($offset) use ($content, $little_endian) { $value = unpack($little_endian ? 'V' : 'N', substr($content, $offset, 4)); return isset($value[1]) ? (int) $value[1] : 0; }; $count = $read(8); $original_table_offset = $read(12); $translation_table_offset = $read(16); for ($i = 0; $i < $count; $i++) { $original_length = $read($original_table_offset + ($i * 8)); $original_offset = $read($original_table_offset + ($i * 8) + 4); $translation_length = $read($translation_table_offset + ($i * 8)); $translation_offset = $read($translation_table_offset + ($i * 8) + 4); $source = substr($content, $original_offset, $original_length); $translation = substr($content, $translation_offset, $translation_length); if ('' === $source || '' === $translation) { continue; } $entries[] = array( 'source_language' => $source_language, 'target_language' => $target_language, 'source_text' => $source, 'translated_text' => $translation, ); } return $entries; } function aitmt_fallback_recent_translations($limit = 20) { global $wpdb; if (!aitmt_fallback_translation_table_exists()) { return array(); } $limit = max(1, min(100, absint($limit))); $table = aitmt_fallback_translation_table_sql(); if ('' === $table) { return array(); } return (array) $wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated by aitmt_fallback_translation_table_sql(); value is prepared below. $wpdb->prepare( 'SELECT id, source_language, target_language, source_text, translated_text, status, provider, updated_at FROM ' . $table . ' ORDER BY updated_at DESC, id DESC LIMIT %d', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Table name is controlled by aitmt_fallback_translation_table_sql(). $limit ), ARRAY_A ); } function aitmt_fallback_translation_matrix($limit = 60, $offset = 0, $search = '', $status_filter = 'all') { global $wpdb; if (!aitmt_fallback_translation_table_exists()) { return array(); } $source_language = aitmt_fallback_source_language(); $targets = array_values(array_diff(aitmt_fallback_enabled_languages(), array($source_language))); $table = aitmt_fallback_translation_table_sql(); if (empty($targets) || '' === $table) { return array(); } $limit = max(1, min(100, absint($limit))); $offset = max(0, absint($offset)); $search = trim((string) $search); $status_filter = in_array($status_filter, array('all', 'translated', 'untranslated'), true) ? $status_filter : 'all'; $where = 'source_language = %s'; $args = array($source_language); if ('' !== $search) { $like = '%' . $wpdb->esc_like($search) . '%'; $where .= ' AND (source_text LIKE %s OR source_hash LIKE %s)'; $args[] = $like; $args[] = $like; } $having = ''; if ('all' !== $status_filter) { $target_placeholders = implode(',', array_fill(0, count($targets), '%s')); $complete_expression = "COUNT(DISTINCT CASE WHEN target_language IN ({$target_placeholders}) AND translated_text <> '' AND status IN ('published', 'manual', 'import', 'draft') THEN target_language END)"; $having = ' HAVING ' . $complete_expression . ('translated' === $status_filter ? ' >= %d' : ' < %d'); $args = array_merge($args, $targets, array(count($targets))); } $args = array_merge($args, array($limit, $offset)); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Table name is validated; dynamic WHERE/HAVING fragments only contain fixed clauses with placeholders. $prepared = $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is validated by aitmt_fallback_translation_table_sql(); WHERE/HAVING fragments use fixed clauses and prepared placeholders. 'SELECT source_hash, source_language, MIN(source_text) AS source_text, MAX(updated_at) AS updated_at FROM ' . $table . " WHERE {$where} GROUP BY source_hash, source_language{$having} ORDER BY updated_at DESC LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is validated and values are prepared. ...$args ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $source_rows = (array) $wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Prepared immediately above. $prepared, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared immediately above. ARRAY_A ); $matrix = array(); $hashes = array(); foreach ($source_rows as $row) { $key = $row['source_language'] . '|' . $row['source_hash']; $hashes[] = $row['source_hash']; $matrix[$key] = array( 'source_hash' => $row['source_hash'], 'source_language' => $row['source_language'], 'source_text' => $row['source_text'], 'updated_at' => $row['updated_at'], 'targets' => array(), ); } if (empty($hashes)) { return array(); } $hash_placeholders = implode(',', array_fill(0, count($hashes), '%s')); $target_placeholders = implode(',', array_fill(0, count($targets), '%s')); $args = array_merge($hashes, array($source_language), $targets); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Dynamic IN placeholders and table name are controlled above; all values are prepared. $prepared = $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name and placeholder fragments are controlled; all values are prepared. 'SELECT id, source_hash, source_language, target_language, source_text, translated_text, status, provider, updated_at FROM ' . $table . " WHERE source_hash IN ({$hash_placeholders}) AND source_language = %s AND target_language IN ({$target_placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name and IN placeholders are controlled and values are prepared. ...$args ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $rows = (array) $wpdb->get_results( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Prepared immediately above. $prepared, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared immediately above. ARRAY_A ); foreach ($rows as $row) { $key = $row['source_language'] . '|' . $row['source_hash']; if (isset($matrix[$key])) { $matrix[$key]['targets'][$row['target_language']] = $row; } } return array_values($matrix); } function aitmt_fallback_translation_matrix_total($search = '', $status_filter = 'all') { global $wpdb; if (!aitmt_fallback_translation_table_exists()) { return 0; } $source_language = aitmt_fallback_source_language(); $targets = array_values(array_diff(aitmt_fallback_enabled_languages(), array($source_language))); $table = aitmt_fallback_translation_table_sql(); if (empty($targets) || '' === $table) { return 0; } $search = trim((string) $search); $status_filter = in_array($status_filter, array('all', 'translated', 'untranslated'), true) ? $status_filter : 'all'; $where = 'source_language = %s'; $args = array($source_language); if ('' !== $search) { $like = '%' . $wpdb->esc_like($search) . '%'; $where .= ' AND (source_text LIKE %s OR source_hash LIKE %s)'; $args[] = $like; $args[] = $like; } $having = ''; if ('all' !== $status_filter) { $target_placeholders = implode(',', array_fill(0, count($targets), '%s')); $complete_expression = "COUNT(DISTINCT CASE WHEN target_language IN ({$target_placeholders}) AND translated_text <> '' AND status IN ('published', 'manual', 'import', 'draft') THEN target_language END)"; $having = ' HAVING ' . $complete_expression . ('translated' === $status_filter ? ' >= %d' : ' < %d'); $args = array_merge($args, $targets, array(count($targets))); } // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Dynamic HAVING placeholders are built from a controlled target language list. $prepared = $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is validated by aitmt_fallback_translation_table_sql(); WHERE/HAVING fragments use fixed clauses and prepared placeholders. 'SELECT COUNT(*) FROM (SELECT source_hash FROM ' . $table . " WHERE {$where} GROUP BY source_hash, source_language{$having}) aitmt_sources", // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is validated and values are prepared. ...$args ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare return (int) $wpdb->get_var($prepared); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Prepared immediately above. } function aitmt_fallback_language_stats() { global $wpdb; $stats = array(); $source_language = aitmt_fallback_source_language(); $targets = array_values(array_diff(aitmt_fallback_enabled_languages(), array($source_language))); foreach ($targets as $target) { $stats[$target] = array( 'queued' => 0, 'published' => 0, 'manual' => 0, 'import' => 0, 'draft' => 0, ); } $table = aitmt_fallback_translation_table_sql(); if (!aitmt_fallback_translation_table_exists() || empty($targets) || '' === $table) { return $stats; } $placeholders = implode(',', array_fill(0, count($targets), '%s')); $args = array_merge(array($source_language), $targets); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Dynamic IN placeholders and table name are controlled above; all values are prepared. $prepared = $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name and placeholder fragments are controlled; all values are prepared. 'SELECT target_language, status, COUNT(*) AS total FROM ' . $table . " WHERE source_language = %s AND target_language IN ({$placeholders}) GROUP BY target_language, status", // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name and IN placeholders are controlled and values are prepared. ...$args ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber $rows = $wpdb->get_results($prepared, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Prepared immediately above. foreach ((array) $rows as $row) { $target = $row['target_language']; $status = $row['status']; if (!isset($stats[$target])) { $stats[$target] = array(); } $stats[$target][$status] = (int) $row['total']; } return $stats; } function aitmt_fallback_admin_page_start($title, $description = '') { ?>
    '; } function aitmt_fallback_sent_notice($success_message) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin notice status is read-only and sanitized. $status = isset($_GET['aitmt_sent']) ? sanitize_key(wp_unslash((string) $_GET['aitmt_sent'])) : ''; if ('' === $status) { return; } $class = 'notice-success'; $message = $success_message; if ('0' === $status) { $class = 'notice-error'; $message = __('The message could not be sent. Please check WordPress email delivery on this site.', 'ait-multilingual-translate'); } elseif ('consent' === $status) { $class = 'notice-warning'; $message = __('Please confirm consent before sending diagnostic information.', 'ait-multilingual-translate'); } echo '

    ' . esc_html($message) . '

    '; } function aitmt_fallback_report_bug_page() { if (!current_user_can('manage_options')) { return; } aitmt_fallback_admin_page_start( __('AI Translate - Report Bug', 'ait-multilingual-translate'), __('Send a safe technical report for Public Beta troubleshooting.', 'ait-multilingual-translate') ); aitmt_fallback_sent_notice(__('Bug report sent. Thank you for helping test AIT Multilingual Translate.', 'ait-multilingual-translate')); aitmt_fallback_public_beta_notice(); aitmt_fallback_support_development_block(true); ?>

    ' . esc_html__('Onboarding settings saved.', 'ait-multilingual-translate') . '

    '; } aitmt_fallback_public_beta_notice(); ?>

    1

    2

    $label) : ?>
    3

    4

    7

    5

    6

    'ait-multilingual-translate-translations', 'per_page' => $per_page); if (!empty($filters['matrix_search'])) { $base_args['matrix_search'] = (string) $filters['matrix_search']; } if (!empty($filters['matrix_status']) && 'all' !== $filters['matrix_status']) { $base_args['matrix_status'] = (string) $filters['matrix_status']; } $first_url = add_query_arg(array_merge($base_args, array('paged' => 1)), admin_url('admin.php')); $prev_url = add_query_arg(array_merge($base_args, array('paged' => max(1, $paged - 1))), admin_url('admin.php')); $next_url = add_query_arg(array_merge($base_args, array('paged' => min($total_pages, $paged + 1))), admin_url('admin.php')); $last_url = add_query_arg(array_merge($base_args, array('paged' => $total_pages)), admin_url('admin.php')); $window_start = max(1, min($paged - 4, max(1, $total_pages - 8))); $window_end = min($total_pages, $window_start + 8); ?>
    1) : ?> 1 2) : ?> ... $page)), admin_url('admin.php')); ?> ...
    $matrix_search, 'matrix_status' => $matrix_status, ); $paged = isset($_GET['paged']) ? max(1, absint($_GET['paged'])) : 1; // phpcs:enable WordPress.Security.NonceVerification.Recommended $total = aitmt_fallback_translation_matrix_total($matrix_search, $matrix_status); $total_pages = max(1, (int) ceil($total / $per_page)); $paged = min($paged, $total_pages); $offset = ($paged - 1) * $per_page; $matrix = aitmt_fallback_translation_matrix($per_page, $offset, $matrix_search, $matrix_status); $save_result = get_option('aitmt_matrix_save_result', array()); $save_result = is_array($save_result) ? $save_result : array(); aitmt_fallback_admin_page_start( __('AI Translate - Translations', 'ait-multilingual-translate'), __('Edit each source string per target language. Empty fields can be filled manually or translated from the queue.', 'ait-multilingual-translate') ); ?>

    $row) : ?>

    $language_row) : ?>

    $source_row) : ?>

    $row) : ?>

    $row) : ?>

    ' . (string) ($entry['target_language'] ?? '')); ?>

    =')); aitmt_fallback_render_debug_value('DOMDocument', class_exists('DOMDocument') ? 'available' : 'missing', class_exists('DOMDocument')); aitmt_fallback_render_debug_value('WP HTTP API', function_exists('wp_remote_post') ? 'available' : 'missing', function_exists('wp_remote_post')); aitmt_fallback_render_debug_value('Source language', aitmt_fallback_source_language(), !empty(aitmt_fallback_source_language())); aitmt_fallback_render_debug_value('Enabled languages', implode(', ', aitmt_fallback_enabled_languages()), count(aitmt_fallback_enabled_languages()) > 1); aitmt_fallback_render_debug_value('Current language', aitmt_fallback_current_language(), true); aitmt_fallback_render_debug_value('Provider', aitmt_fallback_provider_label($provider), true); aitmt_fallback_render_debug_value('Provider key', aitmt_fallback_mask_secret($api_key) . ' (' . aitmt_fallback_provider_key_source($provider) . ')', !empty($api_key)); aitmt_fallback_render_debug_value('Provider model', aitmt_fallback_provider_model($provider), true); aitmt_fallback_render_debug_value('Translation Mode', aitmt_fallback_translation_mode_label(), true); aitmt_fallback_render_debug_value('Quality mode', aitmt_fallback_quality_mode_label(), true); aitmt_fallback_render_debug_value('Provider cooldown', aitmt_fallback_provider_cooldown_remaining($provider) ? aitmt_fallback_provider_cooldown_remaining($provider) . ' seconds' : 'none', !aitmt_fallback_provider_cooldown_remaining($provider)); aitmt_fallback_render_debug_value('Provider daily characters', (string) aitmt_fallback_provider_chars_used($provider, gmdate('Ymd')), true); aitmt_fallback_render_debug_value('Provider monthly characters', (string) aitmt_fallback_provider_chars_used($provider, gmdate('Ym')), true); $provider_stats = aitmt_fallback_provider_stats_for($provider); aitmt_fallback_render_debug_value('Provider API requests', (string) absint($provider_stats['requests'] ?? 0), true); aitmt_fallback_render_debug_value('Estimated input tokens', (string) absint($provider_stats['input_tokens'] ?? 0), true); aitmt_fallback_render_debug_value('Estimated output tokens', (string) absint($provider_stats['output_tokens'] ?? 0), true); aitmt_fallback_render_debug_value('Estimated cost', number_format((float) ($provider_stats['estimated_cost'] ?? 0), 6), true); aitmt_fallback_render_debug_value('Translation memory cache hits', (string) absint($provider_stats['cache_hits'] ?? 0), true); aitmt_fallback_render_debug_value('Duplicate strings skipped', (string) absint($provider_stats['duplicate_skipped'] ?? 0), true); aitmt_fallback_render_debug_value('Translation table', aitmt_fallback_translation_table_exists() ? aitmt_fallback_translation_table() : 'missing', aitmt_fallback_translation_table_exists()); aitmt_fallback_render_debug_value('Queued strings', (string) aitmt_fallback_queued_count(), true); $queue_lock = aitmt_fallback_queue_lock_status(); aitmt_fallback_render_debug_value('Queue lock', !empty($queue_lock['active']) ? sprintf('active (%s, %d seconds)', $queue_lock['owner'], $queue_lock['remaining']) : 'inactive', empty($queue_lock['active'])); aitmt_fallback_render_debug_value('Queue lock TTL', (string) aitmt_fallback_queue_lock_ttl(), true); aitmt_fallback_render_debug_value('Processing TTL', (string) aitmt_fallback_processing_ttl(), true); aitmt_fallback_render_debug_value('Retry limit', (string) aitmt_fallback_retry_limit(), true); $request_log_summary = aitmt_fallback_provider_request_log_summary(); aitmt_fallback_render_debug_value('Logged provider chars today', (string) absint($request_log_summary['today'] ?? 0), true); aitmt_fallback_render_debug_value('Logged provider chars this month', (string) absint($request_log_summary['month'] ?? 0), true); aitmt_fallback_render_debug_value('Saved translations', (string) aitmt_fallback_translation_count(), true); aitmt_fallback_render_debug_value('Last error', $last_error ? $last_error : 'none', !$last_error); aitmt_fallback_render_debug_value('Extended file logging', $debug_file_enabled ? 'enabled' : 'disabled', !$debug_file_enabled ? null : true); aitmt_fallback_render_debug_value('Debug log file', $debug_log_path . ' (' . $debug_log_size . ')', $debug_file_enabled ? file_exists(dirname($debug_log_path)) : null); ?>

    ' . (isset($debug_result['target_language']) ? $debug_result['target_language'] : '')); ?>

    'disabled')); ?>
    aitmt_fallback_debug_log_path(), )); } else { aitmt_fallback_log_event('settings', 'Extended file logging disabled.'); } wp_safe_redirect(admin_url('admin.php?page=ait-multilingual-translate-debugger')); exit; } function aitmt_fallback_debug_clear_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_debug_clear'); delete_option('aitmt_debug_result'); delete_option('aitmt_last_error'); delete_option('aitmt_last_route_debug'); delete_option('aitmt_debug_events'); $log_path = aitmt_fallback_debug_log_path(); if (file_exists($log_path)) { aitmt_fallback_write_local_file($log_path, ''); } wp_safe_redirect(admin_url('admin.php?page=ait-multilingual-translate-debugger')); exit; } function aitmt_fallback_debug_download_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_debug_download'); $log_path = aitmt_fallback_debug_log_path(); if (!file_exists($log_path) || !is_readable($log_path)) { wp_die(esc_html__('Debug log file was not found.', 'ait-multilingual-translate')); } while (ob_get_level()) { ob_end_clean(); } nocache_headers(); header('Content-Type: text/plain; charset=utf-8'); header('Content-Disposition: attachment; filename="ait-multilingual-translate-debug-' . gmdate('Ymd-His') . '.log"'); $contents = aitmt_fallback_read_local_file($log_path); header('Content-Length: ' . (string) strlen($contents)); echo aitmt_fallback_redact_sensitive_text($contents); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Plain-text log download is sanitized/redacted above after nonce/capability checks. exit; } function aitmt_fallback_clear_provider_cooldown_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_clear_provider_cooldown'); $provider = aitmt_fallback_active_provider(); delete_transient(aitmt_fallback_provider_cooldown_key($provider)); update_option('aitmt_debug_result', array( 'ok' => true, 'message' => sprintf('Provider cooldown cleared for %s.', aitmt_fallback_provider_label($provider)), 'provider' => aitmt_fallback_provider_label($provider), 'model' => aitmt_fallback_provider_model($provider), 'created_at' => current_time('mysql'), ), false); wp_safe_redirect(admin_url('admin.php?page=ait-multilingual-translate-debugger')); exit; } function aitmt_fallback_process_queue_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_process_queue'); $options = aitmt_fallback_options(); $limit = isset($options['max_segments_per_request']) ? absint($options['max_segments_per_request']) : 40; $result = aitmt_fallback_process_queue($limit); update_option('aitmt_queue_result', array_merge($result, array('created_at' => current_time('mysql'))), false); if (empty($result['ok']) && !empty($result['message'])) { update_option('aitmt_last_error', current_time('mysql') . ' - ' . $result['message'], false); } wp_safe_redirect(aitmt_fallback_admin_redirect_url_from_post('ait-multilingual-translate-scanner')); exit; } function aitmt_fallback_translate_all_queue_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_translate_all_queue'); $confirmed = !empty($_POST['aitmt_confirm_translate_all']) && '1' === sanitize_text_field(wp_unslash((string) $_POST['aitmt_confirm_translate_all'])); if (!$confirmed) { update_option('aitmt_queue_result', array( 'ok' => false, 'message' => __('Translate All was not started because the cost confirmation checkbox was not selected.', 'ait-multilingual-translate'), 'processed' => 0, 'created_at' => current_time('mysql'), ), false); wp_safe_redirect(aitmt_fallback_admin_redirect_url_from_post('ait-multilingual-translate-scanner')); exit; } $estimate = aitmt_fallback_queue_estimate(); if (!empty($estimate['unknown_queued']) || !empty($estimate['technical_queued'])) { update_option('aitmt_queue_result', array( 'ok' => false, 'message' => sprintf( 'Translate All was blocked because the queue contains %1$d unknown and %2$d technical/non-translatable queued item(s). Clear these rows before sending text to the provider.', absint($estimate['unknown_queued'] ?? 0), absint($estimate['technical_queued'] ?? 0) ), 'processed' => 0, 'created_at' => current_time('mysql'), ), false); wp_safe_redirect(aitmt_fallback_admin_redirect_url_from_post('ait-multilingual-translate-scanner')); exit; } $options = aitmt_fallback_options(); $limit = isset($options['max_segments_per_request']) ? absint($options['max_segments_per_request']) : 40; $limit = max(1, min(100, $limit)); $queued_before = aitmt_fallback_queued_count(); $max_batches = (int) apply_filters('aitmt_translate_all_max_batches', 10); $max_batches = max(1, min(25, $max_batches)); $deadline = time() + (int) apply_filters('aitmt_translate_all_time_budget', 25); $processed = 0; $batches = 0; $routes = array(); $lock = aitmt_fallback_acquire_queue_lock('translate_all'); if (is_wp_error($lock)) { update_option('aitmt_queue_result', array( 'ok' => false, 'message' => $lock->get_error_message(), 'processed' => 0, 'locked' => true, 'created_at' => current_time('mysql'), ), false); wp_safe_redirect(aitmt_fallback_admin_redirect_url_from_post('ait-multilingual-translate-scanner')); exit; } $last_result = array( 'ok' => true, 'message' => 'Queue is empty.', 'processed' => 0, ); try { while ($queued_before > 0 && $batches < $max_batches && time() < $deadline) { $result = aitmt_fallback_process_queue($limit, $lock); $last_result = is_array($result) ? $result : $last_result; $batches++; if (!empty($last_result['routes'])) { $routes[] = (string) $last_result['routes']; } $batch_processed = isset($last_result['processed']) ? absint($last_result['processed']) : 0; $processed += $batch_processed; if (empty($last_result['ok']) || $batch_processed < 1 || aitmt_fallback_queued_count() < 1) { break; } } } finally { aitmt_fallback_release_queue_lock($lock); } $remaining = aitmt_fallback_queued_count(); $ok = !empty($last_result['ok']); if ($queued_before < 1) { $message = __('Queue is empty.', 'ait-multilingual-translate'); } elseif (!$ok) { $message = sprintf( /* translators: 1: processed count, 2: original queued count, 3: error message. */ __('Translate All stopped after processing %1$d of %2$d queued translation(s): %3$s', 'ait-multilingual-translate'), $processed, $queued_before, isset($last_result['message']) ? (string) $last_result['message'] : __('Provider error.', 'ait-multilingual-translate') ); } elseif ($remaining > 0 && $processed < 1 && !empty($last_result['message'])) { $message = (string) $last_result['message']; } elseif ($remaining > 0) { $message = sprintf( /* translators: 1: processed count, 2: original queued count, 3: remaining count, 4: batch count. */ __('Translate All processed %1$d of %2$d queued translation(s) in %4$d safe batch(es). %3$d item(s) remain. Run it again or enable background processing; provider quota or server time limits may stop long runs.', 'ait-multilingual-translate'), $processed, $queued_before, $remaining, $batches ); } else { $message = sprintf( /* translators: 1: processed count, 2: batch count. */ __('Translate All finished. Processed %1$d queued translation(s) in %2$d safe batch(es).', 'ait-multilingual-translate'), $processed, $batches ); } $routes = array_values(array_unique(array_filter($routes))); $queue_result = array_merge($last_result, array( 'ok' => $ok, 'message' => $message, 'processed' => $processed, 'routes' => implode(', ', $routes), 'queued_before' => $queued_before, 'queued_remaining' => $remaining, 'batches' => $batches, 'translate_all' => true, 'created_at' => current_time('mysql'), )); update_option('aitmt_queue_result', $queue_result, false); if (!$ok && !empty($message)) { update_option('aitmt_last_error', current_time('mysql') . ' - ' . wp_strip_all_tags($message), false); } wp_safe_redirect(aitmt_fallback_admin_redirect_url_from_post('ait-multilingual-translate-scanner')); exit; } function aitmt_fallback_clear_queue_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_clear_queue'); global $wpdb; if (aitmt_fallback_translation_table_exists()) { $wpdb->delete(aitmt_fallback_translation_table(), array('status' => 'queued'), array('%s')); } update_option('aitmt_queue_result', array( 'ok' => true, 'message' => 'Queued items cleared.', 'processed' => 0, 'created_at' => current_time('mysql'), ), false); wp_safe_redirect(admin_url('admin.php?page=ait-multilingual-translate-scanner')); exit; } function aitmt_fallback_clear_unknown_queue_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_clear_unknown_queue'); $confirmed = !empty($_POST['aitmt_confirm_clear_unknown']) && '1' === sanitize_text_field(wp_unslash((string) $_POST['aitmt_confirm_clear_unknown'])); if (!$confirmed) { update_option('aitmt_queue_result', array( 'ok' => false, 'message' => __('Unknown queue items were not cleared because the confirmation checkbox was not selected.', 'ait-multilingual-translate'), 'processed' => 0, 'created_at' => current_time('mysql'), ), false); wp_safe_redirect(aitmt_fallback_admin_redirect_url_from_post('ait-multilingual-translate-scanner')); exit; } global $wpdb; $deleted = 0; $table = aitmt_fallback_translation_table_sql(); if ('' !== $table && aitmt_fallback_translation_table_exists()) { $deleted = (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE status = %s AND (source_type = %s OR source_type = %s)", 'queued', '', 'unknown' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated; values are prepared above. } update_option('aitmt_queue_result', array( 'ok' => true, 'message' => sprintf('Cleared %d unknown queued item(s).', $deleted), 'processed' => 0, 'created_at' => current_time('mysql'), ), false); wp_safe_redirect(aitmt_fallback_admin_redirect_url_from_post('ait-multilingual-translate-scanner')); exit; } function aitmt_fallback_clear_technical_queue_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_clear_technical_queue'); $confirmed = !empty($_POST['aitmt_confirm_clear_technical']) && '1' === sanitize_text_field(wp_unslash((string) $_POST['aitmt_confirm_clear_technical'])); if (!$confirmed) { update_option('aitmt_queue_result', array( 'ok' => false, 'message' => __('Technical queue items were not cleared because the confirmation checkbox was not selected.', 'ait-multilingual-translate'), 'processed' => 0, 'created_at' => current_time('mysql'), ), false); wp_safe_redirect(aitmt_fallback_admin_redirect_url_from_post('ait-multilingual-translate-scanner')); exit; } global $wpdb; $deleted = 0; $characters = 0; $table = aitmt_fallback_translation_table_sql(); if ('' !== $table && aitmt_fallback_translation_table_exists()) { $stats = aitmt_fallback_technical_queue_stats(); $characters = absint($stats['characters'] ?? 0); $where = aitmt_fallback_technical_queue_where_sql(); $args = aitmt_fallback_technical_queue_where_args('queued'); $deleted = (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE {$where}", ...$args ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated; values are prepared above. } update_option('aitmt_queue_result', array( 'ok' => true, 'message' => sprintf('Cleared %1$d technical/non-translatable queued item(s), about %2$d character(s).', $deleted, $characters), 'processed' => 0, 'created_at' => current_time('mysql'), ), false); wp_safe_redirect(aitmt_fallback_admin_redirect_url_from_post('ait-multilingual-translate-scanner')); exit; } function aitmt_fallback_clear_page_load_queue_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_clear_page_load_queue'); $confirmed = !empty($_POST['aitmt_confirm_clear_page_load']) && '1' === sanitize_text_field(wp_unslash((string) $_POST['aitmt_confirm_clear_page_load'])); if (!$confirmed) { update_option('aitmt_queue_result', array( 'ok' => false, 'message' => __('Page-load queue items were not cleared because the confirmation checkbox was not selected.', 'ait-multilingual-translate'), 'processed' => 0, 'created_at' => current_time('mysql'), ), false); wp_safe_redirect(aitmt_fallback_admin_redirect_url_from_post('ait-multilingual-translate-scanner')); exit; } global $wpdb; $deleted = 0; $characters = 0; $table = aitmt_fallback_translation_table_sql(); if ('' !== $table && aitmt_fallback_translation_table_exists()) { $characters = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COALESCE(SUM(CHAR_LENGTH(source_text)), 0) FROM {$table} WHERE status = %s AND source_type = %s", 'queued', 'page_load' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated; values are prepared above. $deleted = (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE status = %s AND source_type = %s", 'queued', 'page_load' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated; values are prepared above. } update_option('aitmt_queue_result', array( 'ok' => true, 'message' => sprintf('Cleared %1$d page-load queued item(s), about %2$d queued character(s).', $deleted, absint($characters)), 'processed' => 0, 'created_at' => current_time('mysql'), ), false); wp_safe_redirect(aitmt_fallback_admin_redirect_url_from_post('ait-multilingual-translate-scanner')); exit; } function aitmt_fallback_run_openai_debug_test() { $started = microtime(true); $provider = aitmt_fallback_active_provider(); $api_key = aitmt_fallback_provider_key($provider); $provider_label = aitmt_fallback_provider_label($provider); $model = aitmt_fallback_provider_model($provider); $source_language = aitmt_fallback_source_language(); $enabled = aitmt_fallback_enabled_languages(); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- The debug handler verifies aitmt_debug_test before calling this helper. $target_language = isset($_POST['target_language']) ? aitmt_fallback_normalize_language(sanitize_key(wp_unslash((string) $_POST['target_language']))) : ''; if ($target_language === $source_language || !in_array($target_language, $enabled, true)) { $target_language = ''; } foreach ($enabled as $language) { if ('' === $target_language && $language !== $source_language) { $target_language = $language; break; } } if (empty($api_key)) { return array( 'ok' => false, 'message' => $provider_label . ' API key is missing.', 'provider' => $provider_label, 'created_at' => current_time('mysql'), ); } if (empty($target_language)) { return array( 'ok' => false, 'message' => 'No target language is selected.', 'created_at' => current_time('mysql'), ); } $sample = 'Sample website text for translation.'; $hash = aitmt_fallback_translation_hash($sample); $translations = aitmt_fallback_translate_with_provider(array($hash => $sample), $source_language, $target_language); $duration = round(microtime(true) - $started, 3); if (is_wp_error($translations)) { $data = $translations->get_error_data(); return array( 'ok' => false, 'message' => $translations->get_error_message(), 'code' => $translations->get_error_code(), 'http_status' => is_array($data) && isset($data['status']) ? (int) $data['status'] : '', 'provider' => $provider_label, 'model' => $model, 'source_language' => $source_language, 'target_language' => $target_language, 'duration' => $duration, 'created_at' => current_time('mysql'), ); } return array( 'ok' => true, 'message' => $provider_label . ' test translation succeeded.', 'provider' => $provider_label, 'model' => $model, 'source_language' => $source_language, 'target_language' => $target_language, 'sample_source' => $sample, 'sample_translation' => isset($translations[$hash]) ? $translations[$hash] : '', 'duration' => $duration, 'created_at' => current_time('mysql'), ); } function aitmt_fallback_translation_table_exists() { global $wpdb; $table = aitmt_fallback_translation_table(); $found = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)); return $found === $table; } function aitmt_fallback_translation_count() { global $wpdb; if (!aitmt_fallback_translation_table_exists()) { return 0; } $table = aitmt_fallback_translation_table_sql(); if ('' === $table) { return 0; } return (int) $wpdb->get_var( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated by aitmt_fallback_translation_table_sql(); values are prepared below. $wpdb->prepare( 'SELECT COUNT(*) FROM ' . $table . ' WHERE status IN (%s, %s)', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Table name is controlled by aitmt_fallback_translation_table_sql(). 'published', 'manual' ) ); } function aitmt_fallback_mask_secret($secret) { $secret = (string) $secret; if ('' === $secret) { return 'missing'; } if (strlen($secret) <= 10) { return 'saved'; } return substr($secret, 0, 6) . '...' . substr($secret, -4); } function aitmt_fallback_render_debug_value($label, $value, $good = null) { $class = ''; if (true === $good) { $class = ' is-good'; } elseif (false === $good) { $class = ' is-bad'; } echo '' . esc_html($label) . '' . esc_html((string) $value) . ''; } function aitmt_fallback_settings_page() { if (!current_user_can('manage_options')) { return; } $options = get_option('aitmt_options', array()); if (!is_array($options)) { $options = array(); } $defaults = aitmt_fallback_default_options(); $options = wp_parse_args($options, $defaults); if (!is_array($options['enabled_languages'])) { $options['enabled_languages'] = array_filter(array_map('trim', explode(',', (string) $options['enabled_languages']))); } $languages = aitmt_fallback_languages(); $source_language = (string) $options['source_language']; $admin_mode = 'advanced' === (string) $options['admin_mode'] ? 'advanced' : 'basic'; $last_error = get_option('aitmt_last_error', ''); $debug_result = get_option('aitmt_debug_result', array()); $debug_result = is_array($debug_result) ? $debug_result : array(); $queue_result = get_option('aitmt_queue_result', array()); $queue_result = is_array($queue_result) ? $queue_result : array(); $cron_result = get_option('aitmt_cron_queue_result', array()); $cron_result = is_array($cron_result) ? $cron_result : array(); $auto_scan_result = get_option('aitmt_last_auto_scan', array()); $auto_scan_result = is_array($auto_scan_result) ? $auto_scan_result : array(); $scan_result = get_option('aitmt_scan_result', array()); $scan_result = is_array($scan_result) ? $scan_result : array(); $recent_translations = aitmt_fallback_recent_translations(20); $active_provider = aitmt_fallback_active_provider(); $api_key = aitmt_fallback_provider_key($active_provider); $api_key_source = aitmt_fallback_provider_key_source($active_provider); $cost_safety_issues = aitmt_fallback_cost_safety_issues($options); ?>

    [aitmt_language_switcher]

    ' . (isset($queue_result['target_language']) ? $queue_result['target_language'] : '')); ?>

    =')); aitmt_fallback_render_debug_value('DOMDocument', class_exists('DOMDocument') ? 'available' : 'missing', class_exists('DOMDocument')); aitmt_fallback_render_debug_value('WP HTTP API', function_exists('wp_remote_post') ? 'available' : 'missing', function_exists('wp_remote_post')); aitmt_fallback_render_debug_value('Source language', aitmt_fallback_source_language(), !empty(aitmt_fallback_source_language())); aitmt_fallback_render_debug_value('Enabled languages', implode(', ', aitmt_fallback_enabled_languages()), count(aitmt_fallback_enabled_languages()) > 1); aitmt_fallback_render_debug_value('Current language', aitmt_fallback_current_language(), true); aitmt_fallback_render_debug_value('Provider', aitmt_fallback_provider_label($active_provider), true); aitmt_fallback_render_debug_value('Provider key', aitmt_fallback_mask_secret($api_key) . ' (' . $api_key_source . ')', !empty($api_key)); aitmt_fallback_render_debug_value('Provider model', aitmt_fallback_provider_model($active_provider), true); aitmt_fallback_render_debug_value('Translation Mode', aitmt_fallback_translation_mode_label(), true); aitmt_fallback_render_debug_value('Quality mode', aitmt_fallback_quality_mode_label(), true); aitmt_fallback_render_debug_value('Provider cooldown', aitmt_fallback_provider_cooldown_remaining($active_provider) ? aitmt_fallback_provider_cooldown_remaining($active_provider) . ' seconds' : 'none', !aitmt_fallback_provider_cooldown_remaining($active_provider)); aitmt_fallback_render_debug_value('Provider daily characters', (string) aitmt_fallback_provider_chars_used($active_provider, gmdate('Ymd')), true); aitmt_fallback_render_debug_value('Provider monthly characters', (string) aitmt_fallback_provider_chars_used($active_provider, gmdate('Ym')), true); $provider_stats = aitmt_fallback_provider_stats_for($active_provider); aitmt_fallback_render_debug_value('Provider API requests', (string) absint($provider_stats['requests'] ?? 0), true); aitmt_fallback_render_debug_value('Estimated input tokens', (string) absint($provider_stats['input_tokens'] ?? 0), true); aitmt_fallback_render_debug_value('Estimated output tokens', (string) absint($provider_stats['output_tokens'] ?? 0), true); aitmt_fallback_render_debug_value('Estimated cost', number_format((float) ($provider_stats['estimated_cost'] ?? 0), 6), true); aitmt_fallback_render_debug_value('Translation memory cache hits', (string) absint($provider_stats['cache_hits'] ?? 0), true); aitmt_fallback_render_debug_value('Duplicate strings skipped', (string) absint($provider_stats['duplicate_skipped'] ?? 0), true); aitmt_fallback_render_debug_value('Translation table', aitmt_fallback_translation_table_exists() ? aitmt_fallback_translation_table() : 'missing', aitmt_fallback_translation_table_exists()); aitmt_fallback_render_debug_value('Queued strings', (string) aitmt_fallback_queued_count(), true); $queue_lock = aitmt_fallback_queue_lock_status(); aitmt_fallback_render_debug_value('Queue lock', !empty($queue_lock['active']) ? sprintf('active (%s, %d seconds)', $queue_lock['owner'], $queue_lock['remaining']) : 'inactive', empty($queue_lock['active'])); aitmt_fallback_render_debug_value('Queue lock TTL', (string) aitmt_fallback_queue_lock_ttl(), true); aitmt_fallback_render_debug_value('Processing TTL', (string) aitmt_fallback_processing_ttl(), true); aitmt_fallback_render_debug_value('Retry limit', (string) aitmt_fallback_retry_limit(), true); $request_log_summary = aitmt_fallback_provider_request_log_summary(); aitmt_fallback_render_debug_value('Logged provider chars today', (string) absint($request_log_summary['today'] ?? 0), true); aitmt_fallback_render_debug_value('Logged provider chars this month', (string) absint($request_log_summary['month'] ?? 0), true); aitmt_fallback_render_debug_value('Saved translations', (string) aitmt_fallback_translation_count(), true); aitmt_fallback_render_debug_value('Last error', $last_error ? $last_error : 'none', !$last_error); ?>

    →

    ' . (isset($debug_result['target_language']) ? $debug_result['target_language'] : '')); ?>

    s

    'disabled')); ?>