Whether you’re developing a plugin, designing a custom theme, or managing content for a global audience, preparing your entire codebase for international use goes far beyond simple string translations.
At WordCamp Europe, my session “Multilingual WordPress for Developers” broke it all down with best practices, code samples, and tips that developers can actually use. Watch the video or check out the simplified recap below featuring the key points—with code included.
First: What the Heck Is i18n and L10n?
You’ve probably seen these cryptic codes — i18n, L10n, maybe even g11n or a11y.
They stand for:
- i18n = Internationalization (18 letters between “i” and “n”)
- L10n = Localization
- g11n = Globalization
- a11y = Accessibility
- p13n = Personalization
The difference?
Internationalization (i18n) is our job as devs — it’s about preparing your code so it can be translated.
Localization (L10n) is where the magic happens — translators (or tools) actually adapt the content for different languages and cultures.
Making Your Code Translatable (The Right Way)
WordPress gives us a set of i18n functions that help make strings translatable. These fall into three categories, dubbed:
The Good:
Use these. Learn them. Love them.
- __() — the classic: returns a translated string
- _x() — adds context (like “Save” as in “File Save” vs. “Save a Puppy”)
- _n() — plural support (1 comment vs. 2 comments)
<?php
$translated_string = __(
'Demo__ Text',
'multilingual-wp4devs'
);
$translated_string_with_context = _x(
'Demo_x Text',
'Demo Context',
'multilingual-wp4devs'
);
$stars = wp_rand( 1, 5 );
_n(
'%d star',
'%d stars',
$stars,
'multilingual-wp4devs'
);
The Bad:
- _e() and _ex() — they echo strings directly… but without escaping. Bad idea.
- They work, but don’t use these in production unless you escape the output manually.
<?php
// phpcs:ignore WordPress.Security.EscapeOutput.UnsafePrintingFunction
_e(
'Demo_e Text',
'multilingual-wp4devs'
);
// phpcs:ignore WordPress.Security.EscapeOutput.UnsafePrintingFunction
_ex(
'Demo_x Text',
'Demo Context',
'multilingual-wp4devs'
);
The Ugly (but Safe):
These functions combine translation + escaping:
- esc_html__(), esc_attr__()
- esc_html_x(), esc_attr_x()
<?php
echo esc_html__( 'Demo__ Text', 'multilingual-wp4devs' );
echo esc_attr__( 'Demo__ Text', 'multilingual-wp4devs' );
esc_html_e( 'Demo_e Text', 'multilingual-wp4devs' );
esc_attr_e( 'Demo_e Text', 'multilingual-wp4devs' );
echo esc_html_x(
'Demo_x Text',
'Demo Context',
'multilingual-wp4devs'
);
echo esc_attr_x(
'Demo_x Text',
'Demo Context',
'multilingual-wp4devs'
);
Yes, they’re wordy. But they keep your output safe and your code solid.
Help Translators Help You
A good translation isn’t just about the string. It’s about context.
Use translator comments:
<?php
$stars = wp_rand( 1, 5 );
$stars_str = sprintf(
// translators: %d is an integer for a star-rating.
_n( '%d star', '%d stars', $stars, 'multilingual-wp4devs' ),
$stars
);
$demo_str = sprintf(
// translators: %1$s can be any character, %2$s is another placeholder string.
__( 'Demo Content - %1$s and %2$s', 'multilingual-wp4devs' ),
'A',
'B'
);
Without that comment, a translator might not know what those placeholders mean, and that leads to awkward or incorrect translations, especially in languages with different sentence structures.
Translation Files: Behind the Scenes
So you’ve wrapped your strings and added context. Great! But how do those translations actually make it from your code to the screen?
Let’s take a closer look at the different translation file types, what each one does, and why they matter.
| File | What It Does |
| .pot | Template — extract of strings |
| .po | Editable translation file |
| .mo | Binary version for performance |
| .l10n.php | New in WP 6.5 — PHP translation file, faster via OPcache |
| .json | JavaScript translations (JED format) |
Pro Tip: Use WP-CLI to generate and validate translation files:
# Extract JavaScript strings from PO file
# and add them to individual JSON files.
wp i18n make-json
# Create MO files from PO files.
wp i18n make-mo
# Create PHP files from PO files.
wp i18n make-php
# Create a POT file for a WordPress project.
wp i18n make-pot
# Update PO files from a POT file.
wp i18n update-po
Text Domains: The Namespace of Translation
Every plugin or theme should define a text domain. This is how WordPress knows which translations to load.
Load it manually in your plugin:
<?php
/**
* The hook 'init' should be used to load the plugin's translation files.
*
* Don't use 'plugins_loaded' since it will create a warning!
*/
add_action(
'init',
static function () {
load_plugin_textdomain(
'multilingual-wp4devs',
false,
__DIR__ . '/languages'
);
}
);
Or let WordPress do it automatically, if:
- Your plugin is on WordPress.org (since WP 4.6)
- Or you’ve defined Text Domain and Domain Path headers (since WP 6.8)
Don’t Forget JavaScript!
Modern WordPress = modern JavaScript. That means your JS needs translations too.
Good news: WordPress has your back with the @wordpress/i18n package. It marks a string for translation and retrieves the translated version.
import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
registerBlockType( 'lloc/multilingual-wp4devs', {
apiVersion: 3,
title: __( 'Simple Block', 'multilingual-wp4devs' ),
category: 'widgets',
edit: () => {
return <p>{ __( 'Hello Editor', 'multilingual-wp4devs' ) }</p>;
},
save: () => {
return <p>{ __( 'Dear Reader', 'multilingual-wp4devs' ) }</p>;
},
} );
User Settings: The Invisible Multilingual Layer
Internationalization isn’t just about words. Think:
- Date formats
- Number formats
- Timezones
- Currency
WordPress lets users pick a language, and site admins set locale preferences in Settings.
Respect them by using:
<?php
$date = date_i18n(
get_option( 'date_format' )
);
echo esc_html( $date );
$timestamp = mktime( 13, 42, 23, 7, 17, 2025 );
$date = date_i18n(
get_option( 'date_format' ),
$timestamp
);
echo esc_html( $date );
This ensures the date looks right for each user, no matter their language or region.
Architecture: How You Structure a Multilingual Site Matters
WordPress doesn’t (yet) offer multilingual support in core, so it’s up to you (and your favorite tools) to decide how to handle it.
Here are the three most common approaches developers use today:
| Approach | Description | Pros | Cons |
| Single Site | Everything in one install, with translations stored together | ✅ Simple, unified ✅ One dashboard | ❌ Can get messy ❌ Plugin/vendor lock-in |
| Multisite | Each language gets its own site (great for separation + scale) | ✅ Clean separation ✅ Scalable | ❌ More complex to manage ❌ Syncing content |
| SaaS | External translation service, like Weglot or TranslatePress | ✅ Fast setup ✅ Auto-translate | ❌ Less control ❌ Ongoing costs |
If you’re leaning toward the Multisite model, this is where tools like the MultilingualPress plugin by Syde really shine.
MultilingualPress connects separate sites in a multisite network, managing language relationships and offering deep integration with WordPress, all without duplicating content or forcing rigid workflows. It’s a solid choice for developers who want scalability, flexibility, and performance.
Pro tip: Do a quick SWOT analysis (strengths, weaknesses, opportunities, threats) for each approach before committing. Think not just about launch day, but maintenance and growth two or three years down the line.
Multilingual WordPress Best Practices Recap
Here’s what to do next:
- Wrap every string from the beginning
- Escape translated output — don’t assume it’s safe
- Use translator comments to avoid confusion
- Support both PHP and JS translations
- Choose your architecture wisely
- Test with different languages, scripts, and locales
Ready to Go Multilingual?
If you want to dive deeper, experiment with real code, or just see how all the pieces fit together, we’ve got everything you need to get started.
A companion plugin with all the examples from this session is available on GitHub. It includes practical samples for preparing strings, structuring your plugin for multilingual use, handling JS translations, and safely working with user-facing content. You can access it here, or head over to @realloc to explore, test locally, or even fork it for your own projects.
Looking to understand the internals?
- All of WordPress’s PHP i18n functions live in wp-includes/l10n.php.
- For JavaScript, the @wordpress/i18n package gives you everything you need to internationalize modern front-end code — and it’s fully supported in the block editor.
And if you’re working on a multilingual site at scale, or need help planning the right architecture, check out Syde. We’re Europe’s largest WordPress agency and the creators of MultilingualPress, the powerful plugin that brings true multilingual support to WordPress Multisite. We also partner with businesses and agencies to build custom multilingual solutions that are scalable, secure, and editor-friendly. From strategy to implementation, we can help you do multilingual WordPress the right way.
Make Your Site Multilingual

Related articles
-
Matt Mullenweg on AI, the Open Web, and the Future of WordPress
At WordCamp US 2025 in Portland, Oregon, Jade Englebrecht, Partnership Manager at Syde had the chance to sit down with Matt Mullenweg, co-founder of WordPress and CEO of Automattic.
-
5 Hidden WordPress Performance Traps Costing You Conversions
Enterprise WordPress sites can look fast while quietly costing you revenue. This guide shows you how to avoid those hidden performance traps.
