PTC

WordPress Internationalization: How to Translate Themes and Plugins

Make your WordPress theme or plugin ready for translation. This guide covers text domains, gettext functions, POT generation, and the loading mechanism that puts translations in front of users. PTC (Private Translation Cloud) then translates the resource files and visually reviews the rendered theme or plugin in every language.

This guide is for developers writing the code. If you already have a POT or PO file and just need to translate it, head to the PO files page for the 3-step workflow.

By the end of this guide, your theme or plugin will be:

  • Internationalized correctly per WordPress coding standards.
  • Translatable by PTC into 40+ languages.
  • Distributable via WordPress.org Language Packs.
  • Verifiable on every release with PTC's visual translation review.

What is WordPress internationalization?

Internationalization (i18n) is the work of preparing your code so it can be translated. Localization (l10n) is the next step. It produces the actual translated strings for specific languages.

For WordPress themes and plugins, i18n means three things:

  • Wrap every user-facing string in gettext functions so WordPress can swap them out at runtime.
  • Define a text domain that ties your translations to your project.
  • Generate a POT file that translators (or PTC) work from.

Once that work is done, you have everything you need to produce translated PO, MO, JSON, and .l10n.php files for any language.

PTC translates POT files into MO, JSON, and .l10n.php for WordPress

Five file extensions show up in any WordPress translation workflow. PTC translates from .pot (your source) into .po, .mo, .json, and .l10n.php for every target language. Each format has a specific role:

  • POT (Portable Object Template). The source file generated from your code. It lists every translatable string with no translations attached. You hand this to PTC.
  • PO (Portable Object). A copy of the POT with translations added for one specific language. Plain text, human-readable. PTC returns one PO per target language.
  • MO (Machine Object). The compiled binary version of a PO. WordPress reads MO files at runtime because they load faster than PO text.
  • JSON. The JavaScript equivalent of MO. WordPress cannot read MO from JavaScript, so the build pipeline produces JSON files for browser-side strings.
  • .l10n.php. A newer alternative to MO, introduced in WordPress 6.5. It loads faster and uses less memory. WordPress picks it automatically when one exists alongside the MO file.

Enable .l10n.php for new projects. It is strictly better than MO on the supported WordPress versions.

Why community translation is not enough

WordPress.org offers community translation through GlotPress. In practice, it covers a small fraction of what most plugins and themes need. An analysis of more than 60,000 WordPress plugins and themes found that community translation covers under 5% of translation needs across 40 languages.

Two structural problems explain the gap:

  • Volunteers are scarce in most locales. A handful of plugins with massive user bases attract translators. Most do not.
  • Translations can take months or years to appear, if they appear at all. If you want users to see your plugin or theme in their language from day one, you cannot rely on the community.

This guide assumes you want consistent, full translation coverage on a release schedule you control. That is what PTC delivers.

Preparing your WordPress theme or plugin for translation

A mismatched text domain or an unwrapped string means that text will never appear in your translated output. The details matter.

Step 1: Define your text domain and domain path

Every theme or plugin needs a text domain. The text domain is a unique identifier that tells WordPress which translation files belong to your project. It must match your plugin or theme slug exactly.

Declare it in your plugin's main file header:

<?php
/**
 * Plugin Name: My Plugin
 * Description: An example plugin.
 * Version: 1.0.0
 * Text Domain: my-plugin
 * Domain Path: /languages
 */

Or in your theme's style.css:

/*
Theme Name: My Theme
Text Domain: my-theme
Domain Path: /languages
*/

The domain path tells WordPress where your translation files live relative to the plugin or theme root. /languages is the standard.

Step 2: Wrap your PHP strings in gettext functions

Any string you want translatable needs to be wrapped in one of WordPress's gettext functions. At runtime, those functions look up the correct translation. If none is found, they fall back to the original string.

Basic strings. Use __() when you need to return a string. For HTML output, use the escaped variants. WordPress coding standards recommend echo esc_html__() over _e(). The escaped form makes output escaping explicit and prevents XSS at the point of output:

// Return a translated string
$label = __( 'Settings', 'my-plugin' );

// Echo a translated string, escaped for HTML
echo esc_html__( 'Settings saved.', 'my-plugin' );

Strings with variables. Do not concatenate variables into strings. Translators see only fragments and cannot reorder words for languages with different syntax. Use printf() or sprintf() with a placeholder. Add a translator comment so translators know what %s represents:

printf(
    /* translators: %s: the user's display name */
    esc_html__( 'Welcome back, %s.', 'my-plugin' ),
    esc_html( $display_name )
);

Plural forms. English plurals are simple (one comment, two comments). Other languages are not. Use _n() to handle every plural rule WordPress knows about:

printf(
    esc_html( _n( '%s comment', '%s comments', $count, 'my-plugin' ) ),
    number_format_i18n( $count )
);

Strings that need context. Some words mean different things depending on where they appear. Use _x() to give translators the context they need:

// "Export" as a noun (the file) vs. a verb (the action)
echo esc_html_x( 'Export', 'button label', 'my-plugin' );

Step 3: Internationalize your JavaScript strings

WordPress provides the wp-i18n package so you can use the same gettext functions in JavaScript that you use in PHP. When registering your script, declare wp-i18n as a dependency:

wp_register_script(
    'my-plugin-script',
    plugins_url( 'js/app.js', __FILE__ ),
    array( 'wp-i18n' ),
    '1.0.0',
    true
);

Then in your JavaScript file:

const { __, _n, sprintf } = wp.i18n;

const message = __( 'Settings saved.', 'my-plugin' );

If you use a bundler like Webpack, install @wordpress/babel-plugin-makepot. It extracts translatable strings from your bundle as part of your build.

Step 4: Generate your POT file

Once your strings are wrapped, generate a POT file. The POT is the source file PTC (or any translator) works from. It contains every translatable string but no translations.

wp i18n make-pot . languages/my-plugin.pot

WP-CLI scans your PHP, JavaScript, and block.json files for gettext calls. It compiles them into a single POT. If your team uses Composer, add this command as a Composer script. That keeps WP-CLI usage consistent across the team without a global install.

The full wp i18n command suite covers the rest of the pipeline:

Command What it does
wp i18n make-pot Generate a POT file from source.
wp i18n update-po Sync existing PO files when your POT changes.
wp i18n make-mo Compile PO files into binary MO files.
wp i18n make-json Extract JS strings from PO into JSON files.
wp i18n make-php Generate .l10n.php files (WordPress 6.5+).

You do not need Poedit. Everything in this workflow runs through WP-CLI and PTC. WP-CLI handles POT generation and MO/JSON compilation. PTC handles translation and returns every file format WordPress needs. Poedit is a useful desktop editor for manual translation, but it is not part of this workflow.

Translating POT files with PTC

PTC is built with WordPress developers in mind. Start with a POT file and get back production-ready translation files. The free 30-day trial covers up to 20,000 words across two languages.

Set up your first translation project

Upload your POT file. Choose which output formats you need. PTC returns any combination of:

  • .po files for each target language.
  • .mo files, compiled and ready to ship.
  • .json files for your JavaScript strings.
  • .l10n.php files for faster loading on WordPress 6.5+.

The setup wizard asks you to describe your theme or plugin. PTC uses the description to generate translations with the right tone and context. Add brand-specific terms to the glossary at this stage. The glossary keeps names, feature labels, and any other terminology consistent across all languages.

PTC parses the gettext structure on upload. It recognises placeholders (%s, %1$s, %d), plural forms (_n()-extracted entries with their Plural-Forms header), and contexts (_x() entries with msgctxt). It then generates the correct plural categories per language. Polish gets one / few / many / other. Japanese gets only other. Arabic gets six forms.

Move to continuous localization

Once your first files are translated, upgrade to Pay-As-You-Go. Connect PTC to your GitHub, GitLab, or Bitbucket repository. From that point you do not upload files manually:

# .github/workflows/translate.yml
name: PTC translate
on:
  push:
    branches: [main]
    paths:
      - 'languages/my-plugin.pot'
jobs:
  translate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Generate POT
        run: |
          wp i18n make-pot . languages/my-plugin.pot --domain=my-plugin
      - name: Trigger PTC translation
        run: |
          curl -X POST https://api.ptc.wpml.org/v1/projects/${{ secrets.PTC_PROJECT_ID }}/sync \
            -H "Authorization: Bearer ${{ secrets.PTC_API_KEY }}"

PTC syncs new strings, translates only what changed, and opens a pull request with the updated .po, .mo, .json, and .l10n.php files. See the PTC API reference for the full webhook and REST API.

Whether you commit .mo files to your repository depends on team policy. Many plugins generate them at release time instead. PTC's CI integration produces them on every translation run. Check them in only if you want translated files in version history.

Loading translations in WordPress

With translated files in hand, place them correctly and tell WordPress where to find them. The filename comes first. WordPress looks for translation files using a specific naming pattern. A filename that does not match means the file will not load.

Getting your filenames right

Locale codes follow the language_COUNTRY format. de_DE is German (Germany). fr_FR is French (France). pt_BR is Portuguese (Brazil). zh_CN is Simplified Chinese. The WordPress translation portal lists every supported locale.

The expected filename depends on where you place the file:

Location Pattern Example
Plugin's /languages/ folder {text-domain}-{locale}.mo my-plugin-de_DE.mo
Theme's /languages/ folder {locale}.mo de_DE.mo
Global WordPress language dir (/wp-content/languages/) {text-domain}-{locale}.mo my-plugin-de_DE.mo

Themes use a shorter naming convention when files are bundled inside the theme. In the global language directory, both plugins and themes use the {text-domain}-{locale} pattern.

Loading translations for plugins (PHP)

Register translations with WordPress on the init hook. Do not use plugins_loaded. It triggers a deprecation warning in current WordPress versions:

add_action( 'init', function () {
    load_plugin_textdomain(
        'my-plugin',
        false,
        dirname( plugin_basename( __FILE__ ) ) . '/languages/'
    );
} );

Loading translations for themes (PHP)

Use load_theme_textdomain() hooked to after_setup_theme:

add_action( 'after_setup_theme', function () {
    load_theme_textdomain( 'my-theme', get_template_directory() . '/languages' );
} );

Loading JavaScript translations

After registering your script (Step 3 above), call wp_set_script_translations(). WordPress then loads the JSON translations for that script handle:

add_action( 'init', function () {
    wp_set_script_translations(
        'my-plugin-script',
        'my-plugin',
        plugin_dir_path( __FILE__ ) . 'languages'
    );
} );

Verify the loading

  1. Set your WordPress site language to a target locale. The setting is at Settings > General > Site Language.
  2. Reload the front end and the admin pages your plugin or theme renders.
  3. Strings wrapped in __() or esc_html__() should appear in the new language.

If something is missing, see WordPress plugin translations not showing? Fix missing translations for the most common causes.

Right-to-left languages and bidirectional layouts

The translation workflow is the same for right-to-left (RTL) languages. Arabic, Hebrew, Persian, and Urdu use the same POT/PO/MO pipeline.

The extra step is making sure your theme supports RTL styles. WordPress automatically loads an rtl.css file if one exists in your theme directory. Use the is_rtl() function to conditionally apply RTL-specific styles or scripts.

Localizing dates, numbers, and currencies

A translated string is not the whole story. Dates, numbers, and currencies must also follow the user's locale. Use WordPress's built-in formatting functions instead of PHP's native ones:

  • date_i18n() formats dates according to the active locale.
  • number_format_i18n() formats numbers with locale-aware decimal and thousands separators.

These functions are not part of the translation file workflow. They matter for a fully localized experience.

Internationalizing Block Editor (Gutenberg) blocks

Blocks add two extra steps to the standard pipeline:

  • JavaScript translations need a .json file per locale. Generate it from the .po with wp i18n make-json.
  • The block's editor script needs wp_set_script_translations() in PHP. That tells WordPress to serve the JSON to the block.

The block-rendered output on the front end uses the same __() calls as your other PHP. No extra work there.

Translating your README and WordPress.org listing

Your readme.txt is not a resource file. WP-CLI will not pick it up when generating a POT. To translate it, use PTC's Paste to Translate feature. Paste the content, choose your target languages, and download the result. Customer-facing emails sent from the plugin and the WordPress.org plugin page description translate the same way, all in the same project so terminology stays consistent.

If your plugin or theme is listed on WordPress.org, the translated description appears on the plugin's Details tab in the user's language. To get translations live there, follow the WordPress.org import process.

Translate plugin user content with the PTC API

Plugins that store user-generated data (forum plugins, review plugins, comment plugins) can translate that content as it arrives. The PTC REST API translates user posts, comments, and reviews on demand with Bearer-token authentication, using the same glossary and brand voice as your .po files.

Visual translation review of your rendered theme or plugin - ship without manual QA per language

A translated .po file is necessary, but it is not sufficient. The translated theme or plugin still needs verification:

  • A translated label may overflow a settings-page button in German.
  • "Submit" may translate as a noun in French when the admin action needed a verb.
  • A hardcoded English string outside __() will render untranslated regardless of how many languages you ship.

PTC's visual translation review replaces the manual QA pass. WordPress themes and plugins render in the browser (both wp-admin and the front end). The right flavor is the browser extension.

Install it once. Record a walkthrough of your theme or plugin on a test site. Cover settings pages, admin actions, and front-end output. PTC replays the recording in every target language after every translation update. It captures every screen and reports back two kinds of fixes:

  • Fixes in the .po files when PTC controls them. PTC re-translates a wrong sense, picks a shorter synonym that fits a button, or regenerates a plural form.
  • Cursor or Claude Code prompts when the issue lives in your PHP or JavaScript code. Examples include a missing __() wrapper, a hardcoded English string, or a sentence built by concatenation that should use sprintf( __( ... ) ).

You ship a verified, multilingual plugin per release. The manual QA tail goes away.

Pricing: 30-day free trial, then Pay-As-You-Go

The free trial covers 20,000 words into 2 languages with no credit card. When the trial ends, PTC offers Pay-As-You-Go. No subscription. No minimum commitment. The first 500 words every month are free. You only pay for the rest. The pricing page has a cost calculator. Sign up with a company email for an extended business trial.

Ready to ship a verified plugin or theme?

PTC generates the translations and reviews the rendered plugin. You confirm the result and release. The full loop runs without manual QA:

  1. Generate your .pot file with wp i18n make-pot.
  2. Upload it to PTC and get back .po, .mo, .l10n.php, and .json files in minutes.
  3. Install the browser extension to verify the running plugin in every target language.

Start your free 30-day trial - 20,000 words on us, no credit card required.

Related: