registered as $handle_temp => $script_object ) { if ( in_array( 'wp-i18n', (array) $script_object->deps, true ) && wplng_str_contains( $script_object->src, '/wp-content/' ) ) { $wplng_i18n_scripts[ $handle_temp ] = array( 'handle' => $script_object->handle, 'textdomain' => $script_object->textdomain, 'src' => $script_object->src, ); } } } /** * If we can not associate the current script to a knowed script * (We can not know the script JS file, return) */ if ( empty( $wplng_i18n_scripts ) || empty( $wplng_i18n_scripts[ $handle ] ) ) { return $file; } /** * Check if the replacement translation JSON was already generated by wpLingua */ $file_nomalized = wp_normalize_path( $file ); $dir_cache_script = '/script-i18n'; $file_cache_relative = str_replace( wp_normalize_path( WP_CONTENT_DIR ), '', $file_nomalized ); if ( $file_nomalized === $file_cache_relative ) { return $file; } $file_cache_absolute = WPLNG_CACHE_DIR . $dir_cache_script . $file_cache_relative; if ( is_readable( $file_cache_absolute ) ) { return $file_cache_absolute; } /** * Get the original script path and content */ $script_path = str_replace( content_url(), WP_CONTENT_DIR, $wplng_i18n_scripts[ $handle ]['src'] ); $script_path = wp_normalize_path( $script_path ); if ( ! is_readable( $script_path ) ) { return $file; } $script_content = file_get_contents( $script_path ); /** * Get texts in script */ $texts = wplng_i18n_script_extract_strings( $script_content ); $texts_is_empty = true; foreach ( $texts as $texts_by_extraction_methode ) { if ( ! empty( $texts_by_extraction_methode ) ) { $texts_is_empty = false; break; } } if ( $texts_is_empty ) { // file_put_contents( $file_cache_relative, '' ); wplng_put_cache_file( '/script-i18n' . $file_cache_relative, '' ); return $file; } /** * Make the translations JSON */ $json_content = wplng_i18n_script_generate_json( $texts, $domain ); /** * Generate the wpLingua cached JSON file */ $file_writing_result = wplng_put_cache_file( '/script-i18n' . $file_cache_relative, $json_content ); // Return the generated file if writing was successful if ( $file_writing_result !== false ) { return $file_cache_absolute; } return $file; } /** * Extract translatable strings from a JavaScript script. * * Scans minified WP JS for wp-i18n call patterns used in builds like: * (0,x.__)("text") * (0,x._x)("text","context") * (0,x._n)("singular","plural",count) * (0,x._nx)("singular","plural",count,"context") * * The function handles both single and double quoted strings, escaped characters, * optional domain arguments and returns an associative array grouping extracted * items by function name ('__', '_x', '_n', '_nx'). * * @param string $script JavaScript source code to scan. * @return array { * Associative array with keys: * '__' => array of [ 'text' => string, 'domain' => ?string ], * '_x' => array of [ 'text' => string, 'context' => string, 'domain' => ?string ], * '_n' => array of [ 'singular' => string, 'plural' => string, 'domain' => ?string ], * '_nx' => array of [ 'singular' => string, 'plural' => string, 'context' => string, 'domain' => ?string ], * } */ function wplng_i18n_script_extract_strings( $script ) { $results = array( '__' => array(), '_x' => array(), '_n' => array(), '_nx' => array(), ); // In WordPress minified scripts, calls look like: // (0,r.__)("text") or (0,t._x)("text","context") // where r/t/e/n are aliases for wp.i18n $patterns = array( // (0,X.__)("text") or (0,X.__)('text') with optional domain '__' => '/\(0,[a-zA-Z_$][a-zA-Z0-9_$]*\.__\)\s*\(\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\1)["\'])*)\1(?:\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\3)["\'])*)\3)?\s*\)/u', // (0,X._x)("text","context") or with single quotes, optional domain '_x' => '/\(0,[a-zA-Z_$][a-zA-Z0-9_$]*\._x\)\s*\(\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\1)["\'])*)\1\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\3)["\'])*)\3(?:\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\5)["\'])*)\5)?\s*\)/u', // (0,X._n)("singular","plural",number) with optional domain '_n' => '/\(0,[a-zA-Z_$][a-zA-Z0-9_$]*\._n\)\s*\(\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\1)["\'])*)\1\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\3)["\'])*)\3\s*,\s*[^,)]+(?:\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\5)["\'])*)\5)?\s*\)/u', // (0,X._nx)("singular","plural",number,"context") with optional domain '_nx' => '/\(0,[a-zA-Z_$][a-zA-Z0-9_$]*\._nx\)\s*\(\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\1)["\'])*)\1\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\3)["\'])*)\3\s*,\s*[^,]+\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\5)["\'])*)\5(?:\s*,\s*(["\'])((?:[^"\'\\\\]|\\\\.|(?!\7)["\'])*)\7)?\s*\)/u', ); // Extraction for __() if ( preg_match_all( $patterns['__'], $script, $matches, PREG_SET_ORDER ) ) { foreach ( $matches as $match ) { $text = wplng_unescape_js_string( $match[2] ); if ( ! empty( $text ) ) { $results['__'][] = array( 'text' => $text, 'domain' => isset( $match[4] ) && $match[4] !== '' ? wplng_unescape_js_string( $match[4] ) : null, ); } } } // Extraction for _x() if ( preg_match_all( $patterns['_x'], $script, $matches, PREG_SET_ORDER ) ) { foreach ( $matches as $match ) { $text = wplng_unescape_js_string( $match[2] ); if ( ! empty( $text ) ) { $results['_x'][] = array( 'text' => $text, 'context' => wplng_unescape_js_string( $match[4] ), 'domain' => isset( $match[6] ) && $match[6] !== '' ? wplng_unescape_js_string( $match[6] ) : null, ); } } } // Extraction for _n() if ( preg_match_all( $patterns['_n'], $script, $matches, PREG_SET_ORDER ) ) { foreach ( $matches as $match ) { $singular = wplng_unescape_js_string( $match[2] ); if ( ! empty( $singular ) ) { $results['_n'][] = array( 'singular' => $singular, 'plural' => wplng_unescape_js_string( $match[4] ), 'domain' => isset( $match[6] ) && $match[6] !== '' ? wplng_unescape_js_string( $match[6] ) : null, ); } } } // Extraction for _nx() if ( preg_match_all( $patterns['_nx'], $script, $matches, PREG_SET_ORDER ) ) { foreach ( $matches as $match ) { $singular = wplng_unescape_js_string( $match[2] ); if ( ! empty( $singular ) ) { $results['_nx'][] = array( 'singular' => $singular, 'plural' => wplng_unescape_js_string( $match[4] ), 'context' => wplng_unescape_js_string( $match[6] ), 'domain' => isset( $match[8] ) && $match[8] !== '' ? wplng_unescape_js_string( $match[8] ) : null, ); } } } return $results; } /** * Generates a WordPress wp-i18n compatible translation JSON * * @param array $texts The texts extracted by wplng_i18n_script_extract_strings() * @param string $domain The text domain * @param string $locale The locale (e.g., 'fr_FR') * @param string $script_path Path to the JS script (for reference) * @return string Encoded JSON */ function wplng_i18n_script_generate_json( $texts, $domain = 'messages' ) { $locale = get_locale(); // Determine the plural form based on locale $plural_forms = wplng_get_language_plural_forms( $locale ); // Build the messages array $messages = array( '' => array( 'domain' => $domain, 'plural-forms' => $plural_forms, 'lang' => str_replace( '_', '-', $locale ), ), ); // Add __() translations foreach ( $texts['__'] as $item ) { $key = $item['text']; $text_domain = ! empty( $item['domain'] ) ? $item['domain'] : $domain; // Value = array with the translation $messages[ $key ] = array( __( $item['text'], $text_domain ) ); } // Add _x() translations with context foreach ( $texts['_x'] as $item ) { // Key with context: "text\u0004context" $key = $item['text'] . "\x04" . $item['context']; $text_domain = ! empty( $item['domain'] ) ? $item['domain'] : $domain; $messages[ $key ] = array( _x( $item['text'], $item['context'], $text_domain ) ); } // Add _n() translations (plurals) foreach ( $texts['_n'] as $item ) { $key = $item['singular']; $text_domain = ! empty( $item['domain'] ) ? $item['domain'] : $domain; // Array with [translated singular, translated plural] $messages[ $key ] = array( __( $item['singular'], $text_domain ), __( $item['plural'], $text_domain ), ); } // Add _nx() translations (plurals with context) foreach ( $texts['_nx'] as $item ) { $key = $item['singular'] . "\x04" . $item['context']; $text_domain = ! empty( $item['domain'] ) ? $item['domain'] : $domain; $messages[ $key ] = array( _x( $item['singular'], $item['context'], $text_domain ), _x( $item['plural'], $item['context'], $text_domain ), ); } // Build the complete structure $json_data = array( 'translation-revision-date' => gmdate( 'Y-m-d H:i:s+0000' ), 'generator' => 'wpLingua', 'domain' => $domain, 'locale_data' => array( 'messages' => $messages, ), ); return wp_json_encode( $json_data, JSON_UNESCAPED_UNICODE ); }