SponsoredHosinger Special Offer

WP WordPress Theme Development

Build classic and block WordPress themes from scratch: the template hierarchy, The Loop, the_title() and the_content(), template parts, menus, the Customizer, child themes and theme.json.

Making a Theme Translation-Ready

WordPress Theme Development Lesson 26 of 30 ~3 min read

Overview

A translation-ready theme wraps every visible string in a WordPress translation function with its text domain, so the theme can be shown in any language without changing its code.

Around half of all WordPress sites are not in English. Internationalisation — "i18n" — is not optional for a theme you plan to share.

Load the text domain

<?php
function gdc_starter_setup() {
    load_theme_textdomain( 'gdc-starter', get_template_directory() . '/languages' );
    // ...other setup.
}
add_action( 'after_setup_theme', 'gdc_starter_setup' );

Themes hosted on WordPress.org get translations automatically from translate.wordpress.org, but loading the folder lets you ship translations with the theme too.

The translation functions

FunctionUse for
__( $text, $domain )Returns a translated string.
esc_html__() / esc_attr__()Returns translated and escaped — prefer these.
esc_html_e() / esc_attr_e()Prints translated and escaped.
_x( $text, $context, $domain )Same word, different meaning: "Post" (verb) versus "Post" (noun).
_n( $single, $plural, $number, $domain )Plurals.
Watch out: The text domain must be a literal string, never a variable or constant. Translation tools scan your code as text and cannot follow GDC_DOMAIN to its value.

Placeholders: never concatenate

<?php
// Wrong: word order is fixed in English. Many languages put the name first.
echo esc_html__( 'Posted by ', 'gdc-starter' ) . esc_html( get_the_author() );

// Right: the whole sentence is one translatable string with a placeholder.
printf(
    /* translators: %s: post author name */
    esc_html__( 'Posted by %s', 'gdc-starter' ),
    esc_html( get_the_author() )
);

The /* translators: */ comment directly above appears in translation tools, telling the translator what %s will become. With two or more placeholders, number them — %1$s, %2$s — so translators can reorder them.

Plurals

<?php
$count = (int) get_comments_number();

printf(
    /* translators: %s: number of comments */
    esc_html( _n( '%s comment', '%s comments', $count, 'gdc-starter' ) ),
    esc_html( number_format_i18n( $count ) )
);

Never write if ( 1 === $count ) yourself. Some languages have three or more plural forms — Polish uses different words for 2 and 5 — and _n() handles all of them.

Generating the .pot file

Translators work from a .pot template listing every string. Generate it with WP-CLI from the theme folder:

wp i18n make-pot . languages/gdc-starter.pot --domain=gdc-starter

Translators open the .pot in a tool such as Poedit, translate it, and produce a .po and compiled .mo file named after the locale — gdc-starter-fr_FR.mo for French — which go in languages/.

Right-to-left languages

Arabic and Hebrew read right to left. Add an rtl.css file and WordPress loads it automatically after style.css when the site language is RTL. Better still, write CSS with logical properties — margin-inline-start instead of margin-left — and most of the layout flips by itself.

Practice

  • Wrap every visible string in GDC Starter with the correct function and the gdc-starter domain.
  • Replace any concatenated sentence with printf() and a translators comment.
  • Generate the .pot file and open it to check every string appears.
Create a free account to save which lessons you've finished. Save my progress