'; 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 (AITMT_Provider_Registry::ids() 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'), 'providerTestNonce' => wp_create_nonce('aitmt_provider_test'), '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'), 'testText' => __('Test connection', 'ait-multilingual-translate'), 'testingText' => __('Testing connection...', 'ait-multilingual-translate'), 'testFailedText' => __('Provider test failed.', '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(); $preset = isset($_POST['aitmt_apply_preset']) ? sanitize_key(wp_unslash((string) $_POST['aitmt_apply_preset'])) : ''; $presets = aitmt_fallback_safety_presets(); if (isset($presets[$preset])) { $input = array_merge($input, $presets[$preset]); } $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 (!AITMT_Provider_Registry::get($provider)) { 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_provider_test_options($raw_options, $provider) { $raw_options = is_array($raw_options) ? $raw_options : array(); $provider = sanitize_key((string) $provider); $stored = get_option('aitmt_options', array()); $stored = is_array($stored) ? $stored : array(); $schema = aitmt_fallback_provider_fields_schema(); $test_options = array('provider' => $provider); if (!isset($schema[$provider])) { return $test_options; } foreach ($schema[$provider]['fields'] as $field => $config) { $type = isset($config['type']) ? sanitize_key((string) $config['type']) : 'text'; if ('secret' === $type) { $test_options[$field] = aitmt_fallback_sanitize_secret($raw_options, $field, $stored); continue; } $value = isset($raw_options[$field]) ? sanitize_text_field((string) $raw_options[$field]) : ''; if ('select' === $type && !empty($config['choices']) && !array_key_exists($value, $config['choices'])) { $value = isset($stored[$field]) ? sanitize_text_field((string) $stored[$field]) : ''; } $test_options[$field] = $value; } if (isset($raw_options['translation_mode'])) { $mode = sanitize_key((string) $raw_options['translation_mode']); $test_options['translation_mode'] = array_key_exists($mode, aitmt_fallback_translation_mode_options()) ? $mode : 'neutral'; } if (isset($raw_options['custom_translation_instruction'])) { $instruction = sanitize_textarea_field((string) $raw_options['custom_translation_instruction']); $test_options['custom_translation_instruction'] = function_exists('mb_substr') ? mb_substr($instruction, 0, 500, 'UTF-8') : substr($instruction, 0, 500); } return $test_options; } function aitmt_fallback_ajax_test_provider_settings() { check_ajax_referer('aitmt_provider_test', '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'])) : ''; $adapter = AITMT_Provider_Registry::get($provider); if (!$adapter) { wp_send_json_error(array('message' => __('Invalid provider.', 'ait-multilingual-translate')), 400); } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each allowlisted provider field is sanitized below. $raw_options = isset($_POST['aitmt_options']) && is_array($_POST['aitmt_options']) ? wp_unslash($_POST['aitmt_options']) : array(); $temporary_options = aitmt_fallback_provider_test_options($raw_options, $provider); $source_language = isset($raw_options['source_language']) ? AITMT_Locales::normalize((string) $raw_options['source_language']) : ''; $source_language = $source_language ? $source_language : aitmt_fallback_source_language(); $target_language = isset($_POST['target_language']) ? AITMT_Locales::normalize(sanitize_text_field(wp_unslash((string) $_POST['target_language']))) : ''; if (!$target_language || $target_language === $source_language) { $target_language = 'en' === AITMT_Locales::base($source_language) ? 'de' : 'en'; } global $aitmt_provider_test_options; $aitmt_provider_test_options = $temporary_options; $tested_model = aitmt_fallback_provider_model($provider); $started = microtime(true); $result = $adapter->test_connection($source_language, $target_language); $duration = round(microtime(true) - $started, 3); $aitmt_provider_test_options = null; if (is_wp_error($result)) { $data = $result->get_error_data(); wp_send_json_error( array( 'message' => aitmt_fallback_redact_sensitive_text($result->get_error_message()), 'provider' => $adapter->get_name(), 'model' => $tested_model, 'httpStatus' => is_array($data) && isset($data['status']) ? absint($data['status']) : 0, 'duration' => $duration, ), 400 ); } $translation = isset($result['aitmt-provider-test']) ? trim((string) $result['aitmt-provider-test']) : ''; if ('' === $translation) { wp_send_json_error(array('message' => __('The provider returned no translation.', 'ait-multilingual-translate')), 502); } wp_send_json_success( array( 'message' => __('Connection test succeeded. The settings were tested without being saved.', 'ait-multilingual-translate'), 'provider' => $adapter->get_name(), 'model' => $tested_model, 'translation' => $translation, 'duration' => $duration, ) ); } 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_Runtime_Maintenance::activate(); 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'); wp_clear_scheduled_hook('aitmt_fallback_worker_event'); AITMT_Runtime_Maintenance::deactivate(); 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(); $default_language = aitmt_fallback_default_frontend_language(); if ('directory' === $options['url_mode'] && !empty($segments) && ($language !== $default_language || '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 !== $default_language || '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); $default_language = aitmt_fallback_default_frontend_language(); if (!$language || $language === $default_language || !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_sync_cron_schedule(); 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_secret($input, $field, $stored_options) { $field = sanitize_key((string) $field); if (!empty($input['clear_' . $field])) { return ''; } if (!array_key_exists($field, $input)) { return isset($stored_options[$field]) ? (string) $stored_options[$field] : ''; } $value = sanitize_text_field((string) $input[$field]); return '' === trim($value) && isset($stored_options[$field]) ? (string) $stored_options[$field] : $value; } function aitmt_fallback_sanitize($input) { $input = is_array($input) ? $input : array(); $stored_options = get_option('aitmt_options', array()); $stored_options = is_array($stored_options) ? $stored_options : 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 = AITMT_Locales::normalize((string) $language); if ($language) { $languages[] = $language; } } } $source_language = empty($input['source_language']) ? '' : AITMT_Locales::normalize((string) $input['source_language']); $source_language = 'auto' === strtolower((string) ($input['source_language'] ?? '')) ? '' : $source_language; $default_frontend_language = empty($input['default_frontend_language']) ? '' : AITMT_Locales::normalize((string) $input['default_frontend_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'; $interface_language = AITMT_Interface_Language::sanitize($input['interface_language'] ?? 'auto'); $allowed_providers = AITMT_Provider_Registry::ids(); $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)); $cron_interval = isset($input['cron_interval']) ? absint($input['cron_interval']) : 300; $cron_interval = in_array($cron_interval, array(60, 120, 300, 600, 900, 1800, 3600), true) ? $cron_interval : 300; $cron_max_batches = isset($input['cron_max_batches']) ? absint($input['cron_max_batches']) : 2; $cron_max_batches = min(10, max(1, $cron_max_batches)); $cron_max_runtime = isset($input['cron_max_runtime']) ? absint($input['cron_max_runtime']) : 20; $cron_max_runtime = min(120, max(5, $cron_max_runtime)); $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; $requests_per_minute = max(0, min(1000, absint($input['quota_requests_per_minute'] ?? 5))); $tokens_per_minute = max(0, min(10000000, absint($input['quota_tokens_per_minute'] ?? 10000))); $requests_per_day = max(0, min(1000000, absint($input['quota_requests_per_day'] ?? 100))); $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); $runtime_ttl_days = isset($input['runtime_cache_ttl_days']) ? absint($input['runtime_cache_ttl_days']) : 30; $runtime_ttl_days = min(365, max(1, $runtime_ttl_days)); $runtime_max_rows = isset($input['runtime_max_rows_per_language']) ? absint($input['runtime_max_rows_per_language']) : 5000; $runtime_max_rows = min(100000, max(100, $runtime_max_rows)); $runtime_min_length = isset($input['runtime_min_text_length']) ? absint($input['runtime_min_text_length']) : 3; $runtime_min_length = min(100, max(2, $runtime_min_length)); $runtime_max_length = isset($input['runtime_max_text_length']) ? absint($input['runtime_max_text_length']) : 800; $runtime_max_length = min(5000, max(100, $runtime_max_length)); $runtime_excluded_paths = empty($input['runtime_excluded_paths']) ? '' : sanitize_textarea_field((string) $input['runtime_excluded_paths']); $runtime_excluded_paths = function_exists('mb_substr') ? mb_substr($runtime_excluded_paths, 0, 4000, 'UTF-8') : substr($runtime_excluded_paths, 0, 4000); $runtime_excluded_selectors = empty($input['runtime_excluded_selectors']) ? '' : sanitize_textarea_field((string) $input['runtime_excluded_selectors']); $runtime_excluded_selectors = function_exists('mb_substr') ? mb_substr($runtime_excluded_selectors, 0, 2000, 'UTF-8') : substr($runtime_excluded_selectors, 0, 2000); $selector_validation = AITMT_Protected_Content::validate_selectors($runtime_excluded_selectors); $runtime_excluded_selectors = implode(', ', $selector_validation['valid']); update_option('aitmt_invalid_runtime_selectors', $selector_validation['invalid'], false); $runtime_excluded_source_types = empty($input['runtime_excluded_source_types']) ? '' : sanitize_textarea_field((string) $input['runtime_excluded_source_types']); $runtime_excluded_source_types = function_exists('mb_substr') ? mb_substr($runtime_excluded_source_types, 0, 1000, 'UTF-8') : substr($runtime_excluded_source_types, 0, 1000); return array( 'source_language' => $source_language, 'default_frontend_language' => $default_frontend_language, 'enabled_languages' => array_values(array_unique($languages)), 'admin_mode' => $admin_mode, 'interface_language' => $interface_language, 'provider' => $provider, 'openai_api_key' => aitmt_fallback_sanitize_secret($input, 'openai_api_key', $stored_options), 'openai_model' => empty($input['openai_model']) ? 'gpt-4o-mini' : sanitize_text_field((string) $input['openai_model']), 'gemini_api_key' => aitmt_fallback_sanitize_secret($input, 'gemini_api_key', $stored_options), 'gemini_model' => empty($input['gemini_model']) ? 'gemini-2.5-flash' : sanitize_text_field((string) $input['gemini_model']), 'grok_api_key' => aitmt_fallback_sanitize_secret($input, 'grok_api_key', $stored_options), 'grok_model' => empty($input['grok_model']) ? 'grok-3-mini' : sanitize_text_field((string) $input['grok_model']), 'google_translate_api_key' => aitmt_fallback_sanitize_secret($input, 'google_translate_api_key', $stored_options), 'deepl_api_key' => aitmt_fallback_sanitize_secret($input, 'deepl_api_key', $stored_options), 'deepl_plan' => $deepl_plan, 'claude_api_key' => aitmt_fallback_sanitize_secret($input, 'claude_api_key', $stored_options), 'claude_model' => empty($input['claude_model']) ? 'claude-haiku-4-5-20251001' : sanitize_text_field((string) $input['claude_model']), 'yandex_translate_api_key' => aitmt_fallback_sanitize_secret($input, 'yandex_translate_api_key', $stored_options), 'yandex_translate_folder_id' => empty($input['yandex_translate_folder_id']) ? '' : sanitize_text_field((string) $input['yandex_translate_folder_id']), 'yandexgpt_api_key' => aitmt_fallback_sanitize_secret($input, 'yandexgpt_api_key', $stored_options), 'yandexgpt_folder_id' => empty($input['yandexgpt_folder_id']) ? '' : sanitize_text_field((string) $input['yandexgpt_folder_id']), 'yandexgpt_model' => empty($input['yandexgpt_model']) ? 'yandexgpt-lite/latest' : sanitize_text_field((string) $input['yandexgpt_model']), 'mistral_api_key' => aitmt_fallback_sanitize_secret($input, 'mistral_api_key', $stored_options), 'mistral_model' => empty($input['mistral_model']) ? 'mistral-small-latest' : sanitize_text_field((string) $input['mistral_model']), 'deepseek_api_key' => aitmt_fallback_sanitize_secret($input, 'deepseek_api_key', $stored_options), 'deepseek_model' => empty($input['deepseek_model']) ? 'deepseek-v4-flash' : sanitize_text_field((string) $input['deepseek_model']), 'qwen_api_key' => aitmt_fallback_sanitize_secret($input, 'qwen_api_key', $stored_options), 'qwen_model' => empty($input['qwen_model']) ? 'qwen3.7-plus' : sanitize_text_field((string) $input['qwen_model']), 'qwen_region' => isset($input['qwen_region']) && in_array(sanitize_key((string) $input['qwen_region']), array('us', 'singapore', 'china'), true) ? sanitize_key((string) $input['qwen_region']) : 'us', 'baidu_api_key' => aitmt_fallback_sanitize_secret($input, 'baidu_api_key', $stored_options), 'baidu_model' => empty($input['baidu_model']) ? 'ernie-5.0' : sanitize_text_field((string) $input['baidu_model']), 'quota_daily_chars' => $daily_limit, 'quota_monthly_chars' => $monthly_limit, 'quota_requests_per_minute' => $requests_per_minute, 'quota_tokens_per_minute' => $tokens_per_minute, 'quota_requests_per_day' => $requests_per_day, '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', 'runtime_page_translation' => empty($input['runtime_page_translation']) ? '0' : '1', 'runtime_db_storage' => empty($input['runtime_db_storage']) ? '0' : '1', 'runtime_auto_cleanup' => empty($input['runtime_auto_cleanup']) ? '0' : '1', 'runtime_cache_ttl_days' => $runtime_ttl_days, 'runtime_max_rows_per_language' => $runtime_max_rows, 'runtime_min_text_length' => $runtime_min_length, 'runtime_max_text_length' => $runtime_max_length, 'runtime_excluded_paths' => $runtime_excluded_paths, 'runtime_excluded_selectors' => $runtime_excluded_selectors, 'runtime_excluded_source_types' => $runtime_excluded_source_types, 'runtime_translate_woocommerce_private' => empty($input['runtime_translate_woocommerce_private']) ? '0' : '1', 'scanner_sources' => AITMT_Scanner::sanitize(isset($input['scanner_sources']) && is_array($input['scanner_sources']) ? $input['scanner_sources'] : (isset($stored_options['scanner_sources']) && is_array($stored_options['scanner_sources']) ? $stored_options['scanner_sources'] : array())), 'scanner_exclusions' => AITMT_Scanner::sanitize_exclusions($input['scanner_exclusions'] ?? $stored_options['scanner_exclusions'] ?? array()), 'scanner_object_exclusions' => AITMT_Scanner::sanitize_exclusions($input['scanner_object_exclusions'] ?? $stored_options['scanner_object_exclusions'] ?? array(), true), 'scan_on_save' => empty($input['scan_on_save']) ? '0' : '1', 'cron_enabled' => empty($input['cron_enabled']) ? '0' : '1', 'cron_interval' => $cron_interval, 'cron_max_batches' => $cron_max_batches, 'cron_max_runtime' => $cron_max_runtime, '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() { $languages = 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', ); foreach (AITMT_Locales::regional_variants() as $locale => $metadata) { $languages[$locale] = $metadata['english_name']; } return $languages; } /** * Keep canonical URLs on the active language without changing the underlying * WordPress object URL. * * @param string $canonical Canonical URL. * @param WP_Post|null $post Optional post object from core. * @return string */ function aitmt_fallback_filter_canonical_url($canonical, $post = null) { unset($post); if (!is_string($canonical) || '' === $canonical || is_admin()) { return $canonical; } $language = aitmt_fallback_current_language(); if (!$language || !in_array($language, aitmt_fallback_enabled_languages(), true)) { return $canonical; } return aitmt_fallback_apply_language_to_url($canonical, $language); } /** * Render exact locale alternates and a default-language fallback. */ function aitmt_fallback_render_hreflang() { if (is_admin() || is_feed() || is_robots() || is_trackback() || aitmt_fallback_is_unsafe_page_load_request()) { return; } $seen = array(); foreach (aitmt_fallback_enabled_languages() as $language) { $language = aitmt_fallback_normalize_language($language); if (!$language || isset($seen[$language])) { continue; } $seen[$language] = true; printf( "\n", esc_attr($language), esc_url(aitmt_fallback_language_url($language)) ); } printf( "\n", esc_url(aitmt_fallback_language_url(aitmt_fallback_default_frontend_language())) ); } function aitmt_fallback_default_options() { return array( 'source_language' => '', 'default_frontend_language' => '', 'enabled_languages' => array(), 'admin_mode' => 'basic', 'interface_language' => 'auto', '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', 'claude_api_key' => '', 'claude_model' => 'claude-haiku-4-5-20251001', 'yandex_translate_api_key' => '', 'yandex_translate_folder_id' => '', 'yandexgpt_api_key' => '', 'yandexgpt_folder_id' => '', 'yandexgpt_model' => 'yandexgpt-lite/latest', 'mistral_api_key' => '', 'mistral_model' => 'mistral-small-latest', 'deepseek_api_key' => '', 'deepseek_model' => 'deepseek-v4-flash', 'qwen_api_key' => '', 'qwen_model' => 'qwen3.7-plus', 'qwen_region' => 'us', 'baidu_api_key' => '', 'baidu_model' => 'ernie-5.0', 'quota_daily_chars' => 5000, 'quota_monthly_chars' => 50000, 'quota_requests_per_minute' => 5, 'quota_tokens_per_minute' => 10000, 'quota_requests_per_day' => 100, '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', 'runtime_page_translation' => '0', 'runtime_db_storage' => '0', 'runtime_auto_cleanup' => '0', 'runtime_cache_ttl_days' => 30, 'runtime_max_rows_per_language' => 5000, 'runtime_min_text_length' => 3, 'runtime_max_text_length' => 800, 'runtime_excluded_paths' => "/wp-json/\n/feed/", 'runtime_excluded_selectors' => '.notranslate, [translate="no"], [data-aitmt-no-translate], [data-no-translate], [data-ai-translate-ignore]', 'runtime_excluded_source_types' => '', 'runtime_translate_woocommerce_private' => '0', 'scanner_sources' => AITMT_Scanner::defaults(), 'scanner_exclusions' => array(), 'scanner_object_exclusions' => array(), 'scan_on_save' => '0', 'cron_enabled' => '0', 'cron_interval' => 300, 'cron_max_batches' => 2, 'cron_max_runtime' => 20, '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', 'runtime_page_translation' => '0', 'runtime_db_storage' => '0', 'runtime_auto_cleanup' => '0', 'scan_on_save' => '0', 'cron_enabled' => '0', 'translate_on_page_load' => '0', 'draft_mode' => '0', 'translate_attributes' => '1', ); } function aitmt_fallback_safety_presets() { return array( 'safe_free' => array( 'quality_mode' => 'cheap', 'translation_temperature' => 0.1, 'quota_daily_chars' => 5000, 'quota_monthly_chars' => 50000, 'quota_requests_per_minute' => 5, 'quota_tokens_per_minute' => 10000, 'quota_requests_per_day' => 100, 'max_chars_per_request' => 2000, 'estimated_cost_limit' => 0.02, 'max_segments_per_request' => 5, 'cron_enabled' => '1', 'cron_interval' => 300, 'cron_max_batches' => 1, 'cron_max_runtime' => 10, 'translate_on_page_load' => '0', 'scan_on_save' => '0', ), 'recommended' => array( 'quality_mode' => 'balanced', 'translation_temperature' => 0.1, 'quota_daily_chars' => 25000, 'quota_monthly_chars' => 250000, 'quota_requests_per_minute' => 10, 'quota_tokens_per_minute' => 30000, 'quota_requests_per_day' => 500, 'max_chars_per_request' => 4000, 'estimated_cost_limit' => 0.10, 'max_segments_per_request' => 10, 'cron_enabled' => '1', 'cron_interval' => 300, 'cron_max_batches' => 2, 'cron_max_runtime' => 20, 'translate_on_page_load' => '0', 'scan_on_save' => '0', ), 'fast_bulk' => array( 'quality_mode' => 'balanced', 'translation_temperature' => 0.1, 'quota_daily_chars' => 250000, 'quota_monthly_chars' => 2000000, 'quota_requests_per_minute' => 30, 'quota_tokens_per_minute' => 120000, 'quota_requests_per_day' => 5000, 'max_chars_per_request' => 10000, 'estimated_cost_limit' => 1.00, 'max_segments_per_request' => 50, 'cron_enabled' => '1', 'cron_interval' => 60, 'cron_max_batches' => 5, 'cron_max_runtime' => 60, 'translate_on_page_load' => '0', 'scan_on_save' => '0', ), ); } function aitmt_fallback_options() { global $aitmt_provider_test_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($aitmt_provider_test_options) && !empty($aitmt_provider_test_options)) { $options = array_merge($options, $aitmt_provider_test_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 AITMT_Provider_Registry::get($provider) ? $provider : 'openai'; } function aitmt_fallback_provider_label($provider = '') { $provider = $provider ? sanitize_key($provider) : aitmt_fallback_active_provider(); $adapter = AITMT_Provider_Registry::get($provider); return $adapter ? $adapter->get_name() : 'OpenAI'; } function aitmt_fallback_provider_catalog() { $catalog = array(); foreach (AITMT_Provider_Registry::all() as $provider_id => $adapter) { $catalog[$provider_id] = array( 'label' => $adapter->get_name(), 'status' => 'active', 'badges' => $adapter->get_badges(), ); } return $catalog; } function aitmt_fallback_provider_fields_schema() { return array( 'openai' => array( 'fields' => array( 'openai_api_key' => array('type' => 'secret', 'label' => __('API key', 'ait-multilingual-translate')), 'openai_model' => array('type' => 'model', 'label' => __('Model', 'ait-multilingual-translate')), ), ), 'gemini' => array( 'fields' => array( 'gemini_api_key' => array('type' => 'secret', 'label' => __('API key', 'ait-multilingual-translate')), 'gemini_model' => array('type' => 'model', 'label' => __('Model', 'ait-multilingual-translate')), ), ), 'grok' => array( 'fields' => array( 'grok_api_key' => array('type' => 'secret', 'label' => __('API key', 'ait-multilingual-translate')), 'grok_model' => array('type' => 'model', 'label' => __('Model', 'ait-multilingual-translate')), ), ), 'google_translate' => array( 'fields' => array( 'google_translate_api_key' => array('type' => 'secret', 'label' => __('API key', 'ait-multilingual-translate')), ), ), 'deepl' => array( 'fields' => array( 'deepl_api_key' => array('type' => 'secret', 'label' => __('API key', 'ait-multilingual-translate')), 'deepl_plan' => array( 'type' => 'select', 'label' => __('Endpoint plan', 'ait-multilingual-translate'), 'choices' => array( 'free' => __('DeepL API Free', 'ait-multilingual-translate'), 'pro' => __('DeepL API Pro', 'ait-multilingual-translate'), ), ), ), ), 'claude' => array( 'fields' => array( 'claude_api_key' => array('type' => 'secret', 'label' => __('API key', 'ait-multilingual-translate')), 'claude_model' => array('type' => 'model', 'label' => __('Model', 'ait-multilingual-translate')), ), ), 'yandex_translate' => array( 'fields' => array( 'yandex_translate_api_key' => array('type' => 'secret', 'label' => __('API key', 'ait-multilingual-translate')), 'yandex_translate_folder_id' => array('type' => 'text', 'label' => __('Folder ID', 'ait-multilingual-translate')), ), ), 'yandexgpt' => array( 'fields' => array( 'yandexgpt_api_key' => array('type' => 'secret', 'label' => __('API key', 'ait-multilingual-translate')), 'yandexgpt_folder_id' => array('type' => 'text', 'label' => __('Folder ID', 'ait-multilingual-translate')), 'yandexgpt_model' => array('type' => 'model', 'label' => __('Model', 'ait-multilingual-translate')), ), ), 'mistral' => array( 'fields' => array( 'mistral_api_key' => array('type' => 'secret', 'label' => __('API key', 'ait-multilingual-translate')), 'mistral_model' => array('type' => 'model', 'label' => __('Model', 'ait-multilingual-translate')), ), ), 'deepseek' => array( 'fields' => array( 'deepseek_api_key' => array('type' => 'secret', 'label' => __('API key', 'ait-multilingual-translate')), 'deepseek_model' => array('type' => 'model', 'label' => __('Model', 'ait-multilingual-translate')), ), ), 'qwen' => array( 'fields' => array( 'qwen_api_key' => array('type' => 'secret', 'label' => __('API key', 'ait-multilingual-translate')), 'qwen_region' => array( 'type' => 'select', 'label' => __('API region', 'ait-multilingual-translate'), 'choices' => array( 'us' => __('United States', 'ait-multilingual-translate'), 'singapore' => __('Singapore / international', 'ait-multilingual-translate'), 'china' => __('China', 'ait-multilingual-translate'), ), ), 'qwen_model' => array('type' => 'model', 'label' => __('Model', 'ait-multilingual-translate')), ), ), 'baidu' => array( 'fields' => array( 'baidu_api_key' => array('type' => 'secret', 'label' => __('API key', 'ait-multilingual-translate')), 'baidu_model' => array('type' => 'model', 'label' => __('Model', 'ait-multilingual-translate')), ), ), ); } function aitmt_fallback_provider_field_provider($field) { foreach (aitmt_fallback_provider_fields_schema() as $provider => $schema) { if (isset($schema['fields'][$field])) { return $provider; } } return ''; } function aitmt_fallback_render_provider_fields($options, $compact = false) { $options = is_array($options) ? $options : aitmt_fallback_options(); $stored_options = get_option('aitmt_options', array()); $stored_options = is_array($stored_options) ? $stored_options : array(); $target_languages = aitmt_fallback_enabled_languages(); $source_language = aitmt_fallback_source_language(); $target_language = ''; foreach ($target_languages as $candidate) { if (AITMT_Locales::normalize($candidate) !== $source_language) { $target_language = AITMT_Locales::normalize($candidate); break; } } if (!$target_language) { $target_language = 'en' === AITMT_Locales::base($source_language) ? 'de' : 'en'; } ?>
$schema) : ?> get_available_models(); ?>
> get_name()); ?>
$config) : ?>

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'), ), ), 'claude' => array( 'badge' => 'usage', 'badge_text' => __('Usage Based', 'ait-multilingual-translate'), 'summary' => __('Anthropic API usage is billed by your Anthropic account. Start with a Haiku model and a small workspace spending limit.', 'ait-multilingual-translate'), 'recommendations' => array( __('Use the configured Haiku model for initial tests', 'ait-multilingual-translate'), __('Set a workspace spending limit before bulk translation', 'ait-multilingual-translate'), __('Keep batches small until provider limits are confirmed', 'ait-multilingual-translate'), ), 'links' => array( 'docs' => array('label' => __('Documentation', 'ait-multilingual-translate'), 'url' => 'https://platform.claude.com/docs/en/api/overview'), 'pricing' => array('label' => __('Pricing', 'ait-multilingual-translate'), 'url' => 'https://www.anthropic.com/pricing#api'), 'quota' => array('label' => __('Quota settings', 'ait-multilingual-translate'), 'url' => 'https://platform.claude.com/settings/limits'), 'budget' => array('label' => __('Budget settings', 'ait-multilingual-translate'), 'url' => 'https://platform.claude.com/settings/limits'), ), ), 'yandex_translate' => array( 'badge' => 'usage', 'badge_text' => __('Usage Based', 'ait-multilingual-translate'), 'summary' => __('Yandex Cloud Translate usage depends on your Yandex Cloud billing account and quotas.', 'ait-multilingual-translate'), 'recommendations' => array( __('Use a dedicated service account and folder', 'ait-multilingual-translate'), __('Set provider-side quotas before processing a large queue', 'ait-multilingual-translate'), __('Test with one small manual batch first', 'ait-multilingual-translate'), ), 'links' => array( 'docs' => array('label' => __('Documentation', 'ait-multilingual-translate'), 'url' => 'https://yandex.cloud/en/docs/translate/'), 'pricing' => array('label' => __('Pricing', 'ait-multilingual-translate'), 'url' => 'https://yandex.cloud/en/docs/translate/pricing'), 'quota' => array('label' => __('Quota settings', 'ait-multilingual-translate'), 'url' => 'https://yandex.cloud/en/docs/translate/concepts/limits'), 'budget' => array('label' => __('Budget settings', 'ait-multilingual-translate'), 'url' => 'https://yandex.cloud/en/docs/billing/'), ), ), 'yandexgpt' => array( 'badge' => 'usage', 'badge_text' => __('Usage Based', 'ait-multilingual-translate'), 'summary' => __('YandexGPT usage depends on your Yandex Cloud Foundation Models billing account and quotas.', 'ait-multilingual-translate'), 'recommendations' => array( __('Use the lite model while testing', 'ait-multilingual-translate'), __('Set provider-side quotas and billing notifications', 'ait-multilingual-translate'), __('Use Translation Memory and small queue batches', 'ait-multilingual-translate'), ), 'links' => array( 'docs' => array('label' => __('Documentation', 'ait-multilingual-translate'), 'url' => 'https://yandex.cloud/en/docs/foundation-models/'), 'pricing' => array('label' => __('Pricing', 'ait-multilingual-translate'), 'url' => 'https://yandex.cloud/en/docs/foundation-models/pricing'), 'quota' => array('label' => __('Quota settings', 'ait-multilingual-translate'), 'url' => 'https://yandex.cloud/en/docs/foundation-models/concepts/limits'), 'budget' => array('label' => __('Budget settings', 'ait-multilingual-translate'), 'url' => 'https://yandex.cloud/en/docs/billing/'), ), ), 'mistral' => array( 'badge' => 'usage', 'badge_text' => __('Usage Based', 'ait-multilingual-translate'), 'summary' => __('Mistral API usage is billed according to the model and account plan selected in La Plateforme.', 'ait-multilingual-translate'), 'recommendations' => array( __('Use mistral-small-latest for initial translation tests', 'ait-multilingual-translate'), __('Configure provider-side usage controls before bulk translation', 'ait-multilingual-translate'), __('Keep queue batches small until limits are confirmed', 'ait-multilingual-translate'), ), 'links' => array( 'docs' => array('label' => __('Documentation', 'ait-multilingual-translate'), 'url' => 'https://docs.mistral.ai/api/'), 'pricing' => array('label' => __('Pricing', 'ait-multilingual-translate'), 'url' => 'https://mistral.ai/pricing'), 'quota' => array('label' => __('Quota settings', 'ait-multilingual-translate'), 'url' => 'https://console.mistral.ai/'), 'budget' => array('label' => __('Budget settings', 'ait-multilingual-translate'), 'url' => 'https://console.mistral.ai/'), ), ), 'deepseek' => array( 'badge' => 'usage', 'badge_text' => __('Usage Based', 'ait-multilingual-translate'), 'summary' => __('DeepSeek API usage is charged to your DeepSeek platform balance according to the selected model.', 'ait-multilingual-translate'), 'recommendations' => array( __('Use the Flash model for initial tests', 'ait-multilingual-translate'), __('Add only a small prepaid balance while testing', 'ait-multilingual-translate'), __('Process a small manual queue batch before Translate All', 'ait-multilingual-translate'), ), 'links' => array( 'docs' => array('label' => __('Documentation', 'ait-multilingual-translate'), 'url' => 'https://api-docs.deepseek.com/api/deepseek-api/'), 'pricing' => array('label' => __('Pricing', 'ait-multilingual-translate'), 'url' => 'https://api-docs.deepseek.com/quick_start/pricing'), 'quota' => array('label' => __('Quota settings', 'ait-multilingual-translate'), 'url' => 'https://platform.deepseek.com/'), 'budget' => array('label' => __('Budget settings', 'ait-multilingual-translate'), 'url' => 'https://platform.deepseek.com/usage'), ), ), 'qwen' => array( 'badge' => 'usage', 'badge_text' => __('Usage Based', 'ait-multilingual-translate'), 'summary' => __('Alibaba Cloud Model Studio usage and availability depend on the selected region, model, and account plan.', 'ait-multilingual-translate'), 'recommendations' => array( __('Choose the correct Model Studio region before creating a key', 'ait-multilingual-translate'), __('Use the Flash model for initial tests where available', 'ait-multilingual-translate'), __('Configure billing alerts in Alibaba Cloud before bulk translation', 'ait-multilingual-translate'), ), 'links' => array( 'docs' => array('label' => __('Documentation', 'ait-multilingual-translate'), 'url' => 'https://www.alibabacloud.com/help/en/model-studio/qwen-api-reference'), 'pricing' => array('label' => __('Pricing', 'ait-multilingual-translate'), 'url' => 'https://www.alibabacloud.com/help/en/model-studio/model-pricing'), 'quota' => array('label' => __('Quota settings', 'ait-multilingual-translate'), 'url' => 'https://modelstudio.console.alibabacloud.com/'), 'budget' => array('label' => __('Budget settings', 'ait-multilingual-translate'), 'url' => 'https://billing-cost.console.aliyun.com/'), ), ), 'baidu' => array( 'badge' => 'usage', 'badge_text' => __('Usage Based', 'ait-multilingual-translate'), 'summary' => __('Baidu AI Cloud Qianfan usage depends on the selected ERNIE model and your cloud account billing configuration.', 'ait-multilingual-translate'), 'recommendations' => array( __('Confirm Qianfan access and regional availability first', 'ait-multilingual-translate'), __('Configure cloud account quotas or alerts before bulk translation', 'ait-multilingual-translate'), __('Run a single provider test before queue processing', 'ait-multilingual-translate'), ), 'links' => array( 'docs' => array('label' => __('Documentation', 'ait-multilingual-translate'), 'url' => 'https://intl.cloud.baidu.com/en/doc/qianfan/s/3m7of64lb-intl-en'), 'pricing' => array('label' => __('Pricing', 'ait-multilingual-translate'), 'url' => 'https://intl.cloud.baidu.com/product/wenxinworkshop'), 'quota' => array('label' => __('Quota settings', 'ait-multilingual-translate'), 'url' => 'https://console.bce.baidu.com/qianfan/'), 'budget' => array('label' => __('Budget settings', 'ait-multilingual-translate'), 'url' => 'https://console.bce.baidu.com/finance/'), ), ), ); } 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')); ?>
supports_tone() : false; } function aitmt_fallback_provider_key($provider = '') { $options = aitmt_fallback_options(); $provider = $provider ? sanitize_key($provider) : aitmt_fallback_active_provider(); $map = array( 'openai' => array('AITMT_OPENAI_API_KEY', 'openai_api_key'), 'gemini' => array('AITMT_GEMINI_API_KEY', 'gemini_api_key'), 'grok' => array('AITMT_GROK_API_KEY', 'grok_api_key'), 'google_translate' => array('AITMT_GOOGLE_TRANSLATE_API_KEY', 'google_translate_api_key'), 'deepl' => array('AITMT_DEEPL_API_KEY', 'deepl_api_key'), 'claude' => array('AITMT_CLAUDE_API_KEY', 'claude_api_key'), 'yandex_translate' => array('AITMT_YANDEX_TRANSLATE_API_KEY', 'yandex_translate_api_key'), 'yandexgpt' => array('AITMT_YANDEXGPT_API_KEY', 'yandexgpt_api_key'), 'mistral' => array('AITMT_MISTRAL_API_KEY', 'mistral_api_key'), 'deepseek' => array('AITMT_DEEPSEEK_API_KEY', 'deepseek_api_key'), 'qwen' => array('AITMT_QWEN_API_KEY', 'qwen_api_key'), 'baidu' => array('AITMT_BAIDU_API_KEY', 'baidu_api_key'), ); if (!isset($map[$provider])) { return ''; } list($constant, $option_key) = $map[$provider]; if (defined($constant) && constant($constant)) { return (string) constant($constant); } $environment = getenv($constant); if (false !== $environment && '' !== trim((string) $environment)) { return sanitize_text_field((string) $environment); } return isset($options[$option_key]) ? (string) $options[$option_key] : ''; } function aitmt_fallback_provider_key_source($provider = '') { $provider = $provider ? sanitize_key($provider) : aitmt_fallback_active_provider(); $constant = 'AITMT_' . strtoupper($provider) . '_API_KEY'; $constant = 'GOOGLE_TRANSLATE' === strtoupper($provider) ? 'AITMT_GOOGLE_TRANSLATE_API_KEY' : $constant; if (defined($constant) && constant($constant)) { return 'wp-config.php constant'; } $environment = getenv($constant); return false !== $environment && '' !== trim((string) $environment) ? 'environment variable' : 'plugin settings'; } function aitmt_fallback_provider_setting($provider, $setting, $default = '') { $provider = sanitize_key((string) $provider); $setting = sanitize_key((string) $setting); $options = aitmt_fallback_options(); $key = $provider . '_' . $setting; $constant = 'AITMT_' . strtoupper($provider . '_' . $setting); if (defined($constant) && '' !== trim((string) constant($constant))) { return (string) constant($constant); } $environment = getenv($constant); if (false !== $environment && '' !== trim((string) $environment)) { return sanitize_text_field((string) $environment); } return isset($options[$key]) && '' !== trim((string) $options[$key]) ? (string) $options[$key] : $default; } function aitmt_fallback_provider_model($provider = '') { $options = aitmt_fallback_options(); $provider = $provider ? sanitize_key($provider) : aitmt_fallback_active_provider(); if ('google_translate' === $provider) { return 'Cloud Translation Basic v2'; } if ('deepl' === $provider) { return 'DeepL API ' . ('pro' === $options['deepl_plan'] ? 'api.deepl.com' : 'api-free.deepl.com'); } $adapter = AITMT_Provider_Registry::get($provider); $default = $adapter ? $adapter->get_default_model() : 'gpt-4o-mini'; $model_key = $provider . '_model'; return empty($options[$model_key]) ? $default : sanitize_text_field((string) $options[$model_key]); } function aitmt_fallback_provider_timeout($provider = '', $default = 60) { $provider = $provider ? sanitize_key((string) $provider) : aitmt_fallback_active_provider(); $adapter = AITMT_Provider_Registry::get($provider); return $adapter ? $adapter->get_timeout() : max(5, absint($default)); } function aitmt_fallback_provider_batch_limit($provider = '', $configured_limit = 40) { $provider = $provider ? sanitize_key((string) $provider) : aitmt_fallback_active_provider(); $adapter = AITMT_Provider_Registry::get($provider); $configured_limit = max(1, min(100, absint($configured_limit))); return $adapter ? min($configured_limit, $adapter->get_max_batch_size()) : $configured_limit; } function aitmt_fallback_quality_mode_options() { return array( 'cheap' => __('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', ), 'claude' => array( 'cheap' => 'claude-haiku-4-5-20251001', 'balanced' => 'claude-haiku-4-5-20251001', 'premium' => 'claude-haiku-4-5-20251001', ), 'mistral' => array( 'cheap' => 'mistral-small-latest', 'balanced' => 'mistral-small-latest', 'premium' => 'mistral-small-latest', ), 'deepseek' => array( 'cheap' => 'deepseek-v4-flash', 'balanced' => 'deepseek-v4-flash', 'premium' => 'deepseek-v4-pro', ), 'qwen' => array( 'cheap' => 'qwen3.6-flash', 'balanced' => 'qwen3.7-plus', 'premium' => 'qwen3.7-max', ), 'baidu' => array( 'cheap' => 'ernie-5.0', 'balanced' => 'ernie-5.0', 'premium' => 'ernie-5.0', ), 'yandexgpt' => array( 'cheap' => 'yandexgpt-lite/latest', 'balanced' => 'yandexgpt/latest', 'premium' => 'yandexgpt/latest', ), ); 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()) { $target_info = AITMT_Locales::resolve($target_language, aitmt_fallback_languages()); if ('1' !== ($target_info['known'] ?? '0')) { aitmt_fallback_log_debug_event('locale', 'Blocked provider request for an unknown target locale.', array( 'target_language' => sanitize_text_field((string) $target_language), )); return new WP_Error('aitmt_unknown_target_language', __('The target language is not recognized. Select a supported language before translating.', 'ait-multilingual-translate')); } $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; } $adapter = AITMT_Provider_Registry::get($provider); $result = $adapter ? $adapter->translate_batch($segments_for_provider, $source_language, $target_language) : new WP_Error('aitmt_provider_unavailable', 'The selected translation provider is not available.'); if (!is_wp_error($result)) { foreach ((array) $result as $source_hash => $translated_text) { if (!AITMT_Locales::matches_target_script($translated_text, $target_language)) { $result = new WP_Error( 'aitmt_wrong_target_language', sprintf( /* translators: %s: requested target locale. */ __('The provider response does not appear to use the requested target language (%s). The translation was not saved.', 'ait-multilingual-translate'), $target_info['api_locale'] ) ); break; } } } 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; $requests_per_minute = absint($options['quota_requests_per_minute'] ?? 0); $tokens_per_minute = absint($options['quota_tokens_per_minute'] ?? 0); $requests_per_day = absint($options['quota_requests_per_day'] ?? 0); $cost_limit = isset($options['estimated_cost_limit']) ? (float) $options['estimated_cost_limit'] : 0.0; $day = gmdate('Ymd'); $month = gmdate('Ym'); $now_timestamp = current_time('timestamp', true); $minute_start = $now_timestamp - MINUTE_IN_SECONDS; $day_start = strtotime(gmdate('Y-m-d 00:00:00', $now_timestamp) . ' UTC'); $recent_requests = 0; $recent_tokens = 0; $today_requests = 0; foreach (aitmt_fallback_provider_request_log_entries() as $entry) { if (sanitize_key((string) ($entry['provider'] ?? '')) !== sanitize_key((string) $provider) || 'blocked' === sanitize_key((string) ($entry['status'] ?? ''))) { continue; } $timestamp = strtotime((string) ($entry['created_at'] ?? '') . ' UTC'); if (!$timestamp) { continue; } if ($timestamp >= $minute_start) { $recent_requests++; $recent_tokens += absint($entry['tokens'] ?? ceil(absint($entry['characters'] ?? 0) / 4)); } if ($timestamp >= $day_start) { $today_requests++; } } $request_tokens = aitmt_fallback_estimate_tokens_for_segments($segments); if ($requests_per_minute > 0 && $recent_requests + 1 > $requests_per_minute) { return new WP_Error('aitmt_requests_per_minute', sprintf('Local requests-per-minute limit reached for %s.', aitmt_fallback_provider_label($provider)), array('status' => 429)); } if ($tokens_per_minute > 0 && $recent_tokens + $request_tokens > $tokens_per_minute) { return new WP_Error('aitmt_tokens_per_minute', sprintf('Local tokens-per-minute limit reached for %s.', aitmt_fallback_provider_label($provider)), array('status' => 429)); } if ($requests_per_day > 0 && $today_requests + 1 > $requests_per_day) { return new WP_Error('aitmt_requests_per_day', sprintf('Local requests-per-day limit reached for %s.', aitmt_fallback_provider_label($provider)), array('status' => 429)); } 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', true), '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), 'tokens' => aitmt_fallback_estimate_tokens_for_segments((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) { return AITMT_Locales::normalize($language); } /** * Explain rejected exclusion selectors without allowing them to affect pages. */ function aitmt_fallback_invalid_selector_notice() { if (!current_user_can('manage_options')) { return; } $invalid = get_option('aitmt_invalid_runtime_selectors', array()); if (empty($invalid) || !is_array($invalid)) { return; } $items = implode(', ', array_map('sanitize_text_field', $invalid)); ?>

$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 ( is_user_logged_in() && current_user_can('edit_posts') && !aitmt_fallback_frontend_editor_enabled() ) { 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; } $request_method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper(sanitize_text_field(wp_unslash((string) $_SERVER['REQUEST_METHOD']))) : 'GET'; if (!in_array($request_method, array('GET', 'HEAD'), true)) { return true; } if (function_exists('is_customize_preview') && is_customize_preview()) { return true; } $options = aitmt_fallback_options(); if ( aitmt_fallback_is_woocommerce_private_request() && '1' !== (string) ($options['runtime_translate_woocommerce_private'] ?? '0') ) { 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=', 'nonce=', '_wpnonce=', 'security=', 'add-to-cart=', 'remove_item=', 'removed_item=', 'wc-ajax=', 'rest_route=', 'doing_wp_cron=', 'orderby=', 'filter_', 'min_price=', 'max_price=', 'rating_filter=', '/wp-json', '/feed', '/sitemap', 'sitemap.xml', '.xml', '/xmlrpc.php', '/wp-login.php', ); foreach ($blocked_needles as $needle) { if (false !== strpos($uri, $needle)) { AITMT_Runtime_Maintenance::increment_counter('unsafe_request_skipped'); return true; } } $excluded_paths = preg_split('/\r\n|\r|\n/', (string) ($options['runtime_excluded_paths'] ?? '')); $request_path = strtolower((string) wp_parse_url($request_uri, PHP_URL_PATH)); foreach ((array) $excluded_paths as $excluded_path) { $excluded_path = strtolower(trim((string) $excluded_path)); if ('' !== $excluded_path && false !== strpos($request_path, $excluded_path)) { AITMT_Runtime_Maintenance::increment_counter('excluded_path_skipped'); return true; } } return false; } /** * Identify WooCommerce pages that can expose cart, order, or account data. */ function aitmt_fallback_is_woocommerce_private_request() { if ( (function_exists('is_cart') && is_cart()) || (function_exists('is_checkout') && is_checkout()) || (function_exists('is_account_page') && is_account_page()) ) { return true; } if (function_exists('is_wc_endpoint_url')) { foreach (array( 'order-pay', 'order-received', 'view-order', 'downloads', 'edit-account', 'edit-address', 'payment-methods', 'lost-password', 'customer-logout', ) as $endpoint) { if (is_wc_endpoint_url($endpoint)) { return true; } } } $request_uri = isset($_SERVER['REQUEST_URI']) ? strtolower(sanitize_text_field(wp_unslash((string) $_SERVER['REQUEST_URI']))) : ''; return false !== strpos($request_uri, '/cart') || false !== strpos($request_uri, '/checkout') || false !== strpos($request_uri, '/my-account') || false !== strpos($request_uri, 'wc-ajax=') || false !== strpos($request_uri, 'add-to-cart=') || false !== strpos($request_uri, 'remove_item=') || false !== strpos($request_uri, 'removed_item='); } function aitmt_fallback_translate_html($html) { if ('' === trim((string) $html) || !class_exists('DOMDocument')) { return $html; } $protected = AITMT_Protected_Content::protect($html); if (empty($protected['valid'])) { return $html; } $protected_count = AITMT_Protected_Content::count($protected); if ($protected_count > 0) { AITMT_Runtime_Maintenance::increment_counter('protected_elements', $protected_count); } $protected_html = (string) $protected['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('' . $protected_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 AITMT_Protected_Content::restore($dom->saveHTML(), $protected); } $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 AITMT_Protected_Content::restore($dom->saveHTML(), $protected); } 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 AITMT_Protected_Content::restore($dom->saveHTML(), $protected); } 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; } // Keep form actions and data-* attributes byte-for-byte intact. Language // navigation belongs only on ordinary links; technical URLs are often // consumed by JavaScript, checkout flows, or page-builder runtimes. $url_attributes = array('href'); 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); if (class_exists('AITMT_Protected_Content') && AITMT_Protected_Content::is_placeholder($url)) { continue; } $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', 'kbd', 'samp', 'textarea', 'svg', 'canvas', 'iframe', 'object', 'template'); if (in_array($tag, $skip_tags, true) || aitmt_fallback_runtime_node_excluded($node)) { 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-no-translate') || $node->hasAttribute('data-ai-translate-ignore') || $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'); 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_runtime_node_excluded($node) { if (!$node instanceof DOMElement) { return false; } $options = aitmt_fallback_options(); $configured = preg_split('/\r\n|\r|\n|,/', (string) ($options['runtime_excluded_selectors'] ?? '')); $excluded_classes = array( 'screen-reader-text', 'elementor-screen-only', 'woocommerce-notices-wrapper', 'woocommerce-message', 'woocommerce-error', 'woocommerce-info', 'cart_totals', 'mini_cart_item', 'widget_shopping_cart', ); $current = $node; while ($current instanceof DOMElement) { $tag = strtolower($current->tagName); $id = (string) $current->getAttribute('id'); $class = ' ' . preg_replace('/\s+/', ' ', (string) $current->getAttribute('class')) . ' '; $style = strtolower((string) $current->getAttribute('style')); if ( $current->hasAttribute('hidden') || $current->hasAttribute('inert') || $current->hasAttribute('data-no-translate') || $current->hasAttribute('data-ai-translate-ignore') || $current->hasAttribute('data-aitmt-no-translate') || ('no' === strtolower((string) $current->getAttribute('translate'))) || ('true' === strtolower((string) $current->getAttribute('aria-hidden'))) || ('input' === $tag && 'hidden' === strtolower((string) $current->getAttribute('type'))) || false !== strpos($style, 'display:none') || false !== strpos($style, 'display: none') || false !== strpos($style, 'visibility:hidden') || false !== strpos($style, 'visibility: hidden') ) { AITMT_Runtime_Maintenance::increment_counter('hidden_node_skipped'); return true; } foreach ($excluded_classes as $excluded_class) { if (false !== strpos($class, ' ' . $excluded_class . ' ')) { AITMT_Runtime_Maintenance::increment_counter('dynamic_node_skipped'); return true; } } foreach ((array) $configured as $selector) { $selector = trim((string) $selector); if ('' === $selector) { continue; } if ('.' === substr($selector, 0, 1) && false !== strpos($class, ' ' . substr($selector, 1) . ' ')) { return true; } if ('#' === substr($selector, 0, 1) && $id === substr($selector, 1)) { return true; } if (preg_match('/^\[([a-z0-9_-]+)\]$/i', $selector, $matches) && $current->hasAttribute($matches[1])) { return true; } if (preg_match('/^\[([a-z0-9_:.-]+)\s*=\s*(?:"([^"]*)"|\'([^\']*)\')\]$/i', $selector, $matches)) { $expected = isset($matches[2]) && '' !== $matches[2] ? $matches[2] : (isset($matches[3]) ? $matches[3] : ''); if ($current->hasAttribute($matches[1]) && $expected === $current->getAttribute($matches[1])) { return true; } } if (preg_match('/^[a-z][a-z0-9_-]*$/i', $selector) && $tag === strtolower($selector)) { return true; } } $current = $current->parentNode; } return false; } function aitmt_fallback_runtime_lock_segments($segments, $source_language, $target_language) { $locked = array(); $tokens = array(); $ttl = max(30, min(300, aitmt_fallback_queue_lock_ttl())); foreach ($segments as $hash => $text) { $token = 'aitmt_runtime_lock_' . md5($hash . '|' . $source_language . '|' . $target_language . '|html'); $created_at = (int) get_option($token, 0); if ($created_at > 0 && (time() - $created_at) > $ttl) { delete_option($token); } if (add_option($token, time(), '', false)) { $locked[$hash] = $text; $tokens[] = $token; } else { AITMT_Runtime_Maintenance::increment_counter('runtime_lock_skipped'); } } return array($locked, $tokens); } function aitmt_fallback_runtime_unlock_segments($tokens) { foreach ((array) $tokens as $token) { delete_option(sanitize_key((string) $token)); } } function aitmt_fallback_cache_configuration_signature($target_language = '') { $options = aitmt_fallback_options(); $provider = aitmt_fallback_active_provider(); $target_language = '' !== (string) $target_language ? AITMT_Locales::normalize((string) $target_language) : AITMT_Locales::normalize(aitmt_fallback_default_frontend_language()); $signature = array( 'source_locale' => AITMT_Locales::normalize(aitmt_fallback_source_language()), 'target_locale' => $target_language, 'provider' => $provider, 'model' => aitmt_fallback_provider_model($provider), 'translation_mode' => sanitize_key((string) ($options['translation_mode'] ?? 'neutral')), 'pipeline_version' => AITMT_PIPELINE_VERSION, 'prompt_version' => AITMT_PROMPT_VERSION, 'protected_content_version' => AITMT_Protected_Content::VERSION, 'excluded_selectors' => (string) ($options['runtime_excluded_selectors'] ?? ''), 'excluded_paths' => (string) ($options['runtime_excluded_paths'] ?? ''), ); return hash('sha256', (string) wp_json_encode($signature)); } function aitmt_fallback_runtime_content_signature($segments, $target_language) { $request_uri = isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash((string) $_SERVER['REQUEST_URI'])) : ''; $path = (string) wp_parse_url($request_uri, PHP_URL_PATH); $target_language = AITMT_Locales::normalize((string) $target_language); $content_hashes = array(); foreach ((array) $segments as $hash => $text) { $content_hashes[] = (string) $hash . ':' . hash('sha256', (string) $text); } sort($content_hashes, SORT_STRING); $signature = array( 'path' => $path, 'configuration_signature' => aitmt_fallback_cache_configuration_signature($target_language), 'content_hashes' => $content_hashes, ); return 'aitmt_runtime_seen_' . md5(wp_json_encode($signature)); } function aitmt_fallback_translate_segments($segments, $source_language, $target_language) { $clean_segments = array(); foreach ($segments as $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[aitmt_fallback_translation_hash($text)] = $text; } } if (empty($clean_segments)) { return array(); } $existing = aitmt_fallback_get_existing_translations($clean_segments, $source_language, $target_language, true, false); $known = aitmt_fallback_get_existing_translations($clean_segments, $source_language, $target_language, true, true); if (!empty($known)) { aitmt_fallback_provider_stats_record_cache_hits(aitmt_fallback_active_provider(), count($known)); } $missing = array_diff_key($clean_segments, $known); if (empty($missing)) { return $existing; } AITMT_Runtime_Maintenance::increment_counter('translation_cache_misses', count($missing)); $options = aitmt_fallback_options(); $private_woocommerce_request = aitmt_fallback_is_woocommerce_private_request(); $limit = isset($options['max_segments_per_request']) ? absint($options['max_segments_per_request']) : 40; $limit = aitmt_fallback_provider_batch_limit('', $limit); $missing = array_slice($missing, 0, $limit, true); if ( '1' === $options['queue_missing'] && '1' === (string) ($options['runtime_db_storage'] ?? '0') && !$private_woocommerce_request && !aitmt_fallback_is_unsafe_page_load_request() ) { $signature = aitmt_fallback_runtime_content_signature($missing, $target_language); if (false === get_transient($signature)) { aitmt_fallback_enqueue_translation_batch($missing, $source_language, $target_language, 'page_load'); set_transient($signature, 1, 15 * MINUTE_IN_SECONDS); } else { AITMT_Runtime_Maintenance::increment_counter('unchanged_page_skipped'); } } if ( '1' !== $options['translate_on_page_load'] || '1' !== (string) ($options['runtime_page_translation'] ?? '0') || '1' !== (string) ($options['runtime_db_storage'] ?? '0') || $private_woocommerce_request ) { return $existing; } list($locked_missing, $lock_tokens) = aitmt_fallback_runtime_lock_segments($missing, $source_language, $target_language); if (empty($locked_missing)) { return $existing; } try { $after_lock = aitmt_fallback_get_existing_translations($locked_missing, $source_language, $target_language, true, false); $after_lock_known = aitmt_fallback_get_existing_translations($locked_missing, $source_language, $target_language, true, true); if (!empty($after_lock_known)) { $existing = array_merge($existing, $after_lock); $locked_missing = array_diff_key($locked_missing, $after_lock_known); } if (empty($locked_missing)) { return $existing; } $translated = aitmt_fallback_translate_with_provider( $locked_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($locked_missing, $translated, $source_language, $target_language, $status, 'page_load'); 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()); } } finally { aitmt_fallback_runtime_unlock_segments($lock_tokens); } 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( '%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.', AITMT_Locales::translation_instruction($source_language, $target_language, $languages), $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' => aitmt_fallback_provider_timeout('openai', 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( "%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", AITMT_Locales::translation_instruction($source_language, $target_language, $languages), $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' => aitmt_fallback_provider_timeout('gemini', 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 = 'https://translation.googleapis.com/language/translate/v2'; $response = wp_remote_post( $endpoint, array( 'timeout' => aitmt_fallback_provider_timeout('google_translate', 45), 'headers' => array( 'Content-Type' => 'application/json', 'X-Goog-Api-Key' => $api_key, 'Referer' => home_url('/'), ), 'body' => wp_json_encode(array( 'q' => $texts, 'source' => AITMT_Locales::provider_code($source_language, 'google_translate', true), 'target' => AITMT_Locales::provider_code($target_language, 'google_translate'), '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' => aitmt_fallback_provider_timeout('deepl', 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) { return AITMT_Locales::provider_code($language, 'deepl', $is_source); } function aitmt_fallback_grok_translate_batch($segments, $source_language, $target_language) { return aitmt_fallback_openai_compatible_translate_batch('grok', 'https://api.x.ai/v1/chat/completions', $segments, $source_language, $target_language); } function aitmt_fallback_provider_prompt_items($segments) { $items = array(); foreach ((array) $segments as $hash => $text) { $items[] = array( 'id' => (string) $hash, 'text' => (string) $text, ); } return $items; } function aitmt_fallback_provider_prompt($segments, $source_language, $target_language) { $instruction = aitmt_fallback_translation_instruction(); $locale_instruction = AITMT_Locales::translation_instruction($source_language, $target_language, aitmt_fallback_languages()); return sprintf( '%1$s Preserve formatting and placeholders. %2$s Return only JSON in this exact shape: {"translations":[{"id":"same id","text":"translated text"}]}. Segments: %3$s', $locale_instruction, $instruction, wp_json_encode(aitmt_fallback_provider_prompt_items($segments), JSON_UNESCAPED_UNICODE) ); } function aitmt_fallback_parse_provider_translations($text, $segments, $provider) { $decoded = json_decode((string) $text, true); if (!is_array($decoded)) { $decoded = json_decode(aitmt_fallback_strip_json_fence((string) $text), true); } if (!is_array($decoded) || empty($decoded['translations']) || !is_array($decoded['translations'])) { return new WP_Error( 'aitmt_' . sanitize_key((string) $provider) . '_parse_error', sprintf( /* translators: %s: translation provider name. */ __('%s returned an unexpected translation payload.', 'ait-multilingual-translate'), aitmt_fallback_provider_label($provider) ) ); } $translations = array(); foreach ($decoded['translations'] as $item) { $hash = isset($item['id']) ? (string) $item['id'] : ''; if ('' === $hash || !array_key_exists($hash, $segments) || !array_key_exists('text', $item)) { continue; } $translations[$hash] = (string) $item['text']; } return $translations; } function aitmt_fallback_openai_compatible_translate_batch($provider, $endpoint, $segments, $source_language, $target_language) { $provider = sanitize_key((string) $provider); $api_key = aitmt_fallback_provider_key($provider); if (empty($api_key)) { return new WP_Error( 'aitmt_missing_' . $provider . '_key', sprintf( /* translators: %s: translation provider name. */ __('%s API key is missing.', 'ait-multilingual-translate'), aitmt_fallback_provider_label($provider) ) ); } $body = array( 'model' => aitmt_fallback_provider_model($provider), 'temperature' => aitmt_fallback_translation_temperature(), 'messages' => array( array( 'role' => 'user', 'content' => aitmt_fallback_provider_prompt($segments, $source_language, $target_language), ), ), ); $response = wp_remote_post( esc_url_raw($endpoint), array( 'timeout' => aitmt_fallback_provider_timeout($provider, 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); $data = json_decode((string) wp_remote_retrieve_body($response), true); if ($status < 200 || $status >= 300) { $message = aitmt_fallback_extract_error_message( is_array($data) ? $data : array(), sprintf( /* translators: %s: translation provider name. */ __('%s request failed.', 'ait-multilingual-translate'), aitmt_fallback_provider_label($provider) ) ); if (429 === $status) { aitmt_fallback_set_provider_cooldown($provider, HOUR_IN_SECONDS); } return new WP_Error('aitmt_' . $provider . '_error', $message, array('status' => $status)); } $content = isset($data['choices'][0]['message']['content']) ? (string) $data['choices'][0]['message']['content'] : ''; return aitmt_fallback_parse_provider_translations($content, $segments, $provider); } function aitmt_fallback_mistral_translate_batch($segments, $source_language, $target_language) { return aitmt_fallback_openai_compatible_translate_batch('mistral', 'https://api.mistral.ai/v1/chat/completions', $segments, $source_language, $target_language); } function aitmt_fallback_deepseek_translate_batch($segments, $source_language, $target_language) { return aitmt_fallback_openai_compatible_translate_batch('deepseek', 'https://api.deepseek.com/chat/completions', $segments, $source_language, $target_language); } function aitmt_fallback_qwen_translate_batch($segments, $source_language, $target_language) { $region = aitmt_fallback_provider_setting('qwen', 'region', 'us'); $endpoints = array( 'us' => 'https://dashscope-us.aliyuncs.com/compatible-mode/v1/chat/completions', 'singapore' => 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions', 'china' => 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', ); $endpoint = isset($endpoints[$region]) ? $endpoints[$region] : $endpoints['us']; return aitmt_fallback_openai_compatible_translate_batch('qwen', $endpoint, $segments, $source_language, $target_language); } function aitmt_fallback_baidu_translate_batch($segments, $source_language, $target_language) { return aitmt_fallback_openai_compatible_translate_batch('baidu', 'https://api.baiduqianfan.ai/v1/chat/completions', $segments, $source_language, $target_language); } function aitmt_fallback_claude_translate_batch($segments, $source_language, $target_language) { $api_key = aitmt_fallback_provider_key('claude'); if (empty($api_key)) { return new WP_Error('aitmt_missing_claude_key', __('Claude API key is missing.', 'ait-multilingual-translate')); } $response = wp_remote_post( 'https://api.anthropic.com/v1/messages', array( 'timeout' => aitmt_fallback_provider_timeout('claude', 60), 'headers' => array( 'x-api-key' => $api_key, 'anthropic-version' => '2023-06-01', 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( array( 'model' => aitmt_fallback_provider_model('claude'), 'max_tokens' => 4096, 'temperature' => aitmt_fallback_translation_temperature(), 'messages' => array( array( 'role' => 'user', 'content' => aitmt_fallback_provider_prompt($segments, $source_language, $target_language), ), ), ) ), ) ); if (is_wp_error($response)) { return $response; } $status = (int) wp_remote_retrieve_response_code($response); $data = json_decode((string) wp_remote_retrieve_body($response), true); if ($status < 200 || $status >= 300) { $message = aitmt_fallback_extract_error_message(is_array($data) ? $data : array(), __('Claude request failed.', 'ait-multilingual-translate')); if (429 === $status) { aitmt_fallback_set_provider_cooldown('claude', HOUR_IN_SECONDS); } return new WP_Error('aitmt_claude_error', $message, array('status' => $status)); } $content = isset($data['content'][0]['text']) ? (string) $data['content'][0]['text'] : ''; return aitmt_fallback_parse_provider_translations($content, $segments, 'claude'); } function aitmt_fallback_yandex_translate_batch($segments, $source_language, $target_language) { $api_key = aitmt_fallback_provider_key('yandex_translate'); $folder_id = aitmt_fallback_provider_setting('yandex_translate', 'folder_id'); if (empty($api_key) || empty($folder_id)) { return new WP_Error('aitmt_missing_yandex_translate_credentials', __('Yandex Translate API key and folder ID are required.', 'ait-multilingual-translate')); } $hashes = array_keys($segments); $response = wp_remote_post( 'https://translate.api.cloud.yandex.net/translate/v2/translate', array( 'timeout' => aitmt_fallback_provider_timeout('yandex_translate', 45), 'headers' => array( 'Authorization' => 'Api-Key ' . $api_key, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( array( 'folderId' => $folder_id, 'sourceLanguageCode' => AITMT_Locales::provider_code($source_language, 'yandex_translate', true), 'targetLanguageCode' => AITMT_Locales::provider_code($target_language, 'yandex_translate'), 'format' => 'PLAIN_TEXT', 'texts' => array_values($segments), ) ), ) ); if (is_wp_error($response)) { return $response; } $status = (int) wp_remote_retrieve_response_code($response); $data = json_decode((string) wp_remote_retrieve_body($response), true); if ($status < 200 || $status >= 300) { $message = aitmt_fallback_extract_error_message(is_array($data) ? $data : array(), __('Yandex Translate request failed.', 'ait-multilingual-translate')); if (429 === $status) { aitmt_fallback_set_provider_cooldown('yandex_translate', HOUR_IN_SECONDS); } return new WP_Error('aitmt_yandex_translate_error', $message, array('status' => $status)); } $translations = array(); foreach ((array) ($data['translations'] ?? array()) as $index => $item) { if (isset($hashes[$index], $item['text'])) { $translations[$hashes[$index]] = (string) $item['text']; } } if (empty($translations)) { return new WP_Error( 'aitmt_yandex_translate_parse_error', __('Yandex Translate returned an unexpected translation payload.', 'ait-multilingual-translate') ); } return $translations; } function aitmt_fallback_yandexgpt_translate_batch($segments, $source_language, $target_language) { $api_key = aitmt_fallback_provider_key('yandexgpt'); $folder_id = aitmt_fallback_provider_setting('yandexgpt', 'folder_id'); if (empty($api_key) || empty($folder_id)) { return new WP_Error('aitmt_missing_yandexgpt_credentials', __('YandexGPT API key and folder ID are required.', 'ait-multilingual-translate')); } $response = wp_remote_post( 'https://llm.api.cloud.yandex.net/foundationModels/v1/completion', array( 'timeout' => aitmt_fallback_provider_timeout('yandexgpt', 60), 'headers' => array( 'Authorization' => 'Api-Key ' . $api_key, 'Content-Type' => 'application/json', ), 'body' => wp_json_encode( array( 'modelUri' => 'gpt://' . $folder_id . '/' . aitmt_fallback_provider_model('yandexgpt'), 'completionOptions' => array( 'stream' => false, 'temperature' => aitmt_fallback_translation_temperature(), 'maxTokens' => '4096', ), 'messages' => array( array( 'role' => 'user', 'text' => aitmt_fallback_provider_prompt($segments, $source_language, $target_language), ), ), ) ), ) ); if (is_wp_error($response)) { return $response; } $status = (int) wp_remote_retrieve_response_code($response); $data = json_decode((string) wp_remote_retrieve_body($response), true); if ($status < 200 || $status >= 300) { $message = aitmt_fallback_extract_error_message(is_array($data) ? $data : array(), __('YandexGPT request failed.', 'ait-multilingual-translate')); if (429 === $status) { aitmt_fallback_set_provider_cooldown('yandexgpt', HOUR_IN_SECONDS); } return new WP_Error('aitmt_yandexgpt_error', $message, array('status' => $status)); } $content = isset($data['result']['alternatives'][0]['message']['text']) ? (string) $data['result']['alternatives'][0]['message']['text'] : ''; return aitmt_fallback_parse_provider_translations($content, $segments, 'yandexgpt'); } 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 'string', 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, $allow_locale_fallback = true, $include_drafts = true) { global $wpdb; if (empty($segments)) { return array(); } aitmt_fallback_maybe_create_translation_table(); $hashes = array_keys($segments); foreach ($segments as $source_text) { $legacy_hash = aitmt_fallback_legacy_translation_hash($source_text); if (!in_array($legacy_hash, $hashes, true)) { $hashes[] = $legacy_hash; } } $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 id, source_hash, source_text, translated_text, target_language, context, source_type, status 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) { if (!$include_drafts && 'draft' === (string) ($row['status'] ?? '')) { continue; } $current_hash = aitmt_fallback_translation_hash((string) ($row['source_text'] ?? '')); if (isset($segments[$current_hash])) { $map[$current_hash] = (string) $row['translated_text']; } elseif (isset($segments[$row['source_hash']])) { $map[$row['source_hash']] = (string) $row['translated_text']; } if (in_array((string) ($row['source_type'] ?? ''), array('page_load', 'legacy_runtime'), true)) { AITMT_Runtime_Maintenance::record_usage($row); } } if ($allow_locale_fallback) { $base_language = AITMT_Locales::base($target_language); $target_language = AITMT_Locales::normalize($target_language); if ($base_language && $base_language !== $target_language) { $missing_segments = array_diff_key($segments, $map); if (!empty($missing_segments)) { $base_translations = aitmt_fallback_get_existing_translations( $missing_segments, $source_language, $base_language, false, $include_drafts ); $map = array_replace($base_translations, $map); } } } return $map; } function aitmt_fallback_save_translation_batch($source_segments, $translated_segments, $source_language, $target_language, $status, $source_type = 'string', $object_id = 0, $field_key = '') { 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, $source_type, $object_id, $field_key ); } } function aitmt_fallback_save_translation($source_text, $translated_text, $source_language, $target_language, $status, $source_type = 'manual', $object_id = 0, $field_key = '') { global $wpdb; 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; } $table = aitmt_fallback_translation_table_sql(); if ('' === $table) { return false; } $source_type = aitmt_fallback_normalize_source_type($source_type); if ('' === $source_type) { return false; } $status = sanitize_key($status); if (!in_array($status, array('queued', 'processing', 'published', 'draft', 'manual', 'import', 'failed'), true)) { $status = 'draft'; } $now = current_time('mysql'); $query = $wpdb->prepare( "INSERT INTO {$table} (source_hash, source_language, target_language, context, source_text, translated_text, status, provider, source_type, object_id, field_key, created_at, updated_at) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %d, %s, %s, %s) ON DUPLICATE KEY UPDATE translated_text = CASE WHEN VALUES(status) IN ('manual', 'import') THEN VALUES(translated_text) WHEN status IN ('published', 'manual', 'import', 'draft') AND translated_text <> '' THEN translated_text ELSE VALUES(translated_text) END, status = CASE WHEN VALUES(status) IN ('manual', 'import') THEN VALUES(status) WHEN status IN ('published', 'manual', 'import', 'draft') AND translated_text <> '' THEN status ELSE VALUES(status) END, provider = CASE WHEN VALUES(status) IN ('manual', 'import') THEN VALUES(provider) WHEN status IN ('published', 'manual', 'import', 'draft') AND translated_text <> '' THEN provider ELSE VALUES(provider) END, source_type = CASE WHEN source_type = '' OR source_type = 'unknown' THEN VALUES(source_type) ELSE source_type END, object_id = CASE WHEN object_id = 0 THEN VALUES(object_id) ELSE object_id END, field_key = CASE WHEN field_key = '' THEN VALUES(field_key) ELSE field_key END, updated_at = VALUES(updated_at)", aitmt_fallback_translation_hash($source_text), aitmt_fallback_normalize_language($source_language), aitmt_fallback_normalize_language($target_language), 'html', $source_text, $translated_text, $status, aitmt_fallback_active_provider(), $source_type, absint($object_id), aitmt_fallback_normalize_field_key($field_key), $now, $now ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query and values are prepared above; the table name is validated. $result = $wpdb->query($query); 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; } if ('page_load' === $source_type) { $options = aitmt_fallback_options(); if ('1' !== (string) ($options['runtime_db_storage'] ?? '0')) { AITMT_Runtime_Maintenance::increment_counter('runtime_storage_disabled'); return false; } if (!AITMT_Runtime_Maintenance::runtime_capacity_available($target_language)) { AITMT_Runtime_Maintenance::increment_counter('runtime_capacity_reached'); return false; } } $now = current_time('mysql'); $table = aitmt_fallback_translation_table_sql(); if ('' === $table) { return false; } if (aitmt_fallback_source_type_is_excluded($source_type)) { aitmt_fallback_record_rejected_string('excluded_source_type', $source_text, array( 'source_type' => $source_type, 'object_id' => absint($object_id), 'field_key' => $field_key, )); return false; } $hash = aitmt_fallback_translation_hash($source_text); $scanner_options = aitmt_fallback_options(); if (AITMT_Scanner::is_excluded($source_type, absint($object_id), $field_key, $scanner_options) || AITMT_Scanner::is_flattened_elementor_row(array('source_type' => $source_type, 'object_id' => $object_id, 'field_key' => $field_key), $scanner_options)) { return false; } $legacy_hash = aitmt_fallback_legacy_translation_hash($source_text); if ($legacy_hash !== $hash) { $legacy_id = $wpdb->get_var( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is validated and values are prepared. $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 -- Controlled table name. $legacy_hash, aitmt_fallback_normalize_language($source_language), aitmt_fallback_normalize_language($target_language), 'html' ) ); if ($legacy_id) { return false; } } $query = $wpdb->prepare( "INSERT IGNORE INTO {$table} (source_hash, source_language, target_language, context, source_text, translated_text, status, provider, source_type, object_id, field_key, created_at, updated_at) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %d, %s, %s, %s)", $hash, aitmt_fallback_normalize_language($source_language), aitmt_fallback_normalize_language($target_language), 'html', $source_text, '', 'queued', aitmt_fallback_active_provider(), $source_type, absint($object_id), aitmt_fallback_normalize_field_key($field_key), $now, $now ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Query and values are prepared above; the table name is validated. $result = $wpdb->query($query); // Shared text has one job. A new included owner may replace an excluded legacy owner, never saved memory. if (0 === $result) { $existing = $wpdb->get_row($wpdb->prepare( "SELECT id, source_type, object_id, field_key FROM {$table} WHERE source_hash = %s AND source_language = %s AND target_language = %s AND context = 'html' AND status = 'queued' AND translated_text = '' AND provider NOT IN ('manual', 'import') LIMIT 1", $hash, aitmt_fallback_normalize_language($source_language), aitmt_fallback_normalize_language($target_language) ), ARRAY_A); if ($existing && !AITMT_Scanner::queue_row_enabled($existing, $scanner_options)) { $wpdb->update($table, array('source_type' => $source_type, 'object_id' => absint($object_id), 'field_key' => aitmt_fallback_normalize_field_key($field_key)), array('id' => absint($existing['id']), 'status' => 'queued', 'translated_text' => ''), array('%s', '%d', '%s'), array('%d', '%s', '%s')); } } if (1 === (int) $result && 'page_load' === $source_type) { AITMT_Runtime_Maintenance::increment_runtime_count($target_language); } return 1 === (int) $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(); $limit = max(1, absint($options['retry_limit'] ?? 3)); $adapter = AITMT_Provider_Registry::get(aitmt_fallback_active_provider()); return $adapter ? max(1, min($limit, $adapter->get_retry_limit())) : $limit; } function aitmt_fallback_normalize_source_type($source_type) { $source_type = sanitize_key((string) $source_type); if ('' === $source_type || 'unknown' === $source_type) { return ''; } $allowed = array( 'post', 'page', 'product', 'product_variation', 'taxonomy', 'menu', 'widget', 'elementor', 'elementor_library', 'sectionbuilder', 'litho-mega-menu', 'page_meta', 'wpfd_file', 'page_load', 'string', 'scan', 'seo', 'manual', 'import', 'provider', 'frontend_editor', 'shortcode', 'runtime', 'legacy_runtime', ); if (in_array($source_type, $allowed, true)) { return $source_type; } if (function_exists('get_post_type_object')) { $post_type_object = get_post_type_object($source_type); if ($post_type_object && !empty($post_type_object->public)) { return substr($source_type, 0, 80); } } if (preg_match('/^(post|page|product|product_variation|taxonomy|menu|widget|elementor|seo)_meta$/', $source_type)) { return substr($source_type, 0, 80); } if (preg_match('/^([a-z0-9_]+)_meta$/', $source_type, $matches) && function_exists('get_post_type_object')) { $post_type_object = get_post_type_object($matches[1]); if ($post_type_object && !empty($post_type_object->public)) { return substr($source_type, 0, 80); } } return ''; } function aitmt_fallback_runtime_excluded_source_types() { $options = aitmt_fallback_options(); $raw = preg_split('/[\s,]+/', strtolower((string) ($options['runtime_excluded_source_types'] ?? ''))); $types = array(); foreach ((array) $raw as $source_type) { $source_type = sanitize_key($source_type); if ('' !== $source_type) { $types[] = $source_type; } } return array_values(array_unique($types)); } function aitmt_fallback_source_type_is_excluded($source_type) { $source_type = aitmt_fallback_normalize_source_type($source_type); return '' !== $source_type && in_array($source_type, aitmt_fallback_runtime_excluded_source_types(), true); } function aitmt_fallback_normalize_field_key($field_key) { $field_key = preg_replace('/[^a-z0-9_.:\-]/', '', strtolower((string) $field_key)); return strlen($field_key) <= 120 ? $field_key : substr($field_key, 0, 79) . ':' . sha1($field_key); } 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_skipped($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(wp_strip_all_tags((string) $reason), 0, 500, 'UTF-8') : substr(wp_strip_all_tags((string) $reason), 0, 500); $now = current_time('mysql'); $placeholders = implode(',', array_fill(0, count($ids), '%d')); $args = array_merge(array('skipped', $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. } return array( 'failed' => count($failed_ids), 'requeued' => count($retry_ids), ); } 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, 'error_code' => $lock->get_error_code(), 'error_status' => absint($lock->get_error_data('status')), ); } $release_lock = true; } try { $limit = aitmt_fallback_provider_batch_limit('', $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; $skipped = 0; $cached = 0; $failed = 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_Scanner::queue_row_enabled($row, $options)) { aitmt_fallback_record_rejected_string('disabled_or_unpublished_source', $row['source_text'] ?? '', 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; } 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)) { $skipped += aitmt_fallback_mark_processing_rows_skipped($table, $invalid_rows, 'Skipped unsafe, excluded, obsolete, or unpublished queue item before provider request.'); } $published_existing = aitmt_fallback_get_existing_translations($segments, $source_language, $target_language, true, false); $known_existing = aitmt_fallback_get_existing_translations($segments, $source_language, $target_language, true, true); $cached_hashes = array_intersect_key($segments, $known_existing); $segments = array_diff_key($segments, $known_existing); if (!empty($cached_hashes)) { foreach ($valid_claimed_rows as $row) { $hash = (string) $row['source_hash']; if (!isset($cached_hashes[$hash], $known_existing[$hash])) { continue; } $cached_status = isset($published_existing[$hash]) ? 'published' : 'draft'; $updated = $wpdb->update( $table, array( 'translated_text' => (string) $known_existing[$hash], 'status' => $cached_status, 'provider' => 'translation_memory', '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) { $cached++; } } } if (empty($segments)) { 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()); $failure_counts = 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, 'skipped' => $skipped, 'cached' => $cached, 'failed' => $failed + absint($failure_counts['failed'] ?? 0), 'requeued' => absint($failure_counts['requeued'] ?? 0), 'error_code' => $translated->get_error_code(), 'error_status' => absint($translated->get_error_data('status')), ); } 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.'); $failed++; 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, 'skipped' => $skipped, 'cached' => $cached, 'failed' => $failed, '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); } $options = aitmt_fallback_options(); $source_buckets = AITMT_Scanner::collect_post($post_id, $options); if (empty($source_buckets)) { 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))); $unique = array(); $queued = 0; foreach ($source_buckets as $source_type => $items) { foreach ((array) $items as $hash => $item) { $text = is_array($item) ? (string) ($item['text'] ?? '') : (string) $item; $object_id = is_array($item) ? absint($item['object_id'] ?? $post_id) : $post_id; $field_key = is_array($item) ? aitmt_fallback_normalize_field_key($item['field_key'] ?? '') : ''; if ('' === $text) { continue; } $unique[aitmt_fallback_translation_hash($text)] = true; 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( 'strings' => count($unique), '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_worker_state() { $state = get_option('aitmt_worker_state', array()); $state = wp_parse_args(is_array($state) ? $state : array(), array( 'status' => 'stopped', 'started_at' => '', 'completed_at' => '', 'initial' => 0, 'processed' => 0, 'remaining' => aitmt_fallback_queued_count(), 'failed' => 0, 'skipped' => 0, 'cached' => 0, 'last_batch' => '', 'last_message' => '', )); if ('cooldown' === (string) $state['status']) { $state['status'] = 'provider_cooldown'; } return $state; } function aitmt_fallback_worker_error_status($error_code, $error_status = 0, $locked = false) { $error_code = sanitize_key((string) $error_code); if ($locked || 'aitmt_queue_locked' === $error_code) { return 'waiting_for_lock'; } $quota_codes = array( 'aitmt_requests_per_minute', 'aitmt_tokens_per_minute', 'aitmt_requests_per_day', 'aitmt_request_char_limit', 'aitmt_estimated_cost_limit', 'aitmt_daily_quota', 'aitmt_monthly_quota', ); if (in_array($error_code, $quota_codes, true)) { return 'quota_reached'; } if ('aitmt_provider_cooldown' === $error_code || 429 === absint($error_status)) { return 'provider_cooldown'; } return 'failed'; } function aitmt_fallback_start_worker() { $queued = aitmt_fallback_queued_count(); $estimate = aitmt_fallback_queue_estimate(); if (!empty($estimate['unknown_queued']) || !empty($estimate['technical_queued'])) { $state = aitmt_fallback_worker_state(); $state['status'] = 'blocked'; $state['remaining'] = $queued; $state['last_message'] = __('Background translation is blocked until unknown and technical queue rows are reviewed or cleared.', 'ait-multilingual-translate'); update_option('aitmt_worker_state', $state, false); return $state; } $state = array( 'status' => $queued > 0 ? 'running' : 'completed', 'started_at' => current_time('mysql'), 'completed_at' => $queued > 0 ? '' : current_time('mysql'), 'initial' => $queued, 'processed' => 0, 'remaining' => $queued, 'failed' => 0, 'skipped' => 0, 'cached' => 0, 'last_batch' => '', 'last_message' => $queued > 0 ? __('Background translation worker started.', 'ait-multilingual-translate') : __('Queue is empty.', 'ait-multilingual-translate'), ); update_option('aitmt_worker_state', $state, false); if ($queued > 0 && !wp_next_scheduled('aitmt_fallback_worker_event')) { wp_schedule_single_event(time() + 1, 'aitmt_fallback_worker_event'); } return $state; } function aitmt_fallback_worker_control_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_worker_control'); $operation = isset($_POST['operation']) ? sanitize_key(wp_unslash((string) $_POST['operation'])) : ''; $state = aitmt_fallback_worker_state(); if ('pause' === $operation) { $state['status'] = 'paused'; $state['last_message'] = __('Worker paused by an administrator.', 'ait-multilingual-translate'); } elseif ('resume' === $operation) { $state['status'] = 'running'; $state['last_message'] = __('Worker resumed.', 'ait-multilingual-translate'); if (!wp_next_scheduled('aitmt_fallback_worker_event')) { wp_schedule_single_event(time() + 1, 'aitmt_fallback_worker_event'); } } elseif ('stop' === $operation) { $state['status'] = 'stopped'; $state['last_message'] = __('Worker stopped by an administrator. Queued rows were preserved.', 'ait-multilingual-translate'); wp_clear_scheduled_hook('aitmt_fallback_worker_event'); } elseif ('run' === $operation) { $state = aitmt_fallback_start_worker(); } elseif ('reschedule' === $operation) { wp_clear_scheduled_hook('aitmt_fallback_process_queue_event'); aitmt_fallback_sync_cron_schedule(); $state['last_message'] = __('Background schedule was rebuilt.', 'ait-multilingual-translate'); } elseif ('clear_lock' === $operation) { $lock = aitmt_fallback_queue_lock_status(); if (empty($lock['active'])) { delete_option('aitmt_queue_lock'); $state['last_message'] = __('Stale queue lock cleared.', 'ait-multilingual-translate'); } else { $state['last_message'] = __('The queue lock is active and was not cleared.', 'ait-multilingual-translate'); } } update_option('aitmt_worker_state', $state, false); wp_safe_redirect(admin_url('admin.php?page=ait-multilingual-translate-scanner')); exit; } function aitmt_fallback_save_background_settings_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_save_background_settings'); $options = get_option('aitmt_options', array()); $options = is_array($options) ? $options : array(); $interval = isset($_POST['cron_interval']) ? absint($_POST['cron_interval']) : 300; $options['cron_enabled'] = empty($_POST['cron_enabled']) ? '0' : '1'; $options['cron_interval'] = in_array($interval, array(60, 120, 300, 600, 900, 1800, 3600), true) ? $interval : 300; $options['cron_max_batches'] = max(1, min(10, absint($_POST['cron_max_batches'] ?? 2))); $options['cron_max_runtime'] = max(5, min(120, absint($_POST['cron_max_runtime'] ?? 20))); $options['max_segments_per_request'] = max(1, min(100, absint($_POST['max_segments_per_request'] ?? 5))); update_option('aitmt_options', $options, false); wp_clear_scheduled_hook('aitmt_fallback_process_queue_event'); aitmt_fallback_sync_cron_schedule(); if ('1' === $options['cron_enabled'] && aitmt_fallback_queued_count() > 0) { aitmt_fallback_start_worker(); } wp_safe_redirect(admin_url('admin.php?page=ait-multilingual-translate-scanner')); exit; } function aitmt_fallback_cron_schedules($schedules) { $intervals = array( 60 => __('Every 1 minute', 'ait-multilingual-translate'), 120 => __('Every 2 minutes', 'ait-multilingual-translate'), 300 => __('Every 5 minutes', 'ait-multilingual-translate'), 600 => __('Every 10 minutes', 'ait-multilingual-translate'), 900 => __('Every 15 minutes', 'ait-multilingual-translate'), 1800 => __('Every 30 minutes', 'ait-multilingual-translate'), 3600 => __('Hourly', 'ait-multilingual-translate'), ); foreach ($intervals as $seconds => $display) { $schedules['aitmt_' . $seconds] = array( 'interval' => $seconds, 'display' => $display, ); } return $schedules; } function aitmt_fallback_sync_cron_schedule() { $options = aitmt_fallback_options(); $hook = 'aitmt_fallback_process_queue_event'; $scheduled = wp_next_scheduled($hook); $interval = absint($options['cron_interval'] ?? 300); $schedule = 'aitmt_' . $interval; $event = function_exists('wp_get_scheduled_event') ? wp_get_scheduled_event($hook) : false; if ('1' === $options['cron_enabled']) { if ($event && isset($event->schedule) && $schedule !== $event->schedule) { wp_clear_scheduled_hook($hook); $scheduled = false; } if (!$scheduled) { $result = wp_schedule_event(time() + min(60, $interval), $schedule, $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(); $worker = aitmt_fallback_worker_state(); if ('provider_cooldown' === (string) $worker['status']) { $cooldown = aitmt_fallback_provider_cooldown_remaining(); if ($cooldown > 0) { if (!wp_next_scheduled('aitmt_fallback_worker_event')) { wp_schedule_single_event(time() + $cooldown + 1, 'aitmt_fallback_worker_event'); } return; } $worker['status'] = 'running'; update_option('aitmt_worker_state', $worker, false); } elseif ('waiting_for_lock' === (string) $worker['status']) { $worker['status'] = 'running'; update_option('aitmt_worker_state', $worker, false); } if ('1' === $options['cron_enabled'] && 'completed' === (string) $worker['status'] && aitmt_fallback_queued_count() > 0) { $worker = aitmt_fallback_start_worker(); } if (in_array((string) $worker['status'], array('paused', 'stopped', 'blocked', 'completed', 'quota_reached', 'failed'), true)) { return; } if ('1' !== $options['cron_enabled'] && 'running' !== $worker['status']) { return; } $limit = max(1, min(100, absint($options['max_segments_per_request'] ?? 5))); $max_batches = max(1, min(10, absint($options['cron_max_batches'] ?? 2))); $deadline = time() + max(5, min(120, absint($options['cron_max_runtime'] ?? 20))); $processed = 0; $skipped = 0; $cached = 0; $failed = 0; $batches = 0; $last = array('ok' => true, 'message' => __('Queue is empty.', 'ait-multilingual-translate'), 'processed' => 0); while ($batches < $max_batches && time() < $deadline && aitmt_fallback_queued_count() > 0) { $last = aitmt_fallback_process_queue($limit); $processed += absint($last['processed'] ?? 0); $skipped += absint($last['skipped'] ?? 0); $cached += absint($last['cached'] ?? 0); $failed += absint($last['failed'] ?? 0); $handled = absint($last['processed'] ?? 0) + absint($last['skipped'] ?? 0) + absint($last['cached'] ?? 0) + absint($last['failed'] ?? 0); $batches++; if (empty($last['ok']) || $handled < 1) { break; } } $remaining = aitmt_fallback_queued_count(); $result = array_merge($last, array( 'processed' => $processed, 'skipped' => $skipped, 'cached' => $cached, 'failed' => $failed, 'batches' => $batches, 'remaining' => $remaining, 'created_at' => current_time('mysql'), )); update_option('aitmt_cron_queue_result', $result, false); if ('running' === $worker['status']) { $worker['processed'] = absint($worker['processed'] ?? 0) + $processed; $worker['skipped'] = absint($worker['skipped'] ?? 0) + $skipped; $worker['cached'] = absint($worker['cached'] ?? 0) + $cached; $worker['failed'] = absint($worker['failed'] ?? 0) + $failed; $worker['remaining'] = $remaining; $worker['last_batch'] = current_time('mysql'); $worker['last_message'] = (string) ($last['message'] ?? ''); if (empty($last['ok'])) { $worker['status'] = aitmt_fallback_worker_error_status( (string) ($last['error_code'] ?? ''), absint($last['error_status'] ?? 0), !empty($last['locked']) ); if ('waiting_for_lock' === $worker['status'] && !wp_next_scheduled('aitmt_fallback_worker_event')) { wp_schedule_single_event(time() + 10, 'aitmt_fallback_worker_event'); } elseif ('provider_cooldown' === $worker['status'] && !wp_next_scheduled('aitmt_fallback_worker_event')) { wp_schedule_single_event(time() + max(60, aitmt_fallback_provider_cooldown_remaining() + 1), 'aitmt_fallback_worker_event'); } } elseif ($remaining < 1) { $worker['status'] = 'completed'; $worker['completed_at'] = current_time('mysql'); } else { if (!wp_next_scheduled('aitmt_fallback_worker_event')) { wp_schedule_single_event(time() + 10, 'aitmt_fallback_worker_event'); } } update_option('aitmt_worker_state', $worker, false); } } function aitmt_fallback_legacy_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_normalize_text($text) { $text = html_entity_decode((string) $text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); $text = str_replace(array("\r\n", "\r", "\xc2\xa0", "\xe2\x80\xaf"), array("\n", "\n", ' ', ' '), $text); $text = preg_replace('/[\x{200B}-\x{200D}\x{2060}\x{FEFF}]/u', '', $text); if (class_exists('Normalizer')) { $normalized = Normalizer::normalize($text, Normalizer::FORM_C); if (false !== $normalized) { $text = $normalized; } } $text = preg_replace('/[\p{Z}\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_legacy_translation_hash($text) { return hash('sha256', aitmt_fallback_legacy_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); // Request-local protected tokens must never reach a provider or the // translation table. Their random component would create unstable hashes, // and a provider could alter the token before byte-safe restoration. if (class_exists('AITMT_Protected_Content') && AITMT_Protected_Content::is_placeholder($text)) { return 'protected_placeholder'; } 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(); $source_type = isset($context['source_type']) ? aitmt_fallback_normalize_source_type($context['source_type']) : ''; if ('page_load' === $source_type) { $options = aitmt_fallback_options(); $length = aitmt_fallback_text_length(aitmt_fallback_normalize_text($text)); $minimum = max(3, absint($options['runtime_min_text_length'] ?? 3)); $maximum = max($minimum, min(5000, absint($options['runtime_max_text_length'] ?? 800))); if ($length < $minimum) { if ($record_rejection) { aitmt_fallback_record_rejected_string('runtime_too_short', $text, $context); } return false; } if ($length > $maximum) { if ($record_rejection) { aitmt_fallback_record_rejected_string('runtime_too_long', $text, $context); } return false; } } $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(); } $unit_key = aitmt_fallback_translation_hash($text) . '|' . AITMT_Scanner::context_key($source_type, $object_id, $field_key); $source_buckets[$source_type][$unit_key] = 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 (array_values(aitmt_fallback_extract_strings_from_html($html)) as $index => $text) { aitmt_fallback_add_scan_source_text($source_buckets, $source_type, $text, $object_id, $field_key . '.text:' . $index); } } 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 = aitmt_fallback_collect_site_scan_sources(); $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[aitmt_fallback_translation_hash($text)] = $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), ); } /** * Collect the current scanner manifest without changing the translation table. * * Maintenance uses the same collector as a normal scan so that a still-valid * Translation Memory entry is never classified as obsolete merely because two * different scanner implementations disagreed. * * @return array>> */ function aitmt_fallback_collect_site_scan_sources() { return AITMT_Scanner::collect(aitmt_fallback_options()); } /** * Return hashes and source types represented by the current scanner output. * * @return array{hashes:array,source_types:array,strings:int} */ function aitmt_fallback_current_scanner_manifest() { $buckets = aitmt_fallback_collect_site_scan_sources(); $hashes = array(); $types = array(); foreach ($buckets as $source_type => $items) { $source_type = aitmt_fallback_normalize_source_type($source_type); if ('' !== $source_type) { $types[$source_type] = true; } foreach ((array) $items as $hash => $item) { $text = is_array($item) ? (string) ($item['text'] ?? '') : (string) $item; if ('' === $text) { continue; } $hashes[aitmt_fallback_translation_hash($text)] = true; } } return array( 'hashes' => $hashes, 'source_types' => array_keys($types), 'strings' => count($hashes), ); } 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_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'])) : ''; $uploaded_name = isset($_FILES['translation_file']['name']) ? sanitize_file_name(wp_unslash((string) $_FILES['translation_file']['name'])) : ''; $uploaded_size = isset($_FILES['translation_file']['size']) ? absint($_FILES['translation_file']['size']) : 0; $uploaded_error = isset($_FILES['translation_file']['error']) ? absint($_FILES['translation_file']['error']) : UPLOAD_ERR_NO_FILE; if (UPLOAD_ERR_OK !== $uploaded_error || '' === $uploaded_tmp_name || !is_uploaded_file($uploaded_tmp_name)) { wp_die(esc_html__('No valid import file was uploaded.', 'ait-multilingual-translate')); } if ($uploaded_size < 1 || $uploaded_size > 10 * MB_IN_BYTES) { wp_die(esc_html__('Import file must be smaller than 10 MB.', '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'; $uploaded_extension = strtolower((string) pathinfo($uploaded_name, PATHINFO_EXTENSION)); if ($uploaded_extension !== $format) { wp_die(esc_html__('The uploaded file extension must match the selected import format.', 'ait-multilingual-translate')); } 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_plural_forms($target_language) { $base = AITMT_Locales::base($target_language); $forms = array( 'ar' => 'nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);', 'cs' => 'nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;', 'fr' => 'nplurals=2; plural=(n > 1);', 'ja' => 'nplurals=1; plural=0;', 'ka' => 'nplurals=1; plural=0;', 'ko' => 'nplurals=1; plural=0;', 'pl' => 'nplurals=3; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4 && (n%100<12 || n%100>14)) ? 1 : 2);', 'ru' => 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);', 'sk' => 'nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;', 'uk' => 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);', 'zh' => 'nplurals=1; plural=0;', ); return $forms[$base] ?? 'nplurals=2; plural=(n != 1);'; } 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"', '"MIME-Version: 1.0\n"', '"Content-Type: text/plain; charset=UTF-8\n"', '"Content-Transfer-Encoding: 8bit\n"', '"Plural-Forms: ' . aitmt_fallback_plural_forms($target_language) . '\n"', '"X-Generator: AIT Multilingual Translate ' . AITMT_VERSION . '\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 . "\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\nPlural-Forms: " . aitmt_fallback_plural_forms($target_language) . "\nX-Generator: AIT Multilingual Translate " . AITMT_VERSION . "\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

    5

    6

    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') ); ?>

    0 ? min(100, (int) round(($worker_handled / $worker_initial) * 100)) : 0; $next_worker_event = wp_next_scheduled('aitmt_fallback_worker_event'); $next_cron_event = wp_next_scheduled('aitmt_fallback_process_queue_event'); $next_queue_event = $next_worker_event && $next_cron_event ? min($next_worker_event, $next_cron_event) : ($next_worker_event ?: $next_cron_event); aitmt_fallback_admin_page_start( __('AI Translate - Scanner', 'ait-multilingual-translate'), __('Collect site strings into the queue, then translate queued strings in controlled batches.', '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('Parser pipeline version', AITMT_PIPELINE_VERSION, true); aitmt_fallback_render_debug_value('Prompt version', AITMT_PROMPT_VERSION, true); aitmt_fallback_render_debug_value('Protected content version', AITMT_Protected_Content::VERSION, true); aitmt_fallback_render_debug_value('Cache configuration signature', aitmt_fallback_cache_configuration_signature(), 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); $runtime_diagnostics = AITMT_Runtime_Maintenance::diagnostics_today(); aitmt_fallback_render_debug_value('Protected fragments today', (string) absint($runtime_diagnostics['protected_elements'] ?? 0), true); aitmt_fallback_render_debug_value('Protection restore failures today', (string) absint($runtime_diagnostics['protected_restore_failed'] ?? 0), empty($runtime_diagnostics['protected_restore_failed'])); aitmt_fallback_render_debug_value('Runtime cache misses today', (string) absint($runtime_diagnostics['translation_cache_misses'] ?? 0), true); aitmt_fallback_render_debug_value('Migration version', (string) get_option('aitmt_runtime_safety_migration_version', 0), !get_option('aitmt_migration_error', '')); aitmt_fallback_render_debug_value('Pre-migration settings backup', false !== get_option('aitmt_options_backup_pre_0_4_0', false) ? 'available' : 'missing', false !== get_option('aitmt_options_backup_pre_0_4_0', false)); 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('X-Content-Type-Options: nosniff'); header('Content-Disposition: attachment; filename="ait-multilingual-translate-debug-' . gmdate('Ymd-His') . '.log"'); $contents = aitmt_fallback_read_local_file($log_path); $download = aitmt_fallback_redact_sensitive_text($contents); header('Content-Length: ' . (string) strlen($download)); echo $download; // 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'])); $estimate = aitmt_fallback_queue_estimate(); if (!$confirmed) { $message = __('Translate All was not started because the cost confirmation checkbox was not selected.', 'ait-multilingual-translate'); } elseif (!empty($estimate['unknown_queued']) || !empty($estimate['technical_queued'])) { $message = __('Translate All is blocked until unknown and technical queue rows are reviewed or cleared.', 'ait-multilingual-translate'); } else { $state = aitmt_fallback_start_worker(); $message = (string) $state['last_message']; } update_option('aitmt_queue_result', array( 'ok' => $confirmed && empty($estimate['unknown_queued']) && empty($estimate['technical_queued']), 'message' => $message, 'processed' => 0, 'background' => true, '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_queue_handler() { if (!current_user_can('manage_options')) { wp_die(esc_html__('Permission denied.', 'ait-multilingual-translate')); } check_admin_referer('aitmt_clear_queue'); $result = aitmt_fallback_delete_queue_batch('status = %s', array('queued')); update_option('aitmt_queue_result', array( 'ok' => true, 'message' => sprintf('Cleared %d queued item(s) in this bounded batch. Repeat to continue.', absint($result['rows'] ?? 0)), 'processed' => 0, 'created_at' => current_time('mysql'), ), false); wp_safe_redirect(admin_url('admin.php?page=ait-multilingual-translate-scanner')); exit; } function aitmt_fallback_delete_queue_batch($where, $args, $limit = 1000) { global $wpdb; $table = aitmt_fallback_translation_table_sql(); if ('' === $table || !aitmt_fallback_translation_table_exists()) { return array('rows' => 0, 'characters' => 0); } $args = array_values((array) $args); $args[] = max(1, min(5000, absint($limit))); $select = $wpdb->prepare( "SELECT id, CHAR_LENGTH(source_text) AS characters FROM {$table} WHERE {$where} ORDER BY id ASC LIMIT %d", ...$args ); $rows = $wpdb->get_results($select, ARRAY_A); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Where clause is selected by internal callers and all values are prepared. $ids = array_map('absint', wp_list_pluck((array) $rows, 'id')); if (empty($ids)) { return array('rows' => 0, 'characters' => 0); } $placeholders = implode(',', array_fill(0, count($ids), '%d')); $delete = $wpdb->prepare("DELETE FROM {$table} WHERE id IN ({$placeholders})", ...$ids); $deleted = (int) $wpdb->query($delete); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- IDs are prepared and table name is validated. return array( 'rows' => $deleted, 'characters' => array_sum(array_map('absint', wp_list_pluck((array) $rows, 'characters'))), ); } 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; } $result = aitmt_fallback_delete_queue_batch( 'status = %s AND (source_type = %s OR source_type = %s)', array('queued', '', 'unknown') ); update_option('aitmt_queue_result', array( 'ok' => true, 'message' => sprintf('Cleared %d unknown queued item(s) in this bounded batch. Repeat to continue.', absint($result['rows'] ?? 0)), '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; } $result = aitmt_fallback_delete_queue_batch( aitmt_fallback_technical_queue_where_sql(), aitmt_fallback_technical_queue_where_args('queued') ); update_option('aitmt_queue_result', array( 'ok' => true, 'message' => sprintf( 'Cleared %1$d technical/non-translatable queued item(s), about %2$d character(s), in this bounded batch. Repeat to continue.', absint($result['rows'] ?? 0), absint($result['characters'] ?? 0) ), '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; } $result = aitmt_fallback_delete_queue_batch( 'status = %s AND source_type = %s', array('queued', 'page_load') ); update_option('aitmt_queue_result', array( 'ok' => true, 'message' => sprintf( 'Cleared %1$d page-load queued item(s), about %2$d queued character(s), in this bounded batch. Repeat to continue.', absint($result['rows'] ?? 0), absint($result['characters'] ?? 0) ), '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']; $default_frontend_language = (string) $options['default_frontend_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); $dashboard_queue = aitmt_fallback_queue_estimate(); $dashboard_worker = aitmt_fallback_worker_state(); $dashboard_provider_stats = aitmt_fallback_provider_stats_for($active_provider); $dashboard_day_chars = aitmt_fallback_provider_chars_used($active_provider, gmdate('Ymd')); $dashboard_month_chars = aitmt_fallback_provider_chars_used($active_provider, gmdate('Ym')); $dashboard_next_worker = wp_next_scheduled('aitmt_fallback_worker_event'); $dashboard_next_cron = wp_next_scheduled('aitmt_fallback_process_queue_event'); $dashboard_next_event = $dashboard_next_worker && $dashboard_next_cron ? min($dashboard_next_worker, $dashboard_next_cron) : ($dashboard_next_worker ?: $dashboard_next_cron); $dashboard_last_translation = !empty($recent_translations[0]) && is_array($recent_translations[0]) ? $recent_translations[0] : array(); ?>

    [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('Parser pipeline version', AITMT_PIPELINE_VERSION, true); aitmt_fallback_render_debug_value('Prompt version', AITMT_PROMPT_VERSION, true); aitmt_fallback_render_debug_value('Protected content version', AITMT_Protected_Content::VERSION, true); aitmt_fallback_render_debug_value('Cache configuration signature', aitmt_fallback_cache_configuration_signature(), 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); $runtime_diagnostics = AITMT_Runtime_Maintenance::diagnostics_today(); aitmt_fallback_render_debug_value('Protected fragments today', (string) absint($runtime_diagnostics['protected_elements'] ?? 0), true); aitmt_fallback_render_debug_value('Protection restore failures today', (string) absint($runtime_diagnostics['protected_restore_failed'] ?? 0), empty($runtime_diagnostics['protected_restore_failed'])); aitmt_fallback_render_debug_value('Runtime cache misses today', (string) absint($runtime_diagnostics['translation_cache_misses'] ?? 0), true); aitmt_fallback_render_debug_value('Migration version', (string) get_option('aitmt_runtime_safety_migration_version', 0), !get_option('aitmt_migration_error', '')); aitmt_fallback_render_debug_value('Pre-migration settings backup', false !== get_option('aitmt_options_backup_pre_0_4_0', false) ? 'available' : 'missing', false !== get_option('aitmt_options_backup_pre_0_4_0', false)); 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')); ?>