Developing a multi-language WordPress theme without plugins requires implementing internationalization (i18n) and localization (l10n) directly in your theme code. Here’s a comprehensive approach:
1. Theme Structure Setup
First, organize your theme with language support in mind:
your-theme/
├── languages/
│ ├── theme-name.pot
│ ├── theme-name-es_ES.po
│ ├── theme-name-es_ES.mo
│ └── theme-name-fr_FR.po
├── functions.php
├── index.php
└── other theme files…
2. Enable Text Domain in functions.php
php
function theme_setup() {
// Make theme available for translation
load_theme_textdomain(‘theme-name’, get_template_directory() . ‘/languages’);
// Add language switcher to admin bar
add_theme_support(‘customize-selective-refresh-widgets’);
}
add_action(‘after_setup_theme’, ‘theme_setup’);
3. Internationalize Theme Strings
Wrap all translatable strings in your theme files:
php
// Instead of: echo “Hello World”;
echo __(‘Hello World’, ‘theme-name’);
// For attributes
echo ‘<input placeholder=”‘ . esc_attr__(‘Enter your name’, ‘theme-name’) . ‘”>’;
// For pluralization
printf(_n(‘One comment’, ‘%s comments’, $comment_count, ‘theme-name’), $comment_count);
4. Handle Multi-Language Content
Create a custom solution for content translation:
php
// Add to functions.php
class MultiLanguageContent {
public function __construct() {
add_action(‘init’, array($this, ‘init’));
add_action(‘wp_enqueue_scripts’, array($this, ‘enqueue_scripts’));
add_filter(‘the_content’, array($this, ‘filter_content’));
add_filter(‘the_title’, array($this, ‘filter_title’));
}
public function init() {
// Register custom fields for translations
add_meta_box(‘translation_fields’, ‘Translations’,
array($this, ‘translation_meta_box’), ‘post’);
add_meta_box(‘translation_fields’, ‘Translations’,
array($this, ‘translation_meta_box’), ‘page’);
add_action(‘save_post’, array($this, ‘save_translations’));
}
public function get_current_language() {
// Check URL parameter first
if (isset($_GET[‘lang’]) && in_array($_GET[‘lang’], $this->get_available_languages())) {
return sanitize_text_field($_GET[‘lang’]);
}
// Check session/cookie
if (isset($_COOKIE[‘site_language’])) {
return sanitize_text_field($_COOKIE[‘site_language’]);
}
// Default language
return ‘en’;
}
public function get_available_languages() {
return array(‘en’, ‘es’, ‘fr’, ‘de’); // Add your languages
}
public function translation_meta_box($post) {
$languages = $this->get_available_languages();
wp_nonce_field(‘translation_nonce’, ‘translation_nonce’);
foreach ($languages as $lang) {
if ($lang === ‘en’) continue; // Skip default language
$title = get_post_meta($post->ID, “_title_{$lang}”, true);
$content = get_post_meta($post->ID, “_content_{$lang}”, true);
echo “<h4>” . strtoupper($lang) . ” Translation</h4>”;
echo “<p><label>Title:</label><br>”;
echo “<input type=’text’ name=’title_{$lang}’ value='” . esc_attr($title) . “‘ style=’width:100%’></p>”;
echo “<p><label>Content:</label><br>”;
echo “<textarea name=’content_{$lang}’ rows=’10’ style=’width:100%’>” . esc_textarea($content) . “</textarea></p>”;
}
}
public function save_translations($post_id) {
if (!isset($_POST[‘translation_nonce’]) ||
!wp_verify_nonce($_POST[‘translation_nonce’], ‘translation_nonce’)) {
return;
}
$languages = $this->get_available_languages();
foreach ($languages as $lang) {
if ($lang === ‘en’) continue;
if (isset($_POST[“title_{$lang}”])) {
update_post_meta($post_id, “_title_{$lang}”,
sanitize_text_field($_POST[“title_{$lang}”]));
}
if (isset($_POST[“content_{$lang}”])) {
update_post_meta($post_id, “_content_{$lang}”,
wp_kses_post($_POST[“content_{$lang}”]));
}
}
}
public function filter_title($title) {
if (is_admin()) return $title;
$current_lang = $this->get_current_language();
if ($current_lang === ‘en’) return $title;
global $post;
if ($post) {
$translated_title = get_post_meta($post->ID, “_title_{$current_lang}”, true);
return $translated_title ? $translated_title : $title;
}
return $title;
}
public function filter_content($content) {
if (is_admin()) return $content;
$current_lang = $this->get_current_language();
if ($current_lang === ‘en’) return $content;
global $post;
if ($post) {
$translated_content = get_post_meta($post->ID, “_content_{$current_lang}”, true);
return $translated_content ? $translated_content : $content;
}
return $content;
}
public function enqueue_scripts() {
wp_enqueue_script(‘multilang-script’, get_template_directory_uri() . ‘/js/multilang.js’,
array(‘jquery’), ‘1.0’, true);
}
}
new MultiLanguageContent();
5. Create Language Switcher
Add this function to display language switcher:
php
function display_language_switcher() {
$current_lang = (new MultiLanguageContent())->get_current_language();
$languages = array(
‘en’ => ‘English’,
‘es’ => ‘Español’,
‘fr’ => ‘Français’,
‘de’ => ‘Deutsch’
);
echo ‘<div class=”language-switcher”>’;
foreach ($languages as $code => $name) {
$url = add_query_arg(‘lang’, $code, get_permalink());
$active = ($current_lang === $code) ? ‘active’ : ”;
echo “<a href='{$url}’ class=’lang-link {$active}’ data-lang='{$code}’>{$name}</a> “;
}
echo ‘</div>’;
}
6. JavaScript for Language Switching
Create /js/multilang.js:
javascript
jQuery(document).ready(function($) {
$(‘.lang-link’).on(‘click’, function(e) {
e.preventDefault();
var lang = $(this).data(‘lang’);
// Set cookie for language preference
document.cookie = “site_language=” + lang + “; path=/; max-age=31536000”;
// Reload page with language parameter
window.location.href = $(this).attr(‘href’);
});
});
7. CSS for Language Switcher
css
.language-switcher {
padding: 10px;
text-align: right;
}
.lang-link {
margin: 0 5px;
padding: 5px 10px;
text-decoration: none;
border: 1px solid #ddd;
border-radius: 3px;
}
.lang-link.active {
background-color: #0073aa;
color: white;
}
8. Generate Translation Files
Create a .pot file for translators:
- Use WP-CLI: wp i18n make-pot . languages/theme-name.pot
- Or use Poedit to scan your theme files
- Create .po files for each language
- Generate .mo files from .po files
9. Handle Menus and Widgets
For navigation menus in different languages:
php
function get_multilang_menu($menu_location) {
$current_lang = (new MultiLanguageContent())->get_current_language();
$menu_location_lang = $menu_location . ‘_’ . $current_lang;
if (has_nav_menu($menu_location_lang)) {
return $menu_location_lang;
}
return $menu_location; // Fallback to default
}
// Register language-specific menus
function register_multilang_menus() {
$languages = array(‘en’, ‘es’, ‘fr’, ‘de’);
$menus = array();
foreach ($languages as $lang) {
$menus[“primary_{$lang}”] = “Primary Menu ({$lang})”;
$menus[“footer_{$lang}”] = “Footer Menu ({$lang})”;
}
register_nav_menus($menus);
}
add_action(‘init’, ‘register_multilang_menus’);
10. SEO Considerations
Add hreflang tags:
php
function add_hreflang_tags() {
if (is_singular()) {
$languages = array(‘en’, ‘es’, ‘fr’, ‘de’);
$current_url = get_permalink();
foreach ($languages as $lang) {
$lang_url = add_query_arg(‘lang’, $lang, $current_url);
echo “<link rel=’alternate’ hreflang='{$lang}’ href='{$lang_url}’ />\n”;
}
}
}
add_action(‘wp_head’, ‘add_hreflang_tags’);
This approach gives you complete control over multi-language functionality without relying on plugins. You can extend it further by adding features like automatic language detection based on browser settings, RTL language support, or custom post type translations.
Yes, the setup I provided will make post content multi-language, but let me clarify and expand on how it works:
How the Content Translation Works
The code I provided creates separate translation fields for each post/page where you can store translated versions of:
- Post titles
- Post content
- Any other custom fields you want to translate
Here’s the Complete Flow:
1. Admin Interface
When editing a post, you’ll see additional meta boxes with fields like:
ES Translation
Title: [input field for Spanish title]
Content: [textarea for Spanish content]
FR Translation
Title: [input field for French title]
Content: [textarea for French content]
2. Content Storage
- Original content stays in
wp_posts
table - Translations are stored as post meta:
_title_es
for Spanish title
_content_es
for Spanish content
_title_fr
for French title
_content_fr
for French content
3. Frontend Display
The filters automatically swap content based on selected language:
php
publicfunction
filter_content($content)
{
$current_lang
=
$this->get_current_language();
if
($current_lang
===
'en')
return
$content;
// Show original
global
$post;
$translated_content
=
get_post_meta($post->ID,
"_content_{$current_lang}",
true);
return
$translated_content
?
$translated_content
:
$content;
// Show translation or fallback
}
Enhanced Content Translation Setup
Here’s an improved version that handles more content types:
Enhanced Multi-Language Content System Code <?php // Enhanced Multi-Language Content System // Add this to your theme’s functions.php class EnhancedMultiLanguageContent { private $default_language = ‘en’; private $available_languages = array( ‘en’ => ‘English’, ‘e How to Use This Enhanced System
1. Edit Posts/Pages
When you edit any post or page, you’ll see a new “Content Translations” meta box with tabs for each language where you can enter:
- Translated titles
- Translated content (with full WordPress editor)
- Translated excerpts
- Translated custom fields
2. Display Language Switcher
Add this to your theme templates:
php
// Simple text switcher
display_language_switcher();
// With flag icons
display_language_switcher(true);
3. Get Translated Content Programmatically
php
// Get translated title
$spanish_title=
get_multilang_content($post_id,
'title',
'es');
// Get translated content
$french_content=
get_multilang_content($post_id,
'content',
'fr');
// Get translated custom field
$german_button=
get_multilang_content($post_id,
'button_text',
'de');
4. Automatic Content Switching
The system automatically shows translated content based on the selected language. If no translation exists, it falls back to the original content.
What Gets Translated
✅ Post/Page Content: Full content with WordPress editor ✅ Titles: Post and page titles
✅ Excerpts: Post excerpts ✅ Custom Fields: Any custom fields you define ✅ Theme Strings: All text wrapped in translation functions
Frontend Usage Example
php
// In your theme files
<h1><?php the_title(); ?></h1><!-- Automatically translated -->
<div><?php the_content(); ?></div><!-- Automatically translated -->
// For custom fields
<button><?php echo get_post_meta(get_the_ID(), 'button_text', true); ?></button>
This system creates a complete multi-language solution where all post content is translatable without any plugins. The translations are stored separately and automatically displayed based on the selected language.
improved version that handles more content types:
<?php
// Enhanced Multi-Language Content System
// Add this to your theme's functions.php
class EnhancedMultiLanguageContent {
private $default_language = 'en';
private $available_languages = array(
'en' => 'English',
'es' => 'Español',
'fr' => 'Français',
'de' => 'Deutsch',
'it' => 'Italiano'
);
public function __construct() {
add_action('init', array($this, 'init'));
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
add_action('admin_enqueue_scripts', array($this, 'admin_enqueue_scripts'));
// Content filters
add_filter('the_content', array($this, 'filter_content'));
add_filter('the_title', array($this, 'filter_title'));
add_filter('the_excerpt', array($this, 'filter_excerpt'));
add_filter('get_the_excerpt', array($this, 'filter_excerpt'));
// Admin hooks
add_action('add_meta_boxes', array($this, 'add_translation_meta_boxes'));
add_action('save_post', array($this, 'save_translations'));
// Custom fields support
add_filter('get_post_metadata', array($this, 'filter_custom_fields'), 10, 4);
}
public function init() {
// Register text domain
load_theme_textdomain('multilang-theme', get_template_directory() . '/languages');
// Set current language
$this->set_current_language();
}
public function get_current_language() {
if (isset($_SESSION['current_language'])) {
return $_SESSION['current_language'];
}
// Check URL parameter
if (isset($_GET['lang']) && array_key_exists($_GET['lang'], $this->available_languages)) {
return sanitize_text_field($_GET['lang']);
}
// Check cookie
if (isset($_COOKIE['site_language']) && array_key_exists($_COOKIE['site_language'], $this->available_languages)) {
return sanitize_text_field($_COOKIE['site_language']);
}
return $this->default_language;
}
private function set_current_language() {
if (!session_id()) {
session_start();
}
$_SESSION['current_language'] = $this->get_current_language();
}
public function add_translation_meta_boxes() {
$post_types = get_post_types(array('public' => true), 'names');
foreach ($post_types as $post_type) {
add_meta_box(
'translation_fields',
__('Content Translations', 'multilang-theme'),
array($this, 'translation_meta_box'),
$post_type,
'normal',
'high'
);
}
}
public function translation_meta_box($post) {
wp_nonce_field('translation_nonce', 'translation_nonce');
echo '<div class="translation-tabs">';
echo '<div class="tab-nav">';
foreach ($this->available_languages as $code => $name) {
if ($code === $this->default_language) continue;
$active = ($code === 'es') ? 'active' : '';
echo "<button type='button' class='tab-btn {$active}' data-tab='{$code}'>{$name}</button>";
}
echo '</div>';
foreach ($this->available_languages as $code => $name) {
if ($code === $this->default_language) continue;
$display = ($code === 'es') ? 'block' : 'none';
echo "<div class='tab-content' id='tab-{$code}' style='display: {$display}'>";
$this->render_translation_fields($post->ID, $code, $name);
echo '</div>';
}
echo '</div>';
}
private function render_translation_fields($post_id, $lang_code, $lang_name) {
$title = get_post_meta($post_id, "_title_{$lang_code}", true);
$content = get_post_meta($post_id, "_content_{$lang_code}", true);
$excerpt = get_post_meta($post_id, "_excerpt_{$lang_code}", true);
echo "<h3>{$lang_name} Translation</h3>";
// Title field
echo "<p><label><strong>" . __('Title:', 'multilang-theme') . "</strong></label><br>";
echo "<input type='text' name='title_{$lang_code}' value='" . esc_attr($title) . "' style='width:100%; padding: 5px;'></p>";
// Content field
echo "<p><label><strong>" . __('Content:', 'multilang-theme') . "</strong></label><br>";
wp_editor($content, "content_{$lang_code}", array(
'textarea_name' => "content_{$lang_code}",
'textarea_rows' => 10,
'media_buttons' => true,
'teeny' => false
));
echo "</p>";
// Excerpt field
echo "<p><label><strong>" . __('Excerpt:', 'multilang-theme') . "</strong></label><br>";
echo "<textarea name='excerpt_{$lang_code}' rows='3' style='width:100%; padding: 5px;'>" . esc_textarea($excerpt) . "</textarea></p>";
// Custom fields support
$this->render_custom_field_translations($post_id, $lang_code);
}
private function render_custom_field_translations($post_id, $lang_code) {
// Define translatable custom fields
$translatable_fields = apply_filters('multilang_custom_fields', array(
'custom_title' => __('Custom Title', 'multilang-theme'),
'custom_description' => __('Custom Description', 'multilang-theme'),
'button_text' => __('Button Text', 'multilang-theme')
));
if (!empty($translatable_fields)) {
echo "<h4>" . __('Custom Fields', 'multilang-theme') . "</h4>";
foreach ($translatable_fields as $field_key => $field_label) {
$value = get_post_meta($post_id, "_{$field_key}_{$lang_code}", true);
echo "<p><label><strong>{$field_label}:</strong></label><br>";
echo "<input type='text' name='{$field_key}_{$lang_code}' value='" . esc_attr($value) . "' style='width:100%; padding: 3px;'></p>";
}
}
}
public function save_translations($post_id) {
if (!isset($_POST['translation_nonce']) ||
!wp_verify_nonce($_POST['translation_nonce'], 'translation_nonce') ||
defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
foreach ($this->available_languages as $code => $name) {
if ($code === $this->default_language) continue;
// Save basic fields
if (isset($_POST["title_{$code}"])) {
update_post_meta($post_id, "_title_{$code}", sanitize_text_field($_POST["title_{$code}"]));
}
if (isset($_POST["content_{$code}"])) {
update_post_meta($post_id, "_content_{$code}", wp_kses_post($_POST["content_{$code}"]));
}
if (isset($_POST["excerpt_{$code}"])) {
update_post_meta($post_id, "_excerpt_{$code}", sanitize_textarea_field($_POST["excerpt_{$code}"]));
}
// Save custom fields
$translatable_fields = apply_filters('multilang_custom_fields', array(
'custom_title' => '',
'custom_description' => '',
'button_text' => ''
));
foreach ($translatable_fields as $field_key => $field_label) {
if (isset($_POST["{$field_key}_{$code}"])) {
update_post_meta($post_id, "_{$field_key}_{$code}", sanitize_text_field($_POST["{$field_key}_{$code}"]));
}
}
}
}
public function filter_title($title) {
if (is_admin() || !in_the_loop()) return $title;
$current_lang = $this->get_current_language();
if ($current_lang === $this->default_language) return $title;
global $post;
if ($post && isset($post->ID)) {
$translated_title = get_post_meta($post->ID, "_title_{$current_lang}", true);
return !empty($translated_title) ? $translated_title : $title;
}
return $title;
}
public function filter_content($content) {
if (is_admin() || !in_the_loop()) return $content;
$current_lang = $this->get_current_language();
if ($current_lang === $this->default_language) return $content;
global $post;
if ($post && isset($post->ID)) {
$translated_content = get_post_meta($post->ID, "_content_{$current_lang}", true);
return !empty($translated_content) ? $translated_content : $content;
}
return $content;
}
public function filter_excerpt($excerpt) {
if (is_admin()) return $excerpt;
$current_lang = $this->get_current_language();
if ($current_lang === $this->default_language) return $excerpt;
global $post;
if ($post && isset($post->ID)) {
$translated_excerpt = get_post_meta($post->ID, "_excerpt_{$current_lang}", true);
return !empty($translated_excerpt) ? $translated_excerpt : $excerpt;
}
return $excerpt;
}
public function filter_custom_fields($value, $object_id, $meta_key, $single) {
if (is_admin()) return $value;
$current_lang = $this->get_current_language();
if ($current_lang === $this->default_language) return $value;
// Check if this is a translatable custom field
$translatable_fields = apply_filters('multilang_custom_fields', array(
'custom_title' => '',
'custom_description' => '',
'button_text' => ''
));
if (array_key_exists($meta_key, $translatable_fields)) {
$translated_value = get_post_meta($object_id, "_{$meta_key}_{$current_lang}", true);
if (!empty($translated_value)) {
return $single ? $translated_value : array($translated_value);
}
}
return $value;
}
public function enqueue_scripts() {
wp_enqueue_script('multilang-frontend', get_template_directory_uri() . '/js/multilang-frontend.js', array('jquery'), '1.0', true);
wp_localize_script('multilang-frontend', 'multilang_ajax', array(
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('multilang_nonce')
));
}
public function admin_enqueue_scripts() {
wp_enqueue_script('multilang-admin', get_template_directory_uri() . '/js/multilang-admin.js', array('jquery'), '1.0', true);
wp_enqueue_style('multilang-admin', get_template_directory_uri() . '/css/multilang-admin.css', array(), '1.0');
}
public function get_language_switcher($show_flags = false) {
$current_lang = $this->get_current_language();
$current_url = $this->get_current_url();
$output = '<div class="language-switcher">';
foreach ($this->available_languages as $code => $name) {
$lang_url = add_query_arg('lang', $code, $current_url);
$active_class = ($current_lang === $code) ? 'active' : '';
if ($show_flags) {
$flag = "<img src='" . get_template_directory_uri() . "/images/flags/{$code}.png' alt='{$name}' class='flag-icon'>";
$output .= "<a href='{$lang_url}' class='lang-link {$active_class}' data-lang='{$code}'>{$flag} {$name}</a>";
} else {
$output .= "<a href='{$lang_url}' class='lang-link {$active_class}' data-lang='{$code}'>{$name}</a>";
}
}
$output .= '</div>';
return $output;
}
private function get_current_url() {
return remove_query_arg('lang', $_SERVER['REQUEST_URI']);
}
// Helper function to get translated content
public function get_translated_content($post_id, $field = 'content', $lang = null) {
if (!$lang) {
$lang = $this->get_current_language();
}
if ($lang === $this->default_language) {
switch ($field) {
case 'title':
return get_the_title($post_id);
case 'content':
return get_post_field('post_content', $post_id);
case 'excerpt':
return get_post_field('post_excerpt', $post_id);
default:
return get_post_meta($post_id, $field, true);
}
}
return get_post_meta($post_id, "_{$field}_{$lang}", true);
}
}
// Initialize the multi-language system
new EnhancedMultiLanguageContent();
// Helper functions for theme developers
function get_multilang_content($post_id, $field = 'content', $lang = null) {
global $enhanced_multilang;
if (!$enhanced_multilang) {
$enhanced_multilang = new EnhancedMultiLanguageContent();
}
return $enhanced_multilang->get_translated_content($post_id, $field, $lang);
}
function display_language_switcher($show_flags = false) {
global $enhanced_multilang;
if (!$enhanced_multilang) {
$enhanced_multilang = new EnhancedMultiLanguageContent();
}
echo $enhanced_multilang->get_language_switcher($show_flags);
}
// Custom field translation support
function add_translatable_custom_fields($fields) {
// Add your custom fields here
$fields['hero_title'] = __('Hero Title', 'multilang-theme');
$fields['hero_subtitle'] = __('Hero Subtitle', 'multilang-theme');
$fields['cta_button'] = __('CTA Button Text', 'multilang-theme');
return $fields;
}
add_filter('multilang_custom_fields', 'add_translatable_custom_fields');
?>