Skip to main content

Course Progress

Loading...

🌐 Translation-ready Themes

Create themes ready for global translation

Master POT files, translation workflows, and multilingual testing

Learning Objectives

  • Understand translation file formats (POT, PO, MO)
  • Generate POT files for your theme
  • Set up translation workflow
  • Use translation tools effectively
  • Test translations in your theme
  • Implement language switchers
  • Handle dynamic translations
  • Manage translation updates

Translation File Formats

WordPress uses the GNU gettext localization framework, which involves three types of files: POT (template), PO (translations), and MO (machine-readable).

Understanding Translation Files

languages/
├── my-theme.pot // Template file with all translatable strings
├── my-theme-de_DE.po // German translations (human-readable)
├── my-theme-de_DE.mo // German translations (machine-readable)
├── my-theme-fr_FR.po // French translations
├── my-theme-fr_FR.mo // French compiled
├── my-theme-es_ES.po // Spanish translations
├── my-theme-es_ES.mo // Spanish compiled
└── my-theme-ja.po // Japanese translations
    └── my-theme-ja.mo // Japanese compiled
  • POT (Portable Object Template): Contains all translatable strings from your theme
  • PO (Portable Object): Contains translations for a specific language
  • MO (Machine Object): Binary file compiled from PO for faster loading
  • JSON: For JavaScript translations (WordPress 5.0+)
💡
File Naming Convention
Translation files must follow the pattern: {text-domain}-{locale}.{extension}
Example: my-theme-de_DE.po for German (Germany)

Generating POT Files

Method 1: Using WP-CLI

# Install WP-CLI if not already installed
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp

# Generate POT file for your theme
wp i18n make-pot path/to/your/theme path/to/your/theme/languages/theme-name.pot

# With additional options
wp i18n make-pot . languages/my-theme.pot \
  --domain=my-theme \
  --exclude=node_modules,vendor,dist \
  --headers='{"Report-Msgid-Bugs-To":"https://example.com/support"}'

# Generate JSON files for JavaScript translations
wp i18n make-json languages/ --no-purge

Method 2: Using Grunt

// Gruntfile.js
module.exports = function(grunt) {
    grunt.initConfig({
        makepot: {
            target: {
                options: {
                    domainPath: '/languages',
                    exclude: [
                        'node_modules/.*',
                        'vendor/.*',
                        'dist/.*'
                    ],
                    mainFile: 'style.css',
                    potFilename: 'my-theme.pot',
                    potHeaders: {
                        'poedit': true,
                        'language-team': 'My Theme Team ',
                        'report-msgid-bugs-to': 'https://example.com/support',
                        'last-translator': 'Your Name '
                    },
                    type: 'wp-theme',
                    updateTimestamp: true,
                    processPot: function(pot, options) {
                        pot.headers['language'] = 'en_US';
                        return pot;
                    }
                }
            }
        },
        
        po2mo: {
            files: {
                src: 'languages/*.po',
                expand: true
            }
        }
    });
    
    grunt.loadNpmTasks('grunt-wp-i18n');
    grunt.loadNpmTasks('grunt-po2mo');
    
    grunt.registerTask('i18n', ['makepot', 'po2mo']);
};

Method 3: Using Gulp

// gulpfile.js
const gulp = require('gulp');
const wpPot = require('gulp-wp-pot');
const sort = require('gulp-sort');

gulp.task('pot', function () {
    return gulp.src('**/*.php')
        .pipe(sort())
        .pipe(wpPot({
            domain: 'my-theme',
            package: 'My Theme',
            headers: {
                'Report-Msgid-Bugs-To': 'https://example.com/support',
                'Language-Team': 'My Team ',
                'MIME-Version': '1.0',
                'Content-Type': 'text/plain; charset=UTF-8',
                'Content-Transfer-Encoding': '8bit'
            }
        }))
        .pipe(gulp.dest('languages/my-theme.pot'));
});

// Watch for changes
gulp.task('watch', function() {
    gulp.watch(['**/*.php'], gulp.series('pot'));
});

POT File Structure

# Copyright (C) 2024 My Theme
# This file is distributed under the GPL v2 or later.
msgid ""
msgstr ""
"Project-Id-Version: My Theme 1.0.0\n"
"Report-Msgid-Bugs-To: https://example.com/support\n"
"POT-Creation-Date: 2024-01-15 10:00:00+00:00\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"

#: 404.php:17
msgid "Oops! That page can't be found."
msgstr ""

#: archive.php:33
#, php-format
msgid "Author: %s"
msgstr ""

#: comments.php:35
msgid "Comments are closed."
msgstr ""

#. translators: %s: post title
#: comments.php:42
#, php-format
msgid "One thought on “%s”"
msgstr ""

#. translators: 1: comment count, 2: post title
#: comments.php:48
#, php-format
msgid "%1$s thoughts on “%2$s”"
msgstr ""

#: functions.php:125
msgctxt "post type general name"
msgid "Books"
msgstr ""

#: functions.php:126
msgctxt "post type singular name"
msgid "Book"
msgstr ""

Translation Tools

Poedit

Desktop application for translating PO files

  • Visual translation interface
  • Translation memory
  • Automatic MO compilation
  • WordPress integration
Desktop Free/Pro

Loco Translate

WordPress plugin for in-browser translation

  • Built into WordPress admin
  • No external tools needed
  • Automatic POT generation
  • Team collaboration
Plugin Free

WPML

Complete multilingual solution

  • Content translation
  • Theme/plugin translation
  • Language switcher
  • SEO features
Plugin Premium

Polylang

Multilingual content management

  • Multiple language support
  • Content translation
  • String translation
  • WooCommerce compatible
Plugin Free/Pro

GlotPress

WordPress.org translation platform

  • Web-based interface
  • Collaborative translation
  • Version control
  • Validation tools
Web Open Source

WP-CLI i18n

Command-line translation tools

  • POT generation
  • JSON file creation
  • Translation updates
  • Validation
CLI Free

Complete Translation Workflow

Step-by-Step Process

1. Prepare Theme

Internationalize all strings using WordPress i18n functions

// Wrap all text in translation functions
__( 'Read More', 'my-theme' );
_e( 'Search Results', 'my-theme' );
_n( '%s comment', '%s comments', $count, 'my-theme' );

2. Generate POT File

Extract all translatable strings into template file

wp i18n make-pot . languages/my-theme.pot --domain=my-theme

3. Create Translations

Translate strings using Poedit or similar tool

msgid "Read More"
msgstr "Lire la suite" # French translation

4. Compile MO Files

Generate binary files for WordPress to use

msgfmt languages/my-theme-fr_FR.po -o languages/my-theme-fr_FR.mo

5. Test Translations

Switch language and verify all strings are translated

// Change in wp-config.php
define( 'WPLANG', 'fr_FR' );

Testing Your Translations

Setting Up Test Environment

<?php
// wp-config.php - Switch languages for testing
define( 'WPLANG', 'de_DE' ); // German
// define( 'WPLANG', 'fr_FR' ); // French
// define( 'WPLANG', 'es_ES' ); // Spanish

// Force specific locale (useful for testing)
add_filter( 'locale', function() {
    return 'fr_FR';
});

// Debug translation loading
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', true );

// Check if translations are loading
add_action( 'after_setup_theme', function() {
    $loaded = load_theme_textdomain( 'my-theme', get_template_directory() . '/languages' );
    error_log( 'Theme translations loaded: ' . ( $loaded ? 'yes' : 'no' ) );
});

Language Switcher Implementation

<?php
// Simple language switcher (requires multilingual plugin)
function my_theme_language_switcher() {
    if ( function_exists( 'pll_the_languages' ) ) {
        // Polylang
        pll_the_languages( array(
            'show_flags' => 1,
            'show_names' => 1,
            'dropdown'   => 0,
        ) );
    } elseif ( function_exists( 'icl_get_languages' ) ) {
        // WPML
        $languages = icl_get_languages( 'skip_missing=0&orderby=code' );
        if ( ! empty( $languages ) ) {
            echo '<ul class="language-switcher">';
            foreach ( $languages as $l ) {
                $class = $l['active'] ? ' class="active"' : '';
                printf(
                    '<li%s><a href="%s"><img src="%s" alt="%s" /> %s</a></li>',
                    $class,
                    esc_url( $l['url'] ),
                    esc_url( $l['country_flag_url'] ),
                    esc_attr( $l['native_name'] ),
                    esc_html( $l['native_name'] )
                );
            }
            echo '</ul>';
        }
    } else {
        // Basic WordPress language switcher (4.7+)
        $languages = get_available_languages();
        if ( ! empty( $languages ) ) {
            echo '<form method="get" action="" class="language-switcher">';
            echo '<select name="lang" onchange="this.form.submit()">';
            echo '<option value="">' . __( 'Select Language', 'my-theme' ) . '</option>';
            foreach ( $languages as $lang ) {
                $selected = ( get_locale() === $lang ) ? ' selected' : '';
                printf(
                    '<option value="%s"%s>%s</option>',
                    esc_attr( $lang ),
                    $selected,
                    esc_html( $lang )
                );
            }
            echo '</select>';
            echo '</form>';
        }
    }
}

Translation Debugging Helper

<?php
// Debug helper to identify untranslated strings
class Translation_Debugger {
    private $untranslated = array();
    
    public function __construct() {
        add_filter( 'gettext', array( $this, 'check_translation' ), 10, 3 );
        add_filter( 'ngettext', array( $this, 'check_translation' ), 10, 3 );
        add_action( 'wp_footer', array( $this, 'display_untranslated' ) );
    }
    
    public function check_translation( $translated, $text, $domain ) {
        if ( $domain === 'my-theme' && $translated === $text ) {
            $this->untranslated[] = $text;
        }
        
        // Highlight untranslated strings (development only!)
        if ( WP_DEBUG && $translated === $text && $domain === 'my-theme' ) {
            return '🔴' . $translated . '🔴';
        }
        
        return $translated;
    }
    
    public function display_untranslated() {
        if ( WP_DEBUG && ! empty( $this->untranslated ) ) {
            echo '<!-- Untranslated strings: -->';
            echo '<!-- ' . implode( ', ', array_unique( $this->untranslated ) ) . ' -->';
        }
    }
}

// Enable in development
if ( WP_DEBUG ) {
    new Translation_Debugger();
}

Handling Dynamic Content

Translating Theme Options

<?php
// Register translatable theme options
function my_theme_register_strings() {
    if ( function_exists( 'pll_register_string' ) ) {
        // Polylang
        pll_register_string( 'my-theme', get_theme_mod( 'footer_text' ), 'Footer Text' );
        pll_register_string( 'my-theme', get_theme_mod( 'cta_button_text' ), 'CTA Button' );
    } elseif ( function_exists( 'icl_register_string' ) ) {
        // WPML
        icl_register_string( 'my-theme', 'footer_text', get_theme_mod( 'footer_text' ) );
        icl_register_string( 'my-theme', 'cta_button_text', get_theme_mod( 'cta_button_text' ) );
    }
}
add_action( 'after_setup_theme', 'my_theme_register_strings' );

// Get translated theme option
function my_theme_get_option( $option_name, $default = '' ) {
    $value = get_theme_mod( $option_name, $default );
    
    if ( function_exists( 'pll__' ) ) {
        // Polylang
        return pll__( $value );
    } elseif ( function_exists( 'icl_t' ) ) {
        // WPML
        return icl_t( 'my-theme', $option_name, $value );
    }
    
    return $value;
}

// Usage in template
echo esc_html( my_theme_get_option( 'footer_text', __( 'Default footer text', 'my-theme' ) ) );

JavaScript String Translation

<?php
// Generate JSON translation files for JavaScript
add_action( 'init', function() {
    wp_set_script_translations( 
        'my-theme-script', 
        'my-theme', 
        get_template_directory() . '/languages' 
    );
});

// In your JavaScript file
const { __ } = wp.i18n;

// Use translations in JavaScript
const message = __( 'Welcome to our site', 'my-theme' );
const plural = wp.i18n.sprintf(
    wp.i18n._n(
        '%d item in cart',
        '%d items in cart',
        cartCount,
        'my-theme'
    ),
    cartCount
);

Managing Translation Updates

Automated Translation Updates

<?php
// Enable automatic translation updates
add_filter( 'auto_update_translation', '__return_true' );

// Custom translation update check
function my_theme_check_translations() {
    $translations = wp_get_installed_translations( 'themes' );
    $theme_slug = get_option( 'stylesheet' );
    
    if ( isset( $translations[ $theme_slug ] ) ) {
        foreach ( $translations[ $theme_slug ] as $locale => $data ) {
            error_log( sprintf(
                'Translation found: %s (Version: %s)',
                $locale,
                $data['PO-Revision-Date']
            ) );
        }
    }
}
add_action( 'admin_init', 'my_theme_check_translations' );

// Update POT file when theme is updated
function my_theme_update_pot_file() {
    if ( class_exists( 'WP_CLI' ) ) {
        WP_CLI::runcommand( 'i18n make-pot ' . get_template_directory() . ' ' . 
            get_template_directory() . '/languages/my-theme.pot --domain=my-theme' );
    }
}
add_action( 'upgrader_process_complete', 'my_theme_update_pot_file', 10, 2 );

Translation-Ready Theme Checklist

  • Text domain declared in style.css header
  • Text domain loaded in functions.php
  • All static text wrapped in translation functions
  • Proper escaping with translation (esc_html__, esc_attr__)
  • Context provided for ambiguous strings (_x, _ex)
  • Plural forms handled correctly (_n, _nx)
  • Translator comments added where needed
  • No variables in translation strings
  • Date and number formatting uses i18n functions
  • JavaScript strings localized
  • POT file generated and included
  • Sample translations provided (optional)
  • RTL stylesheet created (if supporting RTL languages)
  • Language switcher implemented (for multilingual sites)
  • Documentation includes translation instructions
  • Theme tested with different languages
  • No hardcoded text in templates
  • Theme options are translatable
  • Email templates are translatable
  • Error messages are translatable

Multilingual Plugin Comparison

Feature WPML Polylang TranslatePress Weglot
Price $39-159/year Free/$99+ Free/$79+ Free/$99+/year
Content Translation
Theme/Plugin Strings ✅ Pro
Visual Editor
Automatic Translation ✅ Add-on ✅ Pro
SEO Features
WooCommerce ✅ Add-on
Performance Impact Medium Low Medium Low

Best Practices

Translation-Ready Best Practices

  • Keep POT file updated: Regenerate when adding new strings
  • Version your translations: Track changes in version control
  • Provide context: Use translator comments liberally
  • Test edge cases: Long translations, special characters
  • Consider text expansion: Allow 30-50% more space
  • Use professional translators: For production sites
  • Maintain consistency: Use glossaries for terms
  • Include sample data: Translated demo content
  • Document the process: Translation guide for contributors
  • Regular updates: Keep translations synchronized
Never trust automatic translations for production sites. Always have a native speaker review translations, especially for critical content like legal text or calls-to-action.
Use translation management platforms like Crowdin, POEditor, or Transifex for managing translations with multiple contributors.

Practice Exercise

💻
Create a Translation-Ready Theme

Make your theme fully translation-ready:

  1. Audit theme for hardcoded strings
  2. Wrap all text in appropriate i18n functions
  3. Add translator comments where needed
  4. Set up build process for POT generation
  5. Generate initial POT file
  6. Create sample translation (choose a language)
  7. Compile PO to MO file
  8. Test theme with translation active
  9. Implement language switcher
  10. Document translation process in README

Additional Resources