Skip to main content

Course Progress

Loading...

🌍 Theme Internationalization (i18n)

Make your WordPress themes globally accessible

Learn internationalization best practices for multi-language support

Learning Objectives

  • Understand internationalization (i18n) vs localization (l10n)
  • Master WordPress translation functions
  • Set up text domains properly
  • Handle plurals and context
  • Work with date and number formatting
  • Implement RTL (Right-to-Left) support
  • Use proper escaping with translation
  • Avoid common i18n mistakes

Understanding i18n and l10n

Internationalization (i18n) is the process of developing your theme so it can easily be translated into other languages. Localization (l10n) is the process of translating an internationalized theme.

💡
Key Concept
i18n = "internationalization" (18 letters between 'i' and 'n')
l10n = "localization" (10 letters between 'l' and 'n')

Why Internationalization Matters

  • Global Reach: WordPress powers 43% of the web globally
  • Accessibility: Makes your theme usable by non-English speakers
  • WordPress.org Requirements: Required for theme directory submission
  • Professional Standard: Expected in premium themes
  • SEO Benefits: Multilingual sites rank better in local searches

Setting Up Text Domain

Declaring Text Domain in style.css

/*
Theme Name: My Awesome Theme
Theme URI: https://example.com/themes/my-awesome-theme/
Author: Your Name
Author URI: https://example.com/
Description: A fully internationalized WordPress theme
Version: 1.0.0
License: GPL v2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: my-awesome-theme
Domain Path: /languages
*/

Loading Text Domain in functions.php

<?php
/**
 * Load theme text domain for translations
 */
function my_awesome_theme_setup() {
    /*
     * Make theme available for translation.
     * Translations can be filed in the /languages/ directory.
     * If you're building a theme based on My Awesome Theme, use a find and replace
     * to change 'my-awesome-theme' to the name of your theme in all the template files.
     */
    load_theme_textdomain( 'my-awesome-theme', get_template_directory() . '/languages' );

    // Add default posts and comments RSS feed links to head.
    add_theme_support( 'automatic-feed-links' );

    // Let WordPress manage the document title.
    add_theme_support( 'title-tag' );
}
add_action( 'after_setup_theme', 'my_awesome_theme_setup' );

/**
 * For child themes, load parent theme text domain
 */
function my_child_theme_setup() {
    // Load parent theme text domain
    load_theme_textdomain( 'parent-theme', get_template_directory() . '/languages' );
    
    // Load child theme text domain
    load_child_theme_textdomain( 'my-child-theme', get_stylesheet_directory() . '/languages' );
}
add_action( 'after_setup_theme', 'my_child_theme_setup' );
The text domain must match your theme's folder name and should use hyphens, not underscores. It should be unique to avoid conflicts with other themes and plugins.

WordPress Translation Functions

Function Description Usage
__() Returns translated string When you need to use the string in PHP
_e() Echoes translated string Direct output to HTML
_x() Translate with context When same text has different meanings
_ex() Echo with context Direct output with context
_n() Plural forms When text changes based on number
_nx() Plural with context Plural forms with context
esc_html__() Translate and escape HTML Safe HTML output
esc_html_e() Echo and escape HTML Direct safe HTML output
esc_attr__() Translate and escape attribute For HTML attributes
esc_attr_e() Echo and escape attribute Direct attribute output

Basic Translation Examples

<?php
// Simple translation - return
$text = __( 'Hello World', 'my-awesome-theme' );

// Simple translation - echo
_e( 'Welcome to our website', 'my-awesome-theme' );

// Translation with context
$comment_title = _x( 'Comment', 'noun', 'my-awesome-theme' );
$comment_action = _x( 'Comment', 'verb', 'my-awesome-theme' );

// Plural forms
$comment_count = get_comments_number();
printf(
    _n(
        '%s comment',
        '%s comments',
        $comment_count,
        'my-awesome-theme'
    ),
    number_format_i18n( $comment_count )
);

// Plural with context
printf(
    _nx(
        '%s post',
        '%s posts',
        $post_count,
        'post type general name',
        'my-awesome-theme'
    ),
    number_format_i18n( $post_count )
);

// Translation with escaping
echo esc_html__( 'User input text', 'my-awesome-theme' );

// Attribute translation with escaping
printf(
    '<input type="text" placeholder="%s" />',
    esc_attr__( 'Enter your name', 'my-awesome-theme' )
);

Advanced Translation Patterns

Working with Variables in Translations

<?php
// DON'T: Never break strings with variables
$text = __( 'Welcome ', 'my-awesome-theme' ) . $username . __( ' to our site', 'my-awesome-theme' );

// DO: Use placeholders
$text = sprintf(
    __( 'Welcome %s to our site', 'my-awesome-theme' ),
    $username
);

// Multiple placeholders with numbered arguments
$text = sprintf(
    /* translators: 1: user name, 2: site name */
    __( 'Welcome %1$s to %2$s', 'my-awesome-theme' ),
    $username,
    get_bloginfo( 'name' )
);

// HTML in translations (be careful!)
$text = sprintf(
    /* translators: %s: user name */
    __( 'Welcome <strong>%s</strong> to our site', 'my-awesome-theme' ),
    esc_html( $username )
);

// Date placeholders
$date_text = sprintf(
    /* translators: %s: publish date */
    __( 'Published on %s', 'my-awesome-theme' ),
    get_the_date()
);

// Link in translation
$privacy_text = sprintf(
    /* translators: %s: privacy policy link */
    __( 'By submitting, you agree to our %s', 'my-awesome-theme' ),
    '<a href="' . esc_url( get_privacy_policy_url() ) . '">' . 
    __( 'Privacy Policy', 'my-awesome-theme' ) . '</a>'
);

Translator Comments

<?php
// Add context for translators
/* translators: %s: search query */
$no_results = sprintf(
    __( 'No results found for "%s"', 'my-awesome-theme' ),
    get_search_query()
);

/* translators: 1: number of posts, 2: category name */
$category_description = sprintf(
    _n(
        '%1$s post in %2$s',
        '%1$s posts in %2$s',
        $post_count,
        'my-awesome-theme'
    ),
    number_format_i18n( $post_count ),
    single_cat_title( '', false )
);

// Complex example with multiple contexts
/* translators: Post meta information. 1: post date, 2: post author, 3: categories list, 4: tags list */
$meta_text = sprintf(
    __( 'Posted on %1$s by %2$s in %3$s. Tagged: %4$s', 'my-awesome-theme' ),
    get_the_date(),
    get_the_author(),
    get_the_category_list( ', ' ),
    get_the_tag_list( '', ', ', '' )
);

Localizing Dates and Numbers

Date and Time Localization

<?php
// Use WordPress date functions that respect locale
echo get_the_date(); // Uses date format from Settings
echo get_the_time(); // Uses time format from Settings

// Custom date formats with localization
$date = date_i18n( 
    get_option( 'date_format' ), 
    get_post_timestamp() 
);

// Relative time (human readable)
$time_diff = human_time_diff( 
    get_post_timestamp(), 
    current_time( 'timestamp' ) 
);
printf(
    /* translators: %s: time difference */
    __( '%s ago', 'my-awesome-theme' ),
    $time_diff
);

// Month names (localized)
$month = date_i18n( 'F', get_post_timestamp() ); // Returns localized month name

// Day names (localized)
$day = date_i18n( 'l', get_post_timestamp() ); // Returns localized day name

// Full localized date
$full_date = date_i18n(
    /* translators: date format, see https://www.php.net/date */
    __( 'F j, Y', 'my-awesome-theme' ),
    get_post_timestamp()
);

Number Formatting

<?php
// Format numbers according to locale
$views = 1234567;
echo number_format_i18n( $views ); // Output depends on locale: 1,234,567 or 1.234.567

// Decimals
$price = 1234.56;
echo number_format_i18n( $price, 2 ); // 1,234.56 or 1.234,56

// File sizes
$bytes = 1048576; // 1MB
echo size_format( $bytes ); // Outputs: 1 MB (localized)

// Percentages
$percentage = 0.754;
echo number_format_i18n( $percentage * 100, 1 ) . '%'; // 75.4% or 75,4%

// Currency (be careful with symbol placement)
$amount = 99.99;
$currency = sprintf(
    /* translators: %s: price amount */
    __( '$%s', 'my-awesome-theme' ),
    number_format_i18n( $amount, 2 )
);

// Ordinals (1st, 2nd, 3rd, etc.) - complex in many languages
function get_ordinal( $number ) {
    $locale = get_locale();
    
    if ( 'en_US' === $locale ) {
        $ends = array( 'th', 'st', 'nd', 'rd', 'th', 'th', 'th', 'th', 'th', 'th' );
        $mod100 = $number % 100;
        return $number . ( ( $mod100 >= 11 && $mod100 <= 13 ) ? 'th' : $ends[$number % 10] );
    }
    
    // Return plain number for other locales
    return $number;
}

RTL (Right-to-Left) Support

Detecting and Supporting RTL

<?php
// Check if current locale is RTL
if ( is_rtl() ) {
    // RTL-specific code
}

// In functions.php - enqueue RTL stylesheet
function my_awesome_theme_scripts() {
    wp_enqueue_style( 
        'my-awesome-theme-style', 
        get_stylesheet_uri(),
        array(),
        wp_get_theme()->get( 'Version' )
    );
    
    // RTL stylesheet (style-rtl.css)
    wp_style_add_data( 'my-awesome-theme-style', 'rtl', 'replace' );
}
add_action( 'wp_enqueue_scripts', 'my_awesome_theme_scripts' );

// Dynamic RTL classes
function my_awesome_theme_body_classes( $classes ) {
    if ( is_rtl() ) {
        $classes[] = 'rtl-language';
    }
    return $classes;
}
add_filter( 'body_class', 'my_awesome_theme_body_classes' );

RTL CSS (style-rtl.css)

/* Automatic RTL CSS generation tools:
 * - rtlcss: https://rtlcss.com/
 * - WordPress build tools include RTL generation
 */

/* Manual RTL overrides */
.site-header {
    text-align: right;
}

.nav-menu {
    float: right;
}

.nav-menu li {
    float: right;
}

/* Use logical properties (modern approach) */
.element {
    margin-inline-start: 20px; /* Instead of margin-left */
    margin-inline-end: 10px;   /* Instead of margin-right */
    padding-block-start: 15px; /* Instead of padding-top */
    padding-block-end: 15px;   /* Instead of padding-bottom */
}

/* Flip icons and images */
.arrow-icon {
    transform: scaleX(-1);
}

/* Directional icons should be flipped */
.breadcrumb-separator::before {
    content: '←'; /* Instead of → */
}

Arabic

ar
RTL Language

Hebrew

he_IL
RTL Language

Persian

fa_IR
RTL Language

Urdu

ur
RTL Language

JavaScript Localization

Localizing JavaScript Strings

<?php
// In functions.php
function my_awesome_theme_scripts() {
    wp_enqueue_script(
        'my-awesome-theme-script',
        get_template_directory_uri() . '/js/script.js',
        array( 'jquery' ),
        '1.0.0',
        true
    );
    
    // Localize script
    wp_localize_script(
        'my-awesome-theme-script',
        'myAwesomeThemeL10n', // Object name
        array(
            'loading'     => __( 'Loading...', 'my-awesome-theme' ),
            'readMore'    => __( 'Read More', 'my-awesome-theme' ),
            'readLess'    => __( 'Read Less', 'my-awesome-theme' ),
            'noResults'   => __( 'No results found', 'my-awesome-theme' ),
            'error'       => __( 'An error occurred', 'my-awesome-theme' ),
            'confirm'     => __( 'Are you sure?', 'my-awesome-theme' ),
            'success'     => __( 'Success!', 'my-awesome-theme' ),
            'copied'      => __( 'Copied to clipboard', 'my-awesome-theme' ),
            'shareText'   => __( 'Share this post', 'my-awesome-theme' ),
            'commentSingular' => __( '1 Comment', 'my-awesome-theme' ),
            'commentPlural'   => __( '%d Comments', 'my-awesome-theme' ),
            'isRTL'       => is_rtl(),
            'locale'      => get_locale(),
        )
    );
}
add_action( 'wp_enqueue_scripts', 'my_awesome_theme_scripts' );

// Alternative: wp_set_script_translations() for .json translation files
wp_set_script_translations( 
    'my-awesome-theme-script', 
    'my-awesome-theme',
    get_template_directory() . '/languages' 
);

Using Localized Strings in JavaScript

// script.js
(function($) {
    'use strict';
    
    // Access localized strings
    const l10n = myAwesomeThemeL10n;
    
    // Use translations
    $('.load-more').on('click', function() {
        const $button = $(this);
        $button.text(l10n.loading);
        
        // After loading
        $button.text(l10n.readMore);
    });
    
    // Handle plurals
    function updateCommentCount(count) {
        let text;
        if (count === 1) {
            text = l10n.commentSingular;
        } else {
            text = l10n.commentPlural.replace('%d', count);
        }
        $('.comment-count').text(text);
    }
    
    // RTL support
    if (l10n.isRTL) {
        // Adjust for RTL
        $('.slider').slick({
            rtl: true,
            // Other options
        });
    }
    
    // Locale-specific formatting
    function formatDate(date) {
        const options = { 
            year: 'numeric', 
            month: 'long', 
            day: 'numeric' 
        };
        return date.toLocaleDateString(l10n.locale.replace('_', '-'), options);
    }
    
})(jQuery);

Common i18n Mistakes to Avoid

What NOT to Do

<?php
// ❌ DON'T: Hardcode text
echo 'Welcome to our site';

// ✅ DO: Use translation functions
echo __( 'Welcome to our site', 'my-awesome-theme' );

// ❌ DON'T: Concatenate translated strings
echo __( 'Published on', 'my-awesome-theme' ) . ' ' . get_the_date();

// ✅ DO: Use placeholders
printf(
    __( 'Published on %s', 'my-awesome-theme' ),
    get_the_date()
);

// ❌ DON'T: Use variables in translation strings
__( $dynamic_text, 'my-awesome-theme' );

// ✅ DO: Use fixed strings with conditions
if ( $condition ) {
    $text = __( 'Option A', 'my-awesome-theme' );
} else {
    $text = __( 'Option B', 'my-awesome-theme' );
}

// ❌ DON'T: Include HTML tags unnecessarily
__( '<strong>Important:</strong> Check this', 'my-awesome-theme' );

// ✅ DO: Separate HTML when possible
echo '<strong>' . __( 'Important:', 'my-awesome-theme' ) . '</strong> ';
echo __( 'Check this', 'my-awesome-theme' );

// ❌ DON'T: Use __() with echo
echo __( 'Text', 'my-awesome-theme' );

// ✅ DO: Use _e() for echo
_e( 'Text', 'my-awesome-theme' );

// ❌ DON'T: Forget text domain
__( 'Translate me' );

// ✅ DO: Always include text domain
__( 'Translate me', 'my-awesome-theme' );

// ❌ DON'T: Translate URLs or technical strings
__( 'https://example.com', 'my-awesome-theme' );
__( 'post', 'my-awesome-theme' ); // Post type slug

// ✅ DO: Keep technical strings untranslated
$url = 'https://example.com';
$post_type = 'post';

Translation Workflow

Theme Translation Process

Step 1: Internationalize

Wrap all strings in translation functions

Step 2: Generate POT

Create .pot template file with all strings

Step 3: Translate

Translators create .po files for each language

Step 4: Compile

Generate .mo files from .po files

Step 5: Load

WordPress loads appropriate translation files

Best Practices

i18n Best Practices

  • Consistent text domain: Use the same text domain throughout
  • Meaningful context: Add translator comments for ambiguous strings
  • Complete sentences: Don't break sentences into parts
  • Avoid slang: Use clear, standard language
  • Gender neutrality: Avoid gender-specific language when possible
  • Test with long translations: German text is often 30% longer
  • Escape properly: Use esc_html__() and esc_attr__() functions
  • Consider cultural differences: Colors, icons, and imagery
  • Provide POT file: Include template for translators
  • Update translations: Keep translations in sync with code changes
Use tools like Poedit, Loco Translate, or WP-CLI to generate POT files and manage translations efficiently.

Practice Exercise

💻
Internationalize a Theme Component

Create a fully internationalized blog post template:

  1. Set up text domain in theme
  2. Internationalize all static text
  3. Handle plurals for comments
  4. Format dates according to locale
  5. Add translator comments
  6. Create RTL stylesheet
  7. Localize JavaScript strings
  8. Generate POT file
  9. Test with a different language
  10. Verify no hardcoded strings remain

Additional Resources