Commercial themes usually ship with language files already, so you just translate that file into your language and you are done. The problem shows up when you write extra functionality in a child theme and want those new strings translatable too.

What a text domain is

Every theme and plugin in WordPress has its own identifier, called a text domain. When you write:

_e( 'Read more', 'web99' );

WordPress reads that as: find the translation of the string Read more in the set of translations named web99. If no translation has been loaded for web99, it prints the original string verbatim.

That is exactly why strings in a child theme do not translate on their own: the parent theme loaded its own text domain, but nobody has loaded yours.

Loading a text domain for the child theme

Add this to your child theme’s functions.php:

<?php
add_action( 'after_setup_theme', 'child_load_text_domain' );

function child_load_text_domain() {
    // Override the parent theme's translations (Flatsome in this example)
    load_theme_textdomain( 'flatsome', get_stylesheet_directory() . '/languages/flatsome' );

    // Load translations for the child theme's own text domain
    load_child_theme_textdomain( 'web99', get_stylesheet_directory() . '/languages' );
}

Those two functions do different jobs:

  • load_theme_textdomain() with the parent theme’s text domain lets you override the existing translations without touching the original files. A parent theme update will not wipe out your work.
  • load_child_theme_textdomain() loads translations for the strings you wrote yourself.

This works with any WordPress theme, not just Flatsome. Replace flatsome with the text domain of the theme you are using, which you find in the parent theme’s style.css or by looking at the second argument in that theme’s __() calls.

Why after_setup_theme

The after_setup_theme hook runs early enough that the translations are ready before any string is printed. Load them later, in init or wp_head for example, and some strings will already have rendered in the original language.

Creating .po and .mo files with Poedit

WordPress does not read plain text files, it reads compiled .mo files. The process:

  1. Download and install Poedit. The free version is enough.
  2. Choose File > New and pick your target language.
  3. Go to Catalog > Properties > Sources paths and point it at the child theme folder.
  4. Switch to the Sources keywords tab and add the WordPress translation functions: __, _e, esc_html__, esc_html_e, esc_attr__, esc_attr_e, _n, _x.
  5. Click Update from code so Poedit scans every string in your code.
  6. Translate each string and save. Poedit produces both a .po and a .mo file.

File naming rules

This is where people get it wrong most often. The filename has to match the locale code exactly, or WordPress will not find it:

  • For load_child_theme_textdomain: name it after the locale, for example vi.po and vi.mo
  • For load_theme_textdomain overriding the parent: name it {text-domain}-{locale}, for example flatsome-vi.mo

The Vietnamese locale in WordPress is vi. You can check the current one by calling get_locale().

Once you have the files, upload them into the folder you declared in the code, which is /languages inside the child theme.

Write the strings correctly in the first place

Translation only works if the strings in your code are written properly:

// Correct: both the string and the text domain are literals
esc_html_e( 'Contact us', 'web99' );

// Wrong: Poedit cannot scan a variable
esc_html_e( $label, 'web99' );

// Wrong: concatenation destroys the context for the translator
_e( 'Only ' . $n . ' left in stock', 'web99' );

// Correct: use a placeholder
printf(
    esc_html__( 'Only %d left in stock', 'web99' ),
    $n
);

Poedit scans source code statically, so anywhere you pass a variable in place of a literal string will simply be missed.

When the translation does not appear

Check these in order:

  1. Does the .mo file exist, and does its name match the locale? Poedit only produces .mo files if the compile option is enabled in Preferences.
  2. Is the site language set correctly under Settings > General?
  3. Does the text domain in your code match the one declared in load_child_theme_textdomain()? A single character difference breaks it.
  4. Is a caching plugin serving an old page? Clear the cache and try again.