The Publishing Project

Reviewing Lazy Loading in WordPress

In the not too distant future, WordPress will have lazy loading by default in WordPress core enabled by default. I believe that this will be merged into WordPress core for the 5.5 release; until then, development is done on a feature plugin for people to continue working on it.

But the problem is that this is enabled by default. As with many of the later decisions by the WordPress core team, this is good for beginners but I can think of at least two instances where lazy loading is not what I want to use:

  • Header images at the top of the page. If we want to minimize the load time then we shouldn’t lazy load them
  • Images and iframes above the fold that the user will see when the page first loads

There are several plugins that already do image and iframe lazyloading in my WordPress installation I have decided to use Jetpack and W3 Total Cache so I won’t lose lazy loading if I deactivate it in core.

Another thing to keep in mind is how does lazy loading affects existing image manipulation plugins such as Cloudinary?

I was thinking about enabling Jetpack’s lazy loading feature but while researching it I realized that it uses data-src attributes to handle lazy loading so I don’t know how it will interact with Cloudinary and the srcset attribute.

W3 Total Cache’s documentation and support leave a lot to be desired for the free version

Incorporating lazy loading without core

The first thing to do if you want to keep your code working as before is to disable lazy loading in WordPress Core with the wp_lazy_loading_enabled filter.

add_filter( 'wp_lazy_loading_enabled', '__return_false' );

We can then alter the content that gets inserted into the (classic) editor and will be published with the post.

The function will take the image from the attachment and the HTML from the image tag and use it to build a figure element with captioning, and alt attributes built from the content of the attachment page.

It also adds lazy loading that you can modify or remove based on your needs. As far as I know, you can’t remove the attribute once it’s been added to the image.

<?
function html5_insert_image($html, $id, $caption, $title, $align, $url, $size, $alt) {
  $src  = wp_get_attachment_image_src( $id, $size, false );
  $html = get_image_tag($id, '', $title, $align, $size);
  $html5 = "<figure>";
  $html5 .= "  <img src='$url' alt='$alt' class='size-$size' loading='lazy' />";
  if ($caption) {
    $html5 .= "  <figcaption class='wp-caption-text'>$caption</figcaption>";
  }
  $html5 .= "</figure>";
  return $html5;
}
add_filter( 'image_send_to_editor', 'html5_insert_image', 10, 9 );

The drawback of this method is that it only handles new images when using the classic editor, it will not go back and retroactively add the loading attribute to images already used in posts, if you need that done you’ll have to do it manually.

Good or Bad?

Whether native lazy loading is good or bad depends on what you need and what you’re using for image management and lazy loading (if anything). Having lazy loading in the core is not the only way to lazy load images, just what appears to be the most convenient because people just want things to work.

If you already use lazy loading from other sources you have to test carefully how they interact with your code and any image manager that you have installed.

Inline scripts in WordPress

There are times when we need to add scripts to the page that will run only when there is a script already enqueued and loaded before it happens.

I got bit by this when trying to use FontFaceObserver. For some reason, it worked fine in development but it would give a FontFaceObserver is not defined error when I moved the theme to production.

The solution was hidden in some older documentation. As of version 4.5 there is an additional script loading function for loading inline scripts: wp_add_inline_script. This function allows adding inline scripts that depend on scripts

The only important thing to remember is that this will only work when using a script that was enqueued using wp_enqueue_script.

The final solution looks like this:

function rivendellweb_enqueue_fontfaceobserver() {
  wp_enqueue_script( 'ffo_script',
    get_stylesheet_directory_uri() . '/js/fontfaceobserver.js',
    array(),
    '20151215',
    false );

   wp_add_inline_script( 'ffo_script', 'const recursive = new FontFaceObserver("Recursive VF");let html = document.documentElement;
     Promise.all([recursive.load()]).then(() => {sessionStorage.fontsLoaded = true;console.log("Recursive has loaded.");
     }).catch((err) => {sessionStorage.fontsLoaded = false;console.log("Recursive failed to load", err);
     });

     // Add a class based on whether the font loaded successfully
     if (sessionStorage.fontsLoaded) {html.classList.add("fonts-loaded");
     } else {html.classList.add("fonts-failed");}'
   );
}

add_action( 'wp_enqueue_scripts', 'rivendellweb_enqueue_fontfaceobserver' );

Links and References

Using ES modules in Node

Node has had experimental support for EcmaScript modules for a while.

Again, this is one thing I’ve wanted to explore for a while but have never felt the need to dig deeper. After all, it is experimental and it hasn’t been approved for production use.

But wit the release of Node 14 we’re coming to the point when module support moved to stable.

So let’s explore what it takes to run ES Modules in Node on their own and together with current Node modules.

What will Node consider an ES Module?

There are certain file extensions and conditions that will cause Node to treat files as ES Modules. These conditions/extensions include:

  • Files ending in .mjs
  • Files ending in .js when the nearest parent package.json file contains a top-level field "type": "module"
  • Files ending in .js when Node runs with the --experimental-modules flag (versions of Node before 14.x)
  • Strings passed in as an argument to --eval or --print, or piped to node via STDIN, with the flag --input-type=module

So, in theory, we could use .mjs for all our ES Modules files but we need to be careful as your server needs to be configured to serve .mjs files as Javascript and I’m not certain all servers are configured to do this out of the box.

File extensions

Two extensions have special meaning. As we discussed earlier in this post, .mjs will always be treated as an ES Module file

import 'commonjs-package/src/index.mjs';
// Loaded as ES module since .mjs is always 
// loaded as ES module. 

The .cjs extension is the opposite. This file will always load as a Common JS module.

import './legacy-file.cjs';
// Loaded as CommonJS since .cjs is always 
// loaded as CommonJS.

Running Node with module support

As of Node 12.16.3, the latest LTS Node version as of this writing, the command to run Node with experimental Node Module support is:

node --experimental-modules colors.js

Using this flag we’ll be able to run the same code both on Node and on the browser.

If you’re interested, keep an eye for announcements about modules in Node. This may change in unexpected ways so check the documentation and be ready.

Links and Resources

Internationalizing WordPress Themes and Plugins (part 2)

Internationalizing a plugin

After setting up the text domain in your plugin metadata we need to load the translated files to use them.

For plugins we use the load_plugin_textdomain function; it takes 4 parameters:

  1. The name of text domain
  2. This parameter is deprecated so always use FALSE
  3. The path to the directory where your plugin’s translations are stored

Then add an action for the plugins_loaded hook and call the function we just created.

<?
function rivendellweb_load_plugin_textdomain() {
  load_plugin_textdomain(
    'my-demo-plugin', // 1
    FALSE,  // 2
    basename(  // 3
      dirname( __FILE__ ) ) . '/languages/'
  );
}
add_action( 'plugins_loaded', 'rivendellweb_load_plugin_textdomain' );

See How to Internationalize Your Plugin for more information about plugin internationalization.

Internationalizing a theme

Internationalizing themes is slightly different than plugins. The function we call is load_theme_textdomain.

The function takes two parameters:

  1. The text domain identifier for the theme we want to use
  2. The location of the language file
<?php
function rivendellweb_load_theme_textdomain() {
    load_theme_textdomain( 'my-demo-theme', get_template_directory() . '/languages' );
}
add_action(
  'after_setup_theme',
  'rivendellweb_load_theme_textdomain'
);

Internationalization

Internationalizing Javascript

Note

We’ve already covered the basics of Javascript i18n in part 3 of my Building Gutenberg Blocks series.

We’ll just revisit some of the most important details here.

At this time the only use I see for JavaScript internationalization in WordPress is for Gutenberg blocks.

wp.i18n provides a subset of the Gettext localization functions discussed earlier.

  • __( 'Hello World', 'my-text-domain' ) – Translate a string
  • _n( '%s Comment', '%s Comments', numberOfComments, 'my-text-domain' ) – Translate and retrieve the singular or plural form based on the supplied number.
  • _x( 'Default', 'block style', 'my-text-domain' ) – Translate a certain string with additional context.

This example provides a basic internationalize Gutenberg block. Since we’re working with React, our process has a build system that will convert the import statements into something older browsers can use.

import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';

registerBlockType( 'myguten/simple', {
  title: __( 'Simple Block', 'myguten' ),
  category: 'widgets',

  edit: () => {
    return (
      <p style="color:red">
        { __( 'Hello World', 'myguten' ) }
      </p>
    );
  },

  save: () => {
    return (
      <p style="color:red">
        { __( 'Hello World', 'myguten' ) }
      </p>
    );
  },
} );

This will not work with external scripts. For that we need to use wp_localize_script

The function will only work with scripts we’ve enqueued to the system using wp-enqueue-script.

wp_enqueue_script(
  'rivendellweb-navigation',
  get_template_directory_uri() . '/js/navigation.js',
  array('jquery'), '20151215', true
);

wp_localize_script takes three attributes:

  • The name of the enqueued script
  • The name of the javascript object we want to work with
  • An array of internationalized objects
wp_localize_script( 'rivendellweb-navigation',
  'rivendellwebScreenReaderText',
  array(
    'expand' => __( 'Expand child menu', 'rivendellweb'),
    'collapse' => __( 'Collapse child menu', 'rivendellweb'),
  )
);

Additional considerations: RTL languages

If you’re planning on distributing your theme and plugin there’s one final consideration that I want to mention on this post; right to left language and how much our designs need to change to accommodate those languages.

Compare the next two images. The first one is in English, left to right, top to bottom language. The second one is in Arabic, a right to left, top to bottom language.

English version of the United Nations Websie

 

Arabic version of the United Nations Website

English and Arabic versions of the United Nations Website www.un.org

The content flows differently and we should take these languages into account when deciding on margin and padding for our content.

Because we don’t know where our themes and plugins will be used we need to keep these differences in mind and provide some level of support for RTL languages.

You can convert your stylesheets to work with RTL languages manually or using tools like gulp-rtlcss

Internationalizing WordPress Themes and Plugins (part 1)

According to GALA (Globalization and Localization Alliance):

Internationalization is a design process that ensures a product (usually a software application) can be adapted to various languages and regions without requiring engineering changes to the source code. Think of internationalization as readiness for localization…

Some practical examples of how internationalization is critical to multilingual products include:

  • Independence from a specific language/character set encoding
  • Independence from specific cultural conventions
  • Removal of hard-coded text
  • Minimization of concatenated text strings
  • Careful use of in-line variables
  • Compatibility with third-party tools
  • Unicode compliance for global text display
  • Accommodation of double-byte languages (for example, Japanese)
  • Accommodation of right-to-left languages (for example, Arabic)

In the context of WordPress, internationalization means at least two things:

  • Ensuring that the text in our themes and plugins is ready for localization
  • Making sure that we consider right-to-left languages

The strategies for internationalizing plugins and themes are slightly different so we’ll cover them separately.

Internationalization tools in WordPress

WordPress translation infrastructure is built on top of GNU Gettext so that’s a good starting point for research. The rest of he post is WordPress Specific.

Text Domain

The text domain provides WordPress with a unique identifier for our plugin or theme. This is important because WordPress will have many plugins and a theme to sort through so having unique names makes things easier.

In a theme, the text domain and the domain path, the location of our translated files, is placed on the root CSS style sheet. For our my-demo-theme theme this is placed in a comment inside style.css.

/*
* Theme Name: My Theme
* Author: Theme Author
* Text Domain: my-demo-theme
* Domain Path: /languages
*/

For plugins, the Text Domain and Domain path are placed in a comment on the root PHP file, either index.php or the root of the plugin code.

/*
 * Plugin Name: My Plugin
 * Author: Plugin Author
 * Text Domain: my-demo-plugin
 * Domain Path: /languages
 */

i18n functions

There are several i18n functions available for PHP internationalization

The most basic one is __() that will just translate its content.

<?php
__('Hello World', 'my-demo-theme');

Note that it will not echo the result, you have to use a different function (_e()), discussed next.

_e() is similar to the previous example but it also displays the output to the page.

<?php
_e('Hello World', 'my-demo-theme');

The next one, _x(), is similar to __() but it also provides a context to help translators. You could do a comment before the item being translated but this will help systems like Polyglots do a better job.

The function takes three arguments:

  • The string to translate
  • The context for the translation
  • The text domain
_x( 'Read', '
    past participle: books I have read',
    'my-demo-theme'
);

In this example, the word read, on its own, has multiple meanings and the translator will not be able to get the context right away so adding the context helps with the translation.

The _ex() function is a combination of _e() and _x().

_ex( 'Read', '
    past participle: books I have read',
    'my-demo-theme'
);

The next block is for strings that should be pluralized. The first one is _n() and it takes a singular term, a plural term, their definition, and the text-domain.

<?php
people = sprintf(
  _n(
    '%s person', // Singular Form
    '%s people', // Plural Form
    $count, // Number to compare to
    'my-demo-theme' // Text Domain
  ),
  number_format_i18n( $count )
  // Converts number to
  // locale appropriate version
);

The second parameter to sprintf() formats the number we produce into something appropriate to the locale the function is being called from.

_nx() is a combination of _n() and _x() in that it provides a way to pluralize content and the context necessary for translators.

<?
$people = sprintf( _nx(
    '%s person', // Singular
    '%s people', // Plural
    $count, // Number to compare
    'context', // Context
    'my-demo-theme' // Text Domaain
  ),
  number_format_i18n( $count )
  // Converts number to
  // locale appropriate version
);

THe final block of functions are equivalent to _n_noop() and_nx_noop() keep structures with translatable plural strings and use them later when the value is known.

Once you’re ready to process the noop functions, you call translate_nooped_plural()

<?
printf(
  translate_nooped_plural(
    $people,
    $count,
    'my-demo-theme'
  ),
  number_format_i18n( $count )
);

Escaping HTML

Three of the functions have equivalent versions that will escape any HTML In their values, rendering the string safe to use in HTML attributes.

The following code will use the translation for Hello World available in the theme represented by my-demo-theme and escape the translated string to render HTML-specific characters safe.

<p><?php esc_html_e(
  'Hello World!',
  'my-demo-theme'
); ?></p>

Variables

What if you have a string like the following:

<?php
echo 'Hello $city.'

$city is a variable and should not be translated as such. The solution is to use placeholders for the variable, along with the printf family of functions. Especially helpful are printf and sprintf. Here is what one solution looks like:

<?php
$city = Sao Paulo;

printf(
  /* translators: %s: Name of a city */
  __( 'Your city is %s.', 'my-plugin' ),
  $city
);

Notice that here the string for translation is just the template “Your city is %s.”, which is the same both in the source and at run-time.

Also, note that there is a hint for translators so that they know the context of the placeholder.

Argument swapping

If you have more than one placeholder in a string, it is recommended that you use argument swapping.

With argument swapping, you must use single quotes (‘) around the string because double quotes (“) will cause PHP to interpret the $s as the s variable, which is not what we want.

In the following example, the first substitution is the name of a city and the second is the zip code.

<?php
printf(
  /* translators: 1: Name of a city 2: ZIP code */
  __( 'Your city is %1$s, and your zip code is %2$s.', 'my-plugin' ),
  $city,
  $zipcode
);

This will work for most western languages. However, for some languages displaying the zip code and city in opposite order would be more appropriate.

Using %s prefix in the above example allows for such a case. A translation can thereby be written:

The modified example changes the order of the variables without changing their meaning.

<?php
printf(
  /* translators:
    1: Name of a city
    2: ZIP code */
  __( 'Your zip code is %2$s, and your city is %1$s.', 'my-plugin' ),
  $city,
  $zipcode
);