$full_path ) { $content = file_get_contents( $full_path ); // Skip files that don't contain i18n translation triggers if ( false === $content || ( false === strpos( $content, 'wp.i18n' ) && false === strpos( $content, '__(' ) && false === strpos( $content, '_x(' ) ) ) { continue; } $clean_content = preg_replace( '/\s+/', ' ', $content ); $js_bundles[ $rel_path ] = array( 'raw' => $content, 'clean' => $clean_content, ); } if ( ! empty( $js_bundles ) ) { foreach ( $entries as $entry ) { $msgid = is_array( $entry ) ? ( $entry['msgid'] ?? '' ) : ( $entry->msgid ?? '' ); if ( empty( $msgid ) || strlen( $msgid ) < 2 ) { continue; } // OPTIMIZATION 2: Fast PO Reference Comment Filter // Extract file reference comments (#: src/app.js:10) $raw_ref = ''; if ( is_array( $entry ) ) { $raw_ref = $entry['reference'] ?? ( $entry['references'] ?? '' ); } elseif ( is_object( $entry ) ) { $raw_ref = $entry->reference ?? ( $entry->references ?? '' ); } if ( is_array( $raw_ref ) ) { $raw_ref = implode( ' ', $raw_ref ); } // If reference comments exist BUT contain no JS/TS extensions, skip PHP-only strings instantly if ( ! empty( $raw_ref ) ) { $has_js_ref = (bool) preg_match( '/\.(js|jsx|ts|tsx)(:\d+)?/i', $raw_ref ); if ( ! $has_js_ref ) { continue; } } // Search pre-processed bundles for confirmed JS candidates foreach ( $js_bundles as $rel_script_path => $bundle ) { if ( self::string_exists_in_preprocessed_bundle( $msgid, $bundle ) ) { if ( ! isset( $js_entries_by_file[ $rel_script_path ] ) ) { $js_entries_by_file[ $rel_script_path ] = array(); } $js_entries_by_file[ $rel_script_path ][] = $entry; } } } } } } // 2. Generate per-script hashed JSON files: {domain}-{locale}-{hash}.json if ( ! empty( $js_entries_by_file ) ) { foreach ( $js_entries_by_file as $rel_script_path => $script_entries ) { $normalized_rel_path = ltrim( wp_normalize_path( $rel_script_path ), '/' ); $script_hash = md5( $normalized_rel_path ); $json_filename = sprintf( '%s-%s-%s.json', $domain, $locale, $script_hash ); $json_target = trailingslashit( $target_dir ) . $json_filename; self::write_jed_json_file( $json_target, $script_entries, $locale, $domain, $normalized_rel_path ); } } // 3. ALWAYS write the primary manifest file: {domain}-{locale}.json $main_json_filename = sprintf( '%s-%s.json', $domain, $locale ); $main_json_target = trailingslashit( $target_dir ) . $main_json_filename; self::write_jed_json_file( $main_json_target, $entries, $locale, $domain ); } /** * Optimized string matcher utilizing pre-processed bundle structures. * * @param string $msgid Raw msgid string from PO file. * @param array $bundle Array containing 'raw' and 'clean' bundle content. * @return bool */ public static function string_exists_in_preprocessed_bundle( $msgid, $bundle ) { // Fast Direct Check if ( false !== strpos( $bundle['raw'], $msgid ) ) { return true; } // Fast Cleaned Whitespace Check $clean_msgid = trim( preg_replace( '/\s+/', ' ', $msgid ) ); if ( ! empty( $clean_msgid ) && false !== strpos( $bundle['clean'], $clean_msgid ) ) { return true; } // Fast Escaped Quote Check $escaped_msgid = addslashes( $clean_msgid ); if ( false !== strpos( $bundle['clean'], $escaped_msgid ) ) { return true; } // Fast Unicode Escape Check $json_encoded = trim( wp_json_encode( $clean_msgid ), '"' ); return ( ! empty( $json_encoded ) && false !== strpos( $bundle['clean'], $json_encoded ) ); } /** * Legacy wrapper for backward compatibility. */ public static function string_exists_in_bundle( $msgid, $bundle_content ) { if ( empty( $msgid ) || empty( $bundle_content ) ) { return false; } $bundle = array( 'raw' => $bundle_content, 'clean' => preg_replace( '/\s+/', ' ', $bundle_content ), ); return self::string_exists_in_preprocessed_bundle( $msgid, $bundle ); } /** * Writes a standardized WordPress Jed 1.x JSON structure to disk. */ public static function write_jed_json_file( $target_path, $entries, $locale, $domain, $source = '' ) { $messages = array( '' => array( 'domain' => 'messages', 'lang' => $locale, 'plural-forms' => 'nplurals=2; plural=(n != 1);', ), ); if ( is_array( $entries ) ) { foreach ( $entries as $e ) { $e_msgid = is_array( $e ) ? ( $e['msgid'] ?? '' ) : ( $e->msgid ?? '' ); $e_msgstr = is_array( $e ) ? ( $e['msgstr'] ?? '' ) : ( $e->msgstr ?? '' ); if ( empty( $e_msgid ) ) { continue; } $msgstr_array = is_array( $e_msgstr ) ? $e_msgstr : array( $e_msgstr ); $messages[ $e_msgid ] = $msgstr_array; } } $jed_payload = array( 'translation-revision-date' => current_time( 'mysql' ), 'generator' => 'AF Translate Engine', 'domain' => 'messages', 'locale_data' => array( 'messages' => $messages, ), ); if ( ! empty( $source ) ) { $jed_payload['source'] = $source; } return false !== file_put_contents( $target_path, wp_json_encode( $jed_payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) ); } /** * Recursively scans for all .js files, ignoring core node_modules/vendor folders. * Uses transient caching to eliminate filesystem scanning overhead during active editing sessions. * * @param string $dir Root path. * @return array */ public static function scan_compiled_js_files( $dir ) { $root = trailingslashit( str_replace( '\\', '/', $dir ) ); $cache_key = 'af_tr_js_scan_' . md5( $root ); $cached_files = get_transient( $cache_key ); if ( false !== $cached_files && is_array( $cached_files ) ) { return $cached_files; } $results = array(); if ( ! is_dir( $root ) ) { return $results; } try { $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $root, RecursiveDirectoryIterator::SKIP_DOTS ) ); $ignored_directories = array( 'node_modules', 'vendor', 'tests', 'coverage', 'assets/vendor', 'build/vendor' ); foreach ( $iterator as $file ) { if ( $file->isDir() || 'js' !== strtolower( $file->getExtension() ) ) { continue; } $full_path = str_replace( '\\', '/', $file->getPathname() ); $rel_path = ltrim( str_replace( $root, '', $full_path ), '/' ); $path_parts = explode( '/', $rel_path ); if ( array_intersect( $path_parts, $ignored_directories ) ) { continue; } // Skip standard third-party libraries if ( preg_match( '#(jquery|lodash|react|react-dom|bootstrap)\.(min\.)?js$#i', $rel_path ) ) { continue; } $results[ $rel_path ] = $full_path; } // Cache file list for 1 hour to speed up editing performance set_transient( $cache_key, $results, HOUR_IN_SECONDS ); } catch ( Exception $e ) { // Safety boundary } return $results; } }