wp_kses_post( $node['msgid'] ), 'msgid_plural' => isset( $node['msgid_plural'] ) ? wp_kses_post( $node['msgid_plural'] ) : '', 'msgctxt' => isset( $node['msgctxt'] ) ? sanitize_text_field( $node['msgctxt'] ) : '', ); $sanitized_str = array(); if ( isset( $node['msgstr'] ) && is_array( $node['msgstr'] ) ) { foreach ( $node['msgstr'] as $str ) { $sanitized_str[] = wp_kses_post( $str ); } } $sanitized_node['msgstr'] = $sanitized_str; $strings[] = $sanitized_node; } } } // --- FAST ROUTE: SINGLE STRING DELTA SYNC --- if ( ( 'single' === $save_mode || isset( $_POST['single_update'] ) ) && ! empty( $strings ) ) { $target_node = reset( $strings ); $msgid = isset( $target_node['msgid'] ) ? $target_node['msgid'] : ''; $msgstr = isset( $target_node['msgstr'][0] ) ? $target_node['msgstr'][0] : ''; if ( ! empty( $msgid ) && class_exists( 'AF_Translate_Delta_Sync' ) ) { $success = AF_Translate_Delta_Sync::update_single_string( $meta['po_target'], $msgid, $msgstr, $meta ); if ( $success ) { // Flush cache layers instantly $active_domain = $meta['is_main_core'] ? 'default' : $text_domain; unload_textdomain( $active_domain ); if ( function_exists( 'clean_textdomain_cache' ) ) { clean_textdomain_cache( $active_domain ); } delete_transient( 'af_translate_schema_' . $active_domain . '_' . $locale ); if ( wp_using_ext_object_cache() ) { wp_cache_delete( 'af_translate_schema_' . $active_domain . '_' . $locale, 'options' ); } $log_context = ! empty( $text_domain ) ? $text_domain : $bundle_type; $log_message = sprintf( /* translators: %s: Locale code */ __( 'Fast Delta Sync completed for locale "%s".', 'af-translate' ), strtoupper( $locale ) ); if ( class_exists( 'AF_Translate_Admin' ) && method_exists( 'AF_Translate_Admin', 'log_activity' ) ) { AF_Translate_Admin::log_activity( $log_message, $log_context ); } wp_send_json_success( array( 'message' => esc_html__( 'String updated instantly.', 'af-translate' ), 'status' => 'updated', 'stage' => 'ready', ) ); } } } // --- SLOW ROUTE: BULK SAVE & FULL RE-COMPILATION FALLBACK --- if ( file_exists( $meta['po_target'] ) && ( 'single' === $save_mode || isset( $_POST['single_update'] ) ) ) { $existing_strings = AF_Translate_PO_Processor::parse_file( $meta['po_target'] ); foreach ( $strings as $new_node ) { $matched = false; foreach ( $existing_strings as &$old_node ) { if ( $old_node['msgid'] === $new_node['msgid'] && ( isset( $old_node['msgctxt'] ) ? $old_node['msgctxt'] : '' ) === ( isset( $new_node['msgctxt'] ) ? $new_node['msgctxt'] : '' ) ) { $old_node['msgstr'] = $new_node['msgstr']; if ( ! empty( $new_node['msgid_plural'] ) ) { $old_node['msgid_plural'] = $new_node['msgid_plural']; } $matched = true; break; } } unset( $old_node ); if ( ! $matched ) { $existing_strings[] = $new_node; } } $strings = $existing_strings; } // 1. Generate Portable Object (.po) source $output = AF_Translate_PO_Processor::generate_po_string( $strings ); if ( false === file_put_contents( $meta['po_target'], $output ) ) { wp_send_json_error( esc_html__( 'Critical System Error: Failed to write translation catalog to disk.', 'af-translate' ) ); } // 2. Compile Binary and JSON sidecars (.mo, .l10n.php, .json) if ( class_exists( 'AF_Translate_Ajax_Extended' ) && method_exists( 'AF_Translate_Ajax_Extended', 'compile_all_workspace_artifacts' ) ) { AF_Translate_Ajax_Extended::compile_all_workspace_artifacts( $meta['po_target'], $meta ); } elseif ( AF_Translate_Path_Validator::validate_containment( $meta['mo_target'], true ) ) { AF_Translate_Compiler_Engine::compile_po_to_mo( $meta['po_target'], $meta['mo_target'] ); } // 3. Proactive Cache Invalidation $active_domain = $meta['is_main_core'] ? 'default' : $text_domain; unload_textdomain( $active_domain ); if ( function_exists( 'clean_textdomain_cache' ) ) { clean_textdomain_cache( $active_domain ); } delete_transient( 'af_translate_schema_' . $active_domain . '_' . $locale ); if ( wp_using_ext_object_cache() ) { wp_cache_delete( 'af_translate_schema_' . $active_domain . '_' . $locale, 'options' ); } if ( ! headers_sent() ) { header( 'X-LiteSpeed-Purge: url=' . wp_get_referer() ); } // Formatted log entry output $log_context = ! empty( $text_domain ) ? $text_domain : $bundle_type; $log_message = sprintf( /* translators: 1: Locale code, 2: Record count */ __( 'Successfully saved and compiled "%1$s" catalog (%2$d records).', 'af-translate' ), strtoupper( $locale ), count( $strings ) ); if ( class_exists( 'AF_Translate_Admin' ) && method_exists( 'AF_Translate_Admin', 'log_activity' ) ) { AF_Translate_Admin::log_activity( $log_message, $log_context ); } wp_send_json_success( array( 'message' => esc_html__( 'Workspace saved successfully. Binary maps and JS translation sidecars compiled.', 'af-translate' ), 'status' => 'completed', 'stage' => 'ready', 'stages' => array( 'po' => esc_html__( 'Writing .po catalog file...', 'af-translate' ), 'compile' => esc_html__( 'Compiling binary .mo, PHP maps (.l10n.php), and JS sidecars (.json)...', 'af-translate' ), 'cache' => esc_html__( 'Purging translation cache layers...', 'af-translate' ), 'ready' => esc_html__( 'Translation saved and ready.', 'af-translate' ), ), ) ); } /** * AJAX endpoint to synchronize translations matching target POT files via pure PHP. * Executes full template merging and full artifact rebuild (including Hash JSONs). */ public static function handle_sync() { check_ajax_referer( 'af_translate_secure_action', 'security' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( esc_html__( 'Security Exception: Unauthorized capability state.', 'af-translate' ) ); } $text_domain = isset( $_POST['text_domain'] ) ? preg_replace( '/[^a-zA-Z0-9_.-]/', '', sanitize_text_field( wp_unslash( $_POST['text_domain'] ) ) ) : ''; $bundle_type = isset( $_POST['bundle_type'] ) ? sanitize_key( wp_unslash( $_POST['bundle_type'] ) ) : 'theme'; $fallback_po = isset( $_POST['po_file_target'] ) ? sanitize_text_field( wp_unslash( $_POST['po_file_target'] ) ) : ''; $locale = isset( $_POST['locale'] ) ? preg_replace( '/[^a-zA-Z0-9_-]/', '', sanitize_text_field( wp_unslash( $_POST['locale'] ) ) ) : ''; if ( ! empty( $fallback_po ) ) { $fallback_po = wp_normalize_path( $fallback_po ); if ( 0 !== strpos( $fallback_po, '/' ) && ! preg_match( '/^([a-zA-Z]:)/', $fallback_po ) ) { $fallback_po = trailingslashit( ABSPATH ) . ltrim( $fallback_po, '/' ); } $fallback_po = AF_Translate_Path_Validator::resolve_canonical_path( $fallback_po ); } // --- PATH AND RUNTIME DISCOVERY LAYER --- $meta = AF_Translate_Path_Resolver::resolve_workspace_meta( $text_domain, $bundle_type, $locale ); if ( ! is_dir( $meta['target_dir'] ) ) { wp_mkdir_p( $meta['target_dir'] ); } // Unified cross-platform file casing normalization pass AF_Translate_Path_Resolver::run_case_migration( $meta, $locale, $text_domain ); if ( ! file_exists( $meta['po_target'] ) && ! empty( $fallback_po ) && file_exists( $fallback_po ) ) { if ( AF_Translate_Path_Validator::validate_containment( $fallback_po, false ) ) { @copy( $fallback_po, $meta['po_target'] ); } } if ( ! AF_Translate_Path_Validator::validate_containment( $meta['po_target'], true ) ) { wp_send_json_error( esc_html__( 'Target synchronization parameters violate safe validation matrix.', 'af-translate' ) ); } // --- MASTER POT AUTO-DISCOVERY ENGINE --- $pot_file = ''; $pot_file_name = $meta['is_main_core'] ? 'wordpress.pot' : "{$text_domain}.pot"; $pot_candidates = array( $meta['target_dir'] . '/' . $pot_file_name ); if ( ! $meta['is_core'] && ! empty( $text_domain ) ) { $pot_candidates[] = AF_Translate_Path_Validator::resolve_canonical_path( $meta['base_dir'] . '/languages/' . $pot_file_name ); $pot_candidates[] = AF_Translate_Path_Validator::resolve_canonical_path( $meta['base_dir'] . '/language/' . $pot_file_name ); $pot_candidates[] = AF_Translate_Path_Validator::resolve_canonical_path( $meta['base_dir'] . '/' . $pot_file_name ); } if ( ! empty( $fallback_po ) && file_exists( $fallback_po ) ) { $fallback_dir = dirname( $fallback_po ); $parent_dir = dirname( $fallback_dir ); if ( ! empty( $text_domain ) ) { $pot_candidates[] = AF_Translate_Path_Validator::resolve_canonical_path( trailingslashit( $fallback_dir ) . $pot_file_name ); $pot_candidates[] = AF_Translate_Path_Validator::resolve_canonical_path( trailingslashit( $parent_dir ) . $pot_file_name ); } if ( is_dir( $fallback_dir ) && false === strpos( $fallback_po, 'af-translate' ) ) { $languages_pots = glob( trailingslashit( $fallback_dir ) . '*.pot' ); if ( ! empty( $languages_pots ) && is_array( $languages_pots ) ) { foreach ( $languages_pots as $f ) { $pot_candidates[] = AF_Translate_Path_Validator::resolve_canonical_path( $f ); } } } if ( is_dir( $parent_dir ) && false === strpos( $fallback_po, 'af-translate' ) ) { $root_pots = glob( trailingslashit( $parent_dir ) . '*.pot' ); if ( ! empty( $root_pots ) && is_array( $root_pots ) ) { foreach ( $root_pots as $f ) { $pot_candidates[] = AF_Translate_Path_Validator::resolve_canonical_path( $f ); } } } } foreach ( $pot_candidates as $candidate ) { if ( ! empty( $candidate ) && is_string( $candidate ) ) { $canonical_pot = AF_Translate_Path_Validator::resolve_canonical_path( $candidate ); if ( file_exists( $canonical_pot ) ) { $pot_file = $canonical_pot; break; } } } $sandbox_sync_pot = $meta['target_dir'] . '/' . $pot_file_name; if ( empty( $pot_file ) ) { $generated = false; if ( class_exists( 'AF_Translate_POT_Generator' ) ) { $generated = AF_Translate_POT_Generator::generate_master_pot( $text_domain, $bundle_type, $sandbox_sync_pot ); } if ( ! $generated && ! empty( $fallback_po ) && file_exists( $fallback_po ) && class_exists( 'AF_Translate_PO_Processor' ) ) { $parsed_entries = AF_Translate_PO_Processor::parse_file( $fallback_po ); if ( is_array( $parsed_entries ) ) { foreach ( $parsed_entries as &$entry ) { if ( isset( $entry['msgstr'] ) ) { $entry['msgstr'] = is_array( $entry['msgstr'] ) ? array_fill( 0, count( $entry['msgstr'] ), '' ) : ''; } } unset( $entry ); $pot_content = AF_Translate_PO_Processor::generate_po_string( $parsed_entries ); if ( ! empty( $pot_content ) && false !== file_put_contents( $sandbox_sync_pot, $pot_content ) ) { $generated = true; } } } if ( $generated && file_exists( $sandbox_sync_pot ) ) { $pot_file = $sandbox_sync_pot; } } if ( empty( $pot_file ) || ! file_exists( $pot_file ) ) { wp_send_json_error( esc_html__( 'Synchronization failed: Could not locate a valid master template source for this item.', 'af-translate' ) ); } if ( ! AF_Translate_Path_Validator::validate_containment( $pot_file, false ) ) { wp_send_json_error( esc_html__( 'Master template reference catalog path constraint violation.', 'af-translate' ) ); } $sync_results = AF_Translate_Compiler_Engine::sync_pot_into_po( $meta['po_target'], $pot_file ); if ( false !== $sync_results ) { // --- FULL MULTI-FORMAT COMPILATION PIPELINE --- $meta['component'] = $text_domain; $meta['domain'] = $text_domain; $meta['locale'] = $locale; if ( class_exists( 'AF_Translate_Ajax_Extended' ) && method_exists( 'AF_Translate_Ajax_Extended', 'compile_all_workspace_artifacts' ) ) { AF_Translate_Ajax_Extended::compile_all_workspace_artifacts( $meta['po_target'], $meta ); } elseif ( AF_Translate_Path_Validator::validate_containment( $meta['mo_target'], true ) ) { AF_Translate_Compiler_Engine::compile_po_to_mo( $meta['po_target'], $meta['mo_target'] ); } // --- PROACTIVE CACHE INVALIDATION LAYER --- $active_domain = $meta['is_main_core'] ? 'default' : $text_domain; unload_textdomain( $active_domain ); if ( function_exists( 'clean_textdomain_cache' ) ) { clean_textdomain_cache( $active_domain ); } delete_transient( 'af_translate_schema_' . $active_domain . '_' . $locale ); if ( wp_using_ext_object_cache() ) { wp_cache_delete( 'af_translate_schema_' . $active_domain . '_' . $locale, 'options' ); } if ( ! headers_sent() ) { header( 'X-LiteSpeed-Purge: url=' . wp_get_referer() ); } $log_context = ! empty( $text_domain ) ? $text_domain : $bundle_type; $log_message = sprintf( /* translators: 1: Locale code, 2: Added strings count, 3: Obsolete strings count */ __( 'Synchronized template layout for "%1$s" (+%2$d strings added, -%3$d obsolete strings dropped).', 'af-translate' ), strtoupper( $locale ), intval( $sync_results['new'] ), intval( $sync_results['obsolete'] ) ); if ( class_exists( 'AF_Translate_Admin' ) && method_exists( 'AF_Translate_Admin', 'log_activity' ) ) { AF_Translate_Admin::log_activity( $log_message, $log_context ); } $message = sprintf( /* translators: 1: Number of new strings added, 2: Number of obsolete strings removed */ esc_html__( 'Template synchronized successfully (%1$d new strings added, %2$d obsolete strings removed).', 'af-translate' ), intval( $sync_results['new'] ), intval( $sync_results['obsolete'] ) ); wp_send_json_success( array( 'message' => $message, 'status' => 'synchronized', 'stage' => 'ready', 'added' => intval( $sync_results['new'] ), 'removed' => intval( $sync_results['obsolete'] ), ) ); } else { wp_send_json_error( esc_html__( 'Synchronization failed: Unable to parse template data structures.', 'af-translate' ) ); } } }