Categorías
WordPress

Utilizar wp_localize_script() múltiples veces

La forma correcta de añadir dependencias del Front End en WordPress(en este caso, archivos JS), es registrándolas con la función wp_register_script($nombre, $ubicacion, ...) y posteriormente utilizar la función wp_enqueue_script($nombre) para imprimir en el DOM las etiquetas que importarán dichos archivos.

En ocasiones, querremos compartir variables de WordPress con nuestros archivos JavaScript. Por ejemplo, la API de traducción sólo está disponible en PHP, por lo que si queremos valernos de las traducciones en el Front, podemos utilizar el siguiente método.

WordPress ofrece una función llamada wp_localize_script($handle, $nombre_obj, $data_array), que permite inyectar variables globalmente accesibles en JavaScript. Supongamos que hemos registrado previamente un archivo JS llamado ‘main’, pues utilizaríamos la función así:

wp_localize_script(
    'main',
    'WP_DATA',
    array('string_to_translate'=>'translated_text')
);

Como podemos ver, ‘main’ es el identificador del script previamente registrado, ‘WP_DATA’ es el nombre del objeto accesible en JS y el último parametro es un array con los valores que deseamos exponer.

Esto quizás parecerá un poco confuso pero el resultado es bastante sencillo, lo único que hace dicha función es imprimir un script con el contenido que hemos específicado en la llamada a wp_localize_script(). El contenido estará asignado a una variable global con el nombre que hemos indicado.

Ahora, qué pasaría si utilizamos dicha función más de 1 vez con el mismo nombre de variable?

wp_localize_script(
    'main',
    'WP_DATA',
    array('string_to_translate'=>'translated_text')
);

wp_localize_script(
    'main',
    'WP_DATA',
    array('another_string_to_translate'=>'translated_text')
);

El resultado sería:

Sencillamente se vuelve a imprimir la variable con los nuevos valores. En caso de que repitamos un nombre de objeto, este sobrescribirá al previo, por lo que si queremos utilizar wp_localize_script() en diversas ocasiones para añadir más valores, tendremos que utilizar un diferente nombre de objeto para no sobrescribir los valores previos. Sin embargo, hay otra solución a la que llegué y que quiero compartir.

Analizando el código de wp_localize_script() (wp-includes\functions.wp-scripts.php), podemos ver que opera sobre una instancia de la clase WP_Scripts y llama al método localize.

/**
 * Localize a script.
 *
 * Works only if the script has already been added.
 *
 * Accepts an associative array $l10n and creates a JavaScript object:
 *
 *     "$object_name" = {
 *         key: value,
 *         key: value,
 *         ...
 *     }
 *
 * @see WP_Dependencies::localize()
 * @link https://core.trac.wordpress.org/ticket/11520
 * @global WP_Scripts $wp_scripts The WP_Scripts object for printing scripts.
 *
 * @since 2.2.0
 *
 * @todo Documentation cleanup
 *
 * @param string $handle      Script handle the data will be attached to.
 * @param string $object_name Name for the JavaScript object. Passed directly, so it should be qualified JS variable.
 *                            Example: '/[a-zA-Z0-9_]+/'.
 * @param array $l10n         The data itself. The data can be either a single or multi-dimensional array.
 * @return bool True if the script was successfully localized, false otherwise.
 */
function wp_localize_script( $handle, $object_name, $l10n ) {
	global $wp_scripts;
	if ( ! ( $wp_scripts instanceof WP_Scripts ) ) {
		_wp_scripts_maybe_doing_it_wrong( __FUNCTION__ );
		return false;
	}

	return $wp_scripts->localize( $handle, $object_name, $l10n );
}

/**
	 * Localizes a script, only if the script has already been added.
	 *
	 * @since 2.1.0
	 *
	 * @param string $handle      Name of the script to attach data to.
	 * @param string $object_name Name of the variable that will contain the data.
	 * @param array  $l10n        Array of data to localize.
	 * @return bool True on success, false on failure.
	 */
public function localize( $handle, $object_name, $l10n ) {
		if ( $handle === 'jquery' ) {
			$handle = 'jquery-core';
		}

		if ( is_array( $l10n ) && isset( $l10n['l10n_print_after'] ) ) { // back compat, preserve the code in 'l10n_print_after' if present.
			$after = $l10n['l10n_print_after'];
			unset( $l10n['l10n_print_after'] );
		}

		foreach ( (array) $l10n as $key => $value ) {
			if ( ! is_scalar( $value ) ) {
				continue;
			}

			$l10n[ $key ] = html_entity_decode( (string) $value, ENT_QUOTES, 'UTF-8' );
		}

		$script = "var $object_name = " . wp_json_encode( $l10n ) . ';';

		if ( ! empty( $after ) ) {
			$script .= "\n$after;";
		}

		$data = $this->get_data( $handle, 'data' );

		if ( ! empty( $data ) ) {
			$script = "$data\n$script";
		}

		return $this->add_data( $handle, 'data', $script );
	}

Siguiendo el rastro, el método localize de la clase WP_Scripts (wp-includes\class.wp-scripts.php), eventualmente llama al método add_data() el cual es heredado de la clase WP_Dependencies por WP_Scripts, así que para irnos al origen, inspeccionamos dicho método (wp-includes\class.wp-dependencies.php):

public function add_data( $handle, $key, $value ) {
	if ( ! isset( $this->registered[ $handle ] ) ) {
		return false;
	}

	return $this->registered[ $handle ]->add_data( $key, $value );
}

Con el código que hemos visto hasta ahora, sabemos que:

  • Hay una variable global en WordPress llamada wp_scripts, la cuál es una instancia de la clase WP_Scripts y se encarga de almacenar todas las dependencias registradas por wp_register_script() en su propiedad registered, la cuál es un array compuesto por instancias de la clase _WP_Dependency(wp-includes\class-wp-dependency.php).
  • En base a esto, sabemos que podemos acceder al objeto de una dependencia registrada de la siguiente forma: $wp_scripts->registered[$handle]

Ahora, volviendo al método add_data() de la clase WP_Dependencies, vemos que a su vez llama a otro método add_data() (el cuál es diferente, ya que el primero pertenece a WP_Dependencies y el segundo a la clase _WP_Dependency), encargado de vincular información extra que vendría siendo aquello que se termina imprimiendo en el DOM en la forma de variable global. Echémosle un vistazo a dicho método:

public function add_data( $name, $data ) {
	if ( ! is_scalar( $name ) ) {
	    return false;
	}
	$this->extra[ $name ] = $data;
        return true;
}

Observando este último método, ya tenemos una idea precisa de cómo funciona wp_localize_script() y procedo a explicarlo de forma más clara:

  • Existe una clase llamada _WP_Dependency, que representa una dependencia; es instanciada cada vez que llamamos a la función wp_register_script() y añadida a la propiedad registered del objeto global wp_scripts.
  • La clase _WP_Dependency posee una propiedad llamada extra, cuyo índice es asignado al nombre del objeto, que en nuestro ejemplo sería WP_DATA. Representa el array que queremos vincular a dicha dependencia y posteriormente añadir en el DOM en forma de objeto global de JS.
  • Por ende, si queremos utilizar multiples veces wp_localize_script() sin cambiar el nombre del objeto, lo que tenemos que hacer es concatenar el array previo con el array de información nueva y esto lo logramos de la siguiente forma:
global $wp_scripts;
$prevData = $wp_scripts->registered['main']->extra['data'];

Esto nos regresaría un String con la información en forma de objeto de JS. Por ejemplo: "var WP_DATA = {"string_to_translate":"translated_text"};"

Para poder manipular dicho objeto, tenemos que convertirlo a un array de PHP utilizando la función json_decode(). Sin embargo, primero debemos filtrar el string de forma tal que sólo nos quede la porción de interés, es decir, sin "VAR WP_DATA =" y el ";" al final.

global $wp_scripts;
$prevData = json_decode(str_replace(';', '', explode('var WP_DATA =', $wp_scripts->registered[$this->plugin_name]->extra['data'])[1]), true);

De esta forma, ya tendríamos el objeto en formato PHP y solo nos quedaría concatenarlo a nuestro nuevo array de información y localizarlo en nuestro script:

wp_localize_script($this->plugin_name, 'WP_DATA', array_merge(array('new_text_translation'=>'some_text'), $prevData));

Esto sería todo.