🌍 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
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