Skip to content

WordPress Localization and Internationalization โ€” Translating Themes and Plugins

DodaTech Updated 2026-06-27 19 min read

In this tutorial, you'll learn how to make WordPress themes and plugins translatable using i18n functions like __(), _e(), _n(), and _x(), create .pot files, translate with Poedit, and load text domains properly.

What You'll Learn

  • The difference between i18n (internationalization) and l10n (localization)
  • What a text domain is and how it namespaces translations
  • All i18n functions: __(), e(), x(), n(), nx(), esc_html(), esc_html_e(), esc_attr(), esc_attr_e()
  • How to load text domains in themes and plugins
  • How to create .pot, .po, and .mo files with Poedit and WP-CLI
  • How to include variables in translated strings with printf and sprintf
  • How to format numbers and dates for different locales
  • How to localize JavaScript with wp_localize_script()
  • How to add right-to-left (RTL) language support with rtl.css

Why It Matters

WordPress powers over 40% of the web, and a large portion of those sites are not in English. If your theme or plugin is only available in one language, you exclude millions of potential users. Internationalization โ€” preparing your code for translation โ€” makes your work accessible globally. The WordPress community has translated the core software into over 200 languages. Your theme or plugin can be part of that ecosystem. Even if you never translate it yourself, making your code translatable means users can contribute translations in their own language.

Real-World Use

A German web developer installs your plugin but all buttons and messages appear in English. The plugin works, but the German user has to guess what each setting does. If you had wrapped every string in __() and provided a .pot file, the community could translate it to German within hours. The same plugin now works for French, Spanish, Japanese, and Arabic users โ€” without you writing a single translation. The work is done entirely by the community.

Learning Path

flowchart LR
    A[REST API] --> B[Localization]
    B --> C[WooCommerce Setup]
    B --> D[Security Hardening]
    B --> E[Performance Optimization]
    style B fill:#4a90d9,color:#fff

What Is i18n and l10n?

These abbreviations are shorthand for long words:

  • i18n = internationalization (18 letters between i and n). The Process of writing code so it can handle multiple languages without rewriting.
  • l10n = localization (10 letters between l and n). The process of translating the interface text into a specific language.

Think of it this way: internationalization is preparing the kitchen to cook any cuisine. Localization is cooking a specific dish. You do i18n once in your code. You can do l10n many times for many languages.

How It Works in WordPress

WordPress stores translations in .mo (Machine Object) files โ€” compiled binary files that PHP reads quickly. Each language has its own .mo file. When a user sets their site language to French, WordPress loads the French .mo file, and every translated string automatically appears in French.

English:     "Hello World"         (source, in code)
French:      "Bonjour le monde"    (in fr_FR.mo)
German:      "Hallo Welt"          (in de_DE.mo)
Arabic:      "ู…ุฑุญุจุง ุจุงู„ุนุงู„ู…"       (in ar_AR.mo)

Text Domain

A text domain is a unique identifier that tells WordPress which set of translations belongs to which theme or plugin. It acts like a namespace.

Rules for Text Domains

  1. The text domain must match the theme/plugin slug
  2. The text domain must match the Text Domain: header in style.css or the plugin header
  3. The text domain must match the folder name used in load_theme_textdomain() or load_plugin_textdomain()
  4. Use hyphens, not underscores (e.g., my-custom-theme, not my_custom_theme)

In a Theme (style.css)

/*
Theme Name: My Custom Theme
Text Domain: my-custom-theme
*/

In a Plugin (plugin header)

<?php
/**
 * Plugin Name: My Custom Plugin
 * Text Domain: my-custom-plugin
 */

i18n Functions

WordPress provides several PHP functions for internationalization. Each has a specific purpose.

__() โ€” Return Translated String

The most common i18n function. It returns the translated version of a string:

<?php
// Basic usage
$translated = __('Hello World', 'my-text-domain');

// In a template
echo __('Welcome to our website', 'my-text-domain');

// Assign to variable
$button_text = __('Submit Form', 'my-text-domain');
?>

The first parameter is the text to translate (the source string). The second is the text domain.

_e() โ€” Echo Translated String

Identical to __() but echoes the result directly instead of returning it:

<?php
// Echo directly โ€” no echo() call needed
_e('Read More', 'my-text-domain');

// These two lines do the same thing:
echo __('Read More', 'my-text-domain');
_e('Read More', 'my-text-domain');
?>

Use _e() when you only need to output the string and do not need to use it elsewhere.

_x() โ€” Translation with Context

Some words have different meanings depending on context. The _x() function adds a context string to help translators:

<?php
// The word "Post" could be a noun (a blog post) or a verb (to post something)
// _x() adds context to disambiguate

// As a noun:
echo _x('Post', 'noun - a blog post', 'my-text-domain');

// As a verb:
echo _x('Post', 'verb - to publish', 'my-text-domain');
?>

Without context, a translator sees "Post" once and does not know which meaning to use. With context, they can translate each usage correctly.

_n() โ€” Singular and Plural

Strings that change form depending on a number need special handling:

<?php
// Parameters: single, plural, number, text domain
$comments_count = 5;
echo _n('1 comment', '%s comments', $comments_count, 'my-text-domain');
// Output: "5 comments"
?>

The %s placeholder is replaced by the number. WordPress handles grammar rules for languages that have multiple plural forms (like Russian and Arabic which have different forms for 1, 2-4, and 5+).

_nx() โ€” Singular/Plural with Context

Combines _n() and _x() for translated plurals with context:

<?php
$found = 3;
echo _nx('%s result found', '%s results found', $found, 'search results', 'my-text-domain');
// Output: "3 results found"
?>

esc_html__() โ€” Translated + Escaped for HTML

Returns a translated string that has been escaped for safe HTML output:

<?php
// Safe for use inside HTML tags
echo esc_html__('User <script>alert("xss")</script>', 'my-text-domain');
// Output: "User &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;"
?>

esc_html_e() โ€” Echo + Escape for HTML

Echoes a translated and HTML-escaped string:

<?php
esc_html_e('This text is safe for HTML output', 'my-text-domain');
?>

esc_attr__() โ€” Translated + Escaped for Attributes

Returns a translated string escaped for use in HTML attributes:

<?php
// Safe inside an HTML attribute value
echo '<input type="text" placeholder="' . esc_attr__('Enter your name', 'my-text-domain') . '">';
?>

esc_attr_e() โ€” Echo + Escape for Attributes

Echoes a translated string escaped for attributes:

<?php
echo '<input type="submit" value="';
esc_attr_e('Save Changes', 'my-text-domain');
echo '">';
?>

Function Reference Table

Function Returns Escapes Use Case
__() String No Store in variable, pass to functions
_e() Echoes No Output directly in templates
_x() String No Disambiguate with context
_n() String No Singular/plural based on count
_nx() String No Singular/plural with context
esc_html__() String HTML Safe inside HTML body
esc_html_e() Echoes HTML Output safely in HTML
esc_attr__() String Attribute Safe in HTML attributes
esc_attr_e() Echoes Attribute Output safely in attributes

Load Text Domain in Themes

You must tell WordPress where to find your translation files. In themes, use load_theme_textdomain() in functions.php:

<?php
function my_theme_setup() {
    // Load translations from /wp-content/themes/my-theme/languages/
    load_theme_textdomain('my-text-domain', get_template_directory() . '/languages');
}
add_action('after_setup_theme', 'my_theme_setup');
?>

Expected File Structure

wp-content/themes/my-theme/
  languages/
    fr_FR.mo         # French translation (compiled)
    fr_FR.po         # French translation (editable)
    de_DE.mo         # German translation
    de_DE.po
    my-theme.pot     # Source template (generated from code)
  style.css
  functions.php
  index.php

Language File Naming Convention

{locale}.mo       โ†’ fr_FR.mo
{locale}.po       โ†’ fr_FR.po
{domain}-{locale}.mo  โ†’ my-text-domain-fr_FR.mo (alternative)

The locale format is language_CODE where language is the ISO 639-1 language code and CODE is the ISO 3166-1 country code:

  • fr_FR = French (France)
  • de_DE = German (Germany)
  • es_ES = Spanish (Spain)
  • ar_AR = Arabic (world)
  • pt_BR = Portuguese (Brazil)
  • en_GB = English (United Kingdom)

Load Text Domain in Plugins

In plugins, use load_plugin_textdomain():

<?php
function my_plugin_load_textdomain() {
    load_plugin_textdomain(
        'my-text-domain',
        false,
        dirname(plugin_basename(__FILE__)) . '/languages/'
    );
}
add_action('plugins_loaded', 'my_plugin_load_textdomain');
?>

Expected Plugin File Structure

wp-content/plugins/my-plugin/
  languages/
    fr_FR.mo
    fr_FR.po
    my-plugin.pot
  my-plugin.php

Creating .pot File

The .pot (Portable Object Template) file is the master list of all translatable strings in your code. It is generated from scanning your source files. You never edit it directly โ€” it is the template from which translators create .po files.

Method 1: Using WP-CLI

WP-CLI is the command-line tool for WordPress management. It can generate .pot files:

# Generate a .pot file for a theme
wp i18n make-pot wp-content/themes/my-theme/ wp-content/themes/my-theme/languages/my-theme.pot

# Generate for a plugin
wp i18n make-pot wp-content/plugins/my-plugin/ wp-content/plugins/my-plugin/languages/my-plugin.pot

# With custom domain and package name
wp i18n make-pot wp-content/themes/my-theme/ languages/my-theme.pot \
  --slug="my-theme" \
  --domain="my-text-domain" \
  --package-name="My Theme"

Method 2: Using Poedit

Poedit (https://poedit.net) is a desktop application for translation. It can also scan source code:

  1. Open Poedit
  2. Click File > New
  3. Select the language you are translating from (always the source language)
  4. Save the empty .po file to your languages folder
  5. Click Catalog > Properties
  6. Set Source paths to your theme/plugin directory
  7. Set Source keywords to include all i18n functions: __;_e;_x;_n;_nx;esc_html__;esc_html_e;esc_attr__;esc_attr_e
  8. Click OK
  9. Click Catalog > Update from sources
  10. Poedit scans your code and populates the translation list
  11. Save โ€” Poedit creates both .po and .mo files

Method 3: Loco Translate Plugin

Loco Translate is a plugin that lets you manage translations from the WordPress admin:

  1. Install Loco Translate from the WordPress plugin Repository
  2. Go to Loco Translate > Themes or Plugins
  3. Select your theme or plugin
  4. Click New language
  5. Choose the language and location (system or custom)
  6. Translate strings in the browser
  7. Save โ€” Loco creates .po and .mo files automatically

Translating with Poedit

Once you have a .po file with the extracted strings, here is how the translation process works:

The Translation Interface

In Poedit, you see three columns:

Source String                    | Translation                    | Status
---------------------------------|--------------------------------|--------
Read More                        | Lire la suite                  | Translated
Search results for %s           | Resultats de recherche pour %s | Translated
1 comment                        | 1 commentaire                  | Needs review

Each row shows:

  • Source String โ€” the original English text from your code
  • Translation โ€” the translated text
  • Status โ€” whether it is translated, needs review, or is untranslated

Saving Files

When you save in Poedit:

  • The .po file is updated with your translations (human-readable)
  • The .mo file is generated (machine-readable, used by PHP)

Always upload both files. WordPress reads the .mo file. The .po file is only for editing.

Translating Plural Forms

Some languages have complex plural rules. Poedit handles this automatically:

English:         1 comment / 2 comments / 5 comments (2 forms)
French:          1 commentaire / 2 commentaires (2 forms)
Russian:         1 ะบะพะผะผะตะฝั‚ะฐั€ะธะน / 2 ะบะพะผะผะตะฝั‚ะฐั€ะธั / 5 ะบะพะผะผะตะฝั‚ะฐั€ะธะตะฒ (3 forms)
Arabic:          1 ุชุนู„ูŠู‚ / 2 ุชุนู„ูŠู‚ุงู† / 5 ุชุนู„ูŠู‚ุงุช / 11 ุชุนู„ูŠู‚ุงู‹ (6 forms)

WordPress uses _n() to handle plurals. The .po file provides translations for each plural form.

Translating Theme Strings in Templates

Here is how a real theme template looks with i18n functions:

<!-- In a theme template file (e.g., index.php or archive.php) -->
<article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
    <h2><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>

    <p class="post-meta">
        <?php
        printf(
            /* translators: 1: post date, 2: post author */
            esc_html__('Posted on %1$s by %2$s', 'my-text-domain'),
            get_the_date(),
            get_the_author()
        );
        ?>
    </p>

    <div class="entry-content">
        <?php the_excerpt(); ?>
        <a href="<?php the_permalink(); ?>"><?php esc_html_e('Continue reading', 'my-text-domain'); ?></a>
    </div>

    <footer class="entry-footer">
        <?php
        if (has_tag()) {
            echo '<span class="tags">';
            esc_html_e('Tagged: ', 'my-text-domain');
            the_tags('', ', ');
            echo '</span>';
        }
        ?>
    </footer>
</article>

PHP Template Example with All Functions

<?php
// Using __() to return a translated string
$site_title = __('My WordPress Site', 'my-text-domain');

// Using _e() to echo directly
_e('Welcome to our site', 'my-text-domain');

// Using _x() with context
echo _x('Post', 'noun - a blog post', 'my-text-domain');

// Using _n() for plurals
$count = 5;
echo _n('1 item', '%s items', $count, 'my-text-domain');

// Using _nx() for plurals with context
echo _nx('1 result found', '%s results found', $count, 'search results', 'my-text-domain');

// Escaped versions for safe output
echo esc_html__('Safe HTML title', 'my-text-domain');
esc_html_e('Safe HTML content', 'my-text-domain');

// For HTML attributes
echo '<a href="#" title="' . esc_attr__('Click here', 'my-text-domain') . '">';
esc_attr_e('Read More', 'my-text-domain');
echo '</a>';
?>

Including Variables in Translations

You cannot include dynamic values directly in translatable strings because translators would have to duplicate each variant:

// โŒ WRONG โ€” translator would need to translate every possible name
echo __('Hello John', 'my-text-domain');
echo __('Hello Sarah', 'my-text-domain');
echo __('Hello Mike', 'my-text-domain');

// โœ… CORRECT โ€” use %s placeholder and printf/sprintf
printf(
    __('Hello %s', 'my-text-domain'),
    $user_name
);

Using printf() for Variable Substitution

<?php
$username = 'John';
$site_name = 'My Site';

printf(
    /* translators: 1: username, 2: site name */
    esc_html__('Welcome back, %1$s! You are logged into %2$s.', 'my-text-domain'),
    $username,
    $site_name
);
// Output: "Welcome back, John! You are logged into My Site."
?>

The %1$s, %2$s syntax allows reordering. Translators can rearrange placeholder positions if the grammar of their language requires it:

<?php
// English: "Posts by John in Category"
printf(
    __('Posts by %1$s in %2$s', 'my-text-domain'),
    $author_name,
    $category_name
);

// If a language needs: "Posts in Category by John"
// The translator can use: "Posts in %2$s by %1$s"
?>

Translators Comment

Always add a translators comment before complex strings using /* translators: */. This comment appears in the .po file to help translators understand the context:

<?php
/* translators: %s: number of new messages */
echo _n('You have %s new message.', 'You have %s new messages.', $count, 'my-text-domain');

/* translators: 1: post title, 2: category name */
printf(
    __('The post "%1$s" is filed under %2$s.', 'my-text-domain'),
    $post_title,
    $category_name
);
?>

In Poedit, the translator sees:

Source: You have %s new message.
Comment: translators: %s: number of new messages

Number Formatting

Different locales format numbers differently. In the US, 1,234.56 is common. In Germany, it is 1.234,56. Use number_format_i18n() to handle this:

<?php
$price = 1234.56;

// English (US): 1,234.56
echo number_format_i18n($price, 2);

// German (DE): 1.234,56
echo number_format_i18n($price, 2); // Automatically uses the locale's format
?>

With Currency Formatting

<?php
$price = 49.99;

/* translators: %s: price with currency symbol */
printf(
    __('Price: %s', 'my-text-domain'),
    '$' . number_format_i18n($price, 2)
);
?>

Date/Time Formatting

Use date_i18n() to display dates in the correct format for the current locale. It translates month and day names automatically:

<?php
$timestamp = time(); // Current timestamp

// "June 27, 2026" (English) or "27 juin 2026" (French)
echo date_i18n('F j, Y', $timestamp);

// Use WordPress date format setting
echo date_i18n(get_option('date_format'), $timestamp);

// With time
echo date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $timestamp);
?>

The Time Since Function

<?php
// human_time_diff() shows relative time like "2 hours ago"
// It is already localized via WordPress core translations
printf(
    /* translators: %s: human-readable time difference */
    esc_html__('%s ago', 'my-text-domain'),
    human_time_diff(get_the_time('U'), current_time('timestamp'))
);
// Output: "3 hours ago" or "il y a 3 heures" (French)
?>

Localizing Scripts

When you enqueue JavaScript that contains strings, you need to make those strings available to JavaScript. Use wp_localize_script():

In PHP (functions.php or plugin):

<?php
function my_theme_scripts() {
    // Enqueue the script first
    wp_enqueue_script(
        'my-theme-script',
        get_template_directory_uri() . '/js/script.js',
        array(),
        '1.0.0',
        true
    );

    // Localize strings for JavaScript
    wp_localize_script('my-theme-script', 'myThemeI18n', array(
        'confirmDelete' => __('Are you sure you want to delete this?', 'my-text-domain'),
        'loading'       => __('Loading...', 'my-text-domain'),
        'error'         => __('Something went wrong.', 'my-text-domain'),
        'submit'        => __('Submit', 'my-text-domain'),
    ));
}
add_action('wp_enqueue_scripts', 'my_theme_scripts');
?>

In JavaScript (script.js):

// Use the localized strings
document.getElementById('delete-button').addEventListener('click', function(e) {
    if (!confirm(myThemeI18n.confirmDelete)) {
        e.preventDefault();
    }
});

document.getElementById('submit-form').addEventListener('submit', function() {
    const button = this.querySelector('button');
    button.textContent = myThemeI18n.loading;
    button.disabled = true;
});

Real-World Example: AJAX with Localized Strings

// PHP: Enqueue and localize
function my_theme_load_more_scripts() {
    wp_enqueue_script('my-load-more', get_template_directory_uri() . '/js/load-more.js', array('jquery'), '1.0', true);

    wp_localize_script('my-load-more', 'myLoadMore', array(
        'ajaxUrl'   => admin_url('admin-ajax.php'),
        'loadMore'  => __('Load More Posts', 'my-text-domain'),
        'loading'   => __('Loading...', 'my-text-domain'),
        'noMore'    => __('No more posts to load.', 'my-text-domain'),
        'error'     => __('Error loading posts.', 'my-text-domain'),
        'nonce'     => wp_create_nonce('load_more_nonce'),
    ));
}
add_action('wp_enqueue_scripts', 'my_theme_load_more_scripts');
// JavaScript: load-more.js
jQuery(document).ready(function($) {
    $('#load-more-button').text(myLoadMore.loadMore);

    $('#load-more-button').on('click', function() {
        const button = $(this);
        button.text(myLoadMore.loading);

        $.ajax({
            url: myLoadMore.ajaxUrl,
            type: 'POST',
            data: {
                action: 'load_more_posts',
                nonce: myLoadMore.nonce,
                page: button.data('page')
            },
            success: function(response) {
                if (response.success) {
                    if (response.data.has_more) {
                        button.text(myLoadMore.loadMore);
                        button.data('page', response.data.next_page);
                    } else {
                        button.text(myLoadMore.noMore);
                        button.prop('disabled', true);
                    }
                    $('#posts-container').append(response.data.html);
                } else {
                    button.text(myLoadMore.error);
                }
            },
            error: function() {
                button.text(myLoadMore.error);
            }
        });
    });
});

Right-to-Left Language Support

Languages like Arabic, Hebrew, Urdu, and Persian are written right-to-left (RTL). WordPress supports RTL through rtl.css.

How RTL Works

When a user selects an RTL language (e.g., Arabic), WordPress:

  1. Adds rtl class to the <body> tag
  2. Loads rtl.css after the main stylesheet if it exists
  3. Flips margins, padding, float directions automatically if rtl.css is properly written

Creating rtl.css

Create an rtl.css file in your theme root. It contains CSS overrides that flip the layout:

/* In rtl.css โ€” flip text direction */
body {
    direction: rtl;
    unicode-bidi: embed;
}

/* Fix alignment */
.alignleft {
    float: right;
    margin-left: 20px;
    margin-right: 0;
}

.alignright {
    float: left;
    margin-right: 20px;
    margin-left: 0;
}

/* Fix text alignment */
.text-left {
    text-align: right;
}

.text-right {
    text-align: left;
}

/* Fix padding/margin for RTL */
.site-branding {
    float: right;
}

.main-navigation {
    float: left;
}

/* Fix icon direction */
.dropdown-toggle::after {
    margin-right: 5px;
    margin-left: 0;
}

Modern Alternative: CSS Logical Properties

Modern CSS supports logical properties that automatically adapt to writing direction:

/* Instead of margin-left/margin-right, use margin-inline-start/margin-inline-end */
.element {
    margin-inline-start: 20px;  /* margin-left in LTR, margin-right in RTL */
    padding-inline-end: 10px;   /* padding-right in LTR, padding-left in RTL */
    border-inline-start: 2px solid #000;
}

/* Instead of float: left/right, use inset-inline-start/end */
.container {
    display: flex;
}

.item {
    margin-inline-end: auto;
}

Using logical properties means you may not need rtl.css at all โ€” the CSS adapts automatically.

Common Mistakes

  1. Not including the text domain: Every i18n function needs a text domain as the second parameter. Leaving it out causes the string to never be translated. Always pass the text domain: __('Hello', 'my-text-domain') not __('Hello').

  2. Using concatenation inside translation functions: You cannot concatenate strings inside __() because translators see only separate fragments:

    // WRONG โ€” translators see "Hello " and " World" separately
    echo __('Hello ', 'my-text-domain') . __('World', 'my-text-domain');
    
    // CORRECT โ€” one string with placeholder
    printf(__('Hello %s', 'my-text-domain'), 'World');
    
  3. Hardcoding strings instead of using i18n functions: Every user-facing string must use an i18n function. Debug logs and internal messages do not need translation, but everything visible to users does.

  4. Incorrect text domain in style.css: If the text domain in the style.css header does not match the text domain used in __() calls, translations never load. Double-check they match exactly.

  5. Forgetting to load the text domain: Simply using __() in your code does not work unless you call load_theme_textdomain() or load_plugin_textdomain() to tell WordPress where your .mo files are.

Practice Questions

  1. What is the difference between __() and _e()? When would you use each one?

  2. Why would you use _x() instead of __()? Give an example where context matters.

  3. How does _n() handle plural forms? What happens in languages with more than two plural forms?

  4. What is a text domain, and where must it be declared in a theme?

  5. What is the difference between .pot, .po, and .mo files?

Challenge: Create a minimal plugin that registers a shortcode called [welcome_message]. The shortcode should display "Welcome, [username]!" using i18n functions with a text domain of welcome-plugin. Include proper text domain loading, translators comments, and create the plugin structure with a languages folder ready for .mo files. The plugin must handle pluralization for Visitor counts.

FAQ

### Do I need to create translations for every language?

No. You just make your code translatable (i18n) and optionally provide a .pot file. The community can contribute translations for their own languages. Many themes and plugins are translated entirely by volunteers.

### Can I use the same text domain for multiple plugins?

No. Each theme and plugin must have its own unique text domain. Using the same text domain across multiple plugins causes conflicts where translations from one plugin interfere with the other.

### Does WordPress support regional variants (en-US vs en-GB)?

Yes. Locale codes like en_US, en_GB, pt_BR, pt_PT represent different regional variants. You can create separate .mo files for each variant.

### How do I test a translation without switching my entire site language?

Set the WPLANG constant in wp-config.php temporarily, or use the Loco Translate plugin which lets you preview translations without changing the site language.

### Can I dynamically switch languages on the frontend?

Yes, with plugins like Polylang, WPML, or TranslatePress. These plugins handle language switching and load the appropriate .mo files. You do not need to modify your i18n code.

Mini Project

Internationalize an existing theme with these steps:

  1. Choose a minimal theme (or create one with 3โ€“4 template files). Identify every user-facing string.

  2. Wrap every string in the appropriate i18n function (__(), _e(), _n(), _x()). Add translators comments for any string with placeholders.

  3. Add the text domain to style.css header and create the load_theme_textdomain() call in functions.php.

  4. Generate a .pot file using WP-CLI or Poedit. Verify it contains all your strings.

  5. Create a French translation (fr_FR.po and fr_FR.mo) translating at least 10 strings. Save the files in the languages folder.

  6. Switch your WordPress site language to French (Settings > General > Site Language). Verify your translated strings appear correctly.

  7. Add an rtl.css file with basic RTL overrides. Switch to Arabic and verify the layout flips correctly.

What's Next

Now that you can translate themes and plugins, move on to WooCommerce Setup to learn how to build an online store. Then explore Security Hardening to protect your multilingual site from common vulnerabilities.

For more development techniques, see the REST API tutorial to connect your translated site with frontend applications, and Hooks to understand the action and filter system that powers WordPress extensibility.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro