* @copyright 2026 Dobrzepostawieni.pl * @license GPL-3.0-or-later */ namespace Primeplaced_Logless_Analytics\Engine\Traits; // Prevent direct file access. if ( !defined( 'ABSPATH' ) ) { exit; } /** * Trait AssetLoader */ trait AssetLoader { /** * Reads the .asset.php file dynamically generated by Webpack and appends cache buster. * * @param string $asset_file_path Absolute path to the .asset.php file. * @param array $default_deps Fallback dependencies. * @return array{dependencies: array, version: string} Asset configuration. */ protected function get_webpack_asset_config( string $asset_file_path, array $default_deps = array('wp-element', 'wp-components', 'wp-i18n') ) : array { $config = array( 'dependencies' => $default_deps, 'version' => ( \defined( 'PPLA_VERSION' ) ? \PPLA_VERSION : '1.0.0' ), ); if ( \file_exists( $asset_file_path ) ) { $file_data = (require $asset_file_path); if ( \is_array( $file_data ) ) { $config['dependencies'] = $file_data['dependencies'] ?? $config['dependencies']; $config['version'] = (string) ($file_data['version'] ?? $config['version']); } } $config['version'] = $this->append_build_revision( $config['version'], $asset_file_path ); $config['version'] = $this->append_cache_buster_suffix( $config['version'] ); return $config; } /** * Appends one build-wide revision so entry assets and dynamic chunks refresh together. * * @param string $version Original asset version. * @param string $asset_file_path Absolute entry asset metadata path. * @return string Version including the trusted build revision when present. */ private function append_build_revision( string $version, string $asset_file_path ) : string { $asset_directory = \dirname( $asset_file_path ); $revision_files = array($asset_directory . '/ppla-build-revision.php', \dirname( $asset_directory ) . '/ppla-build-revision.php'); foreach ( $revision_files as $revision_file ) { if ( !\file_exists( $revision_file ) ) { continue; } $revision = (require $revision_file); if ( \is_string( $revision ) && 1 === \preg_match( '/^[a-f0-9]{8,64}$/', $revision ) ) { return $version . '-' . \substr( $revision, 0, 20 ); } } return $version; } /** * Appends the package edition so a plan change also refreshes cached assets. * * @param string $version The original version string from Webpack. * @return string The modified version string. */ private function append_cache_buster_suffix( string $version ) : string { $suffix = 'free'; return $version . '-' . $suffix; } /** * Configures script translations and JSON merging for provided handles. * * @param string|string[] $handles Single handle or array of script handles. */ protected function setup_translations( $handles ) : void { $handles = (array) $handles; $action = ( \is_admin() ? 'admin_enqueue_scripts' : 'wp_enqueue_scripts' ); \add_action( $action, function () use($handles) : void { foreach ( $handles as $handle ) { $this->set_script_translations_for_handle( $handle ); } }, 99 ); \add_filter( 'pre_load_script_translations', function ( $translations, $file, $handle, $domain ) use($handles) { if ( !\in_array( $handle, $handles, true ) || \PPLA_TEXTDOMAIN !== $domain ) { return $translations; } return $this->merge_json_translations_logic( $translations, $handle, $domain ); }, 10, 4 ); } /** * Binds translations to a specific script handle. * * @param string $handle The script handle. */ protected function set_script_translations_for_handle( string $handle ) : void { \wp_set_script_translations( $handle, \PPLA_TEXTDOMAIN, \plugin_dir_path( \PPLA_PLUGIN_ABSOLUTE ) . 'languages' ); } /** * Merges split JSON translation files generated by WP-CLI into a single translation object. * * @param string|false|null $translations The existing translations JSON string. * @param string $handle The current script handle. * @param string $domain The current text domain. * @return string|false|null Merged JSON string or original value on failure. */ protected function merge_json_translations_logic( $translations, string $handle, string $domain ) { $locale = \determine_locale(); $dir = \plugin_dir_path( \PPLA_PLUGIN_ABSOLUTE ) . 'languages'; $files = \glob( $dir . '/' . $domain . '-' . $locale . '-*.json' ); if ( !\is_array( $files ) || empty( $files ) ) { return $translations; } if ( !\class_exists( '\\WP_Filesystem_Direct' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; } $file_system = new \WP_Filesystem_Direct(null); $merged_messages = array(); $base_data = array(); foreach ( $files as $json_file ) { $data = $this->read_json_translation_file( $json_file, $file_system ); if ( empty( $data ) ) { continue; } if ( empty( $base_data ) ) { $base_data = $data; } $merged_messages = \array_merge( $merged_messages, $data['locale_data']['messages'] ); } if ( empty( $merged_messages ) ) { return $translations; } $base_data['locale_data']['messages'] = $merged_messages; $encoded = \wp_json_encode( $base_data ); return ( false !== $encoded ? $encoded : $translations ); } /** * Reads and decodes a local JSON translation file via WordPress Filesystem. * * @param string $file_path Absolute path to the JSON file. * @param \WP_Filesystem_Direct $file_system The filesystem instance. * @return array Decoded JSON data or empty array on failure. */ private function read_json_translation_file( string $file_path, \WP_Filesystem_Direct $file_system ) : array { $content = $file_system->get_contents( $file_path ); if ( empty( $content ) || !\is_string( $content ) ) { return array(); } $data = \json_decode( $content, true ); if ( !\is_array( $data ) || empty( $data['locale_data']['messages'] ) ) { return array(); } return $data; } }