🌐 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
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
Loco Translate
WordPress plugin for in-browser translation
- Built into WordPress admin
- No external tools needed
- Automatic POT generation
- Team collaboration
WPML
Complete multilingual solution
- Content translation
- Theme/plugin translation
- Language switcher
- SEO features
Polylang
Multilingual content management
- Multiple language support
- Content translation
- String translation
- WooCommerce compatible
GlotPress
WordPress.org translation platform
- Web-based interface
- Collaborative translation
- Version control
- Validation tools
WP-CLI i18n
Command-line translation tools
- POT generation
- JSON file creation
- Translation updates
- Validation
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