One day I had a prob­lem. On this web­site, I use the Avada theme for Word­Press. As you can see, you can leave a com­ment under every blog post. You can enter a com­ment text, a name, an email and — nor­mally — a web­site ad­dress. Un­for­tu­nately, that ad­dress is click­able by de­fault when the com­ment is shown. As I didn’t want to at­tract spam com­ments, I dis­abled the input of a web­site URL with CSS. Every­thing’s fine.

Or so it seemed. With the ad­vent of the GDPR reg­u­la­tion in the Eu­ro­pean Union (read all about it here), there was a new prob­lem: the input of name and email in a Word­Press com­ment sec­tion is not op­tional, but re­quired by de­fault. Ac­cord­ing to GDPR rules, you are not al­lowed to force users to pass on per­sonal data if they don’t want to. But no prob­lem here, you can make the input of name and email op­tional by uncheck­ing the set­ting Com­ment au­thor must fill out name and email in the Word­Press Set­tings in the sec­tion Dis­cus­sion, sub­sec­tion Other com­ment set­tings. After that, a user can post com­ments with­out en­ter­ing a name or email.

That is, he could do that. The place­holder la­bels in the input fields, even after uncheck­ing that set­ting, still read Name (re­quired) and Email (re­quired). So I don’t have a tech­ni­cal prob­lem, but a legal one. Even though I don’t re­quire users to input name and email, it looks as if I would do that, vi­o­lat­ing the GDPR rules.

The text of these la­bels are con­trolled by the Avada Word­Press theme and can­not be changed eas­ily. My first ap­proach was to cre­ate changed lan­guage files for the Avada theme. As you al­ready might know, these lan­guage files — which are called PO files after their file ex­ten­sion (or MO files, when they are com­piled) — are used for in­ter­na­tion­al­iza­tion and trans­la­tion in Word­Press. As this site is bilin­gual, I had to cre­ate two files: a Ger­man trans­la­tion file and, al­though nor­mally not needed, an Eng­lish trans­la­tion file, which leaves every­thing as it is, ex­cept the two la­bels I wanted to change. So, in the Eng­lish trans­la­tion file, the two la­bels Name (re­quired) and Email (re­quired) were “trans­lated” to Name and Email re­spec­tively.

The prob­lem is, of course, that a change to the trans­la­tion files will re­vert every time after a ver­sion up­date of the Avada theme, or at least after an up­date of the Avada lan­guage files, re­quir­ing to reap­ply that patch. It is also not pos­si­ble to over­ride trans­la­tions in a child theme: you can only cre­ate new trans­la­tions, not over­write ex­ist­ing ones (that is: the func­tion load_textdomain uses the func­tion merge_with of the MO ob­ject where ex­ist­ing keys take prece­dence over new keys).

The so­lu­tion here is to use a fil­ter func­tion to change strings, namely the gettext fil­ter and its de­riv­a­tives. To clearly state the lim­i­ta­tions of this method from the be­gin­ning: you can only change strings which are in­tended by the theme, plu­gin or wid­get to be trans­lated. Ad­di­tion­ally, it would be best to con­fine the num­ber of string re­place­ments to a rea­son­ably small num­ber. Also, this fil­ter func­tion will be called for every trans­lat­able string in your Word­Press in­stal­la­tion: so it’s best to keep it small and fast.

But let’s start. The code we de­velop here will have to be ap­pended to the file functions.php in your theme file di­rec­tory. Or bet­ter: it should be part of the file functions.php in your child theme, to be up­date proof. You don’t have a child theme? Cre­ate one. Or even bet­ter: cre­ate a sep­a­rate PHP file in your child themes folder and in­clude it in the file functions.php .

So to change the place­holder in the com­ment input fields of the Avada theme, the gettext fil­ter might be used like this:

add_filter( 'gettext', 'my_gettext', 20, 3 );
function my_gettext( $msgstr, $msgid, $domain ) {
  if ($msgid == 'Name (required)' && $domain == 'Avada') return 'Name (optional)';
  if ($msgid == 'Email (required)' && $domain == 'Avada') return 'Email (optional, not shown)';
  return $msgstr;
}

We reg­is­ter a fil­ter call­back, which re­ceives three pa­ra­me­ters: $msgstr con­tains the de­fault text as it would be shown with­out the fil­ter func­tion, $msgid con­tains the msgid of the text to be trans­lated and $domain is a key to con­fine the trans­la­tion to a spe­cific named sub­set. This way, the same msgid can be used mul­ti­ple times by dif­fer­ent themes, plu­g­ins or wid­gets. In our case, the Avada Word­Press theme reg­is­ters its lan­guage files with the do­main name “Avada“. The de­fault Word­Press trans­la­tions use the do­main name “de­fault“. If we don’t want to change the string, we just re­turn the de­fault trans­la­tion.

To use a more generic ex­am­ple, we might change the place­holder la­bels of the de­fault Word­Press com­ment input fields, used by e.g. the new stan­dard theme Twenty Nine­teen:

add_filter( 'gettext', 'my_gettext', 20, 3 );
function my_gettext( $msgstr, $msgid, $domain ) {
  if ($msgid == 'Name' && $domain == 'default') return 'Name (optional)';
  if ($msgid == 'Email' && $domain == 'default') return 'Email (optional, not shown)';
  return $msgstr;
}

But it’s a bit more com­plex, a trans­la­tion can also be de­fined within a con­text. That al­lows for dif­fer­ent trans­la­tions in the same do­main when a word in the source lan­guage might have dif­fer­ent trans­la­tions in the tar­get lan­guage de­pend­ing on the con­text. For ex­am­ple, the word “com­ment” can be a noun or a verb, but the trans­la­tion in many lan­guages will be dif­fer­ent. So in the stan­dard Word­Press lan­guage file, the msgid for the word “com­ment” has the con­text “noun“.

There is an­other fil­ter func­tion to allow con­texts, which is named gettext_with_context with an ad­di­tional con­text pa­ra­me­ter named $msgctxt . So if we would want to change the word “com­ment” to, let’s say, “opin­ion“, we can do it like this:

add_filter( 'gettext_with_context', 'my_gettext_with_context', 20, 4 );
function my_gettext( $msgstr, $msgid, $msgctxt, $domain ) {
  if ($msgid == 'Comment' && $msgctxt == 'noun' && $domain == 'default') return 'Opinion';
  return $msgstr;
}

To make it all more prac­ti­cal, we can in­tro­duce an array of trans­la­tions, in this case, an as­so­cia­tive array of ar­rays con­tain­ing msgid , msgctxt , and domain in that order, with msgstr as the key. As gettext_with_context is more generic, we call it from our gettext call­back:

$my_translation_table['Name'] = ['Name (optional)', '', 'default'];
$my_translation_table['Email'] = ['Email (optional, not shown)', '', 'default'];
$my_translation_table['Comment'] = ['Opinion', 'noun', 'default'];

add_filter( 'gettext_with_context', 'my_gettext_with_context', 20, 4 );
function my_gettext_with_context( $msgstr, $msgid, $msgctxt, $domain ) {
  global $my_translation_table;

  if (isset($my_translation_table[$msgid])) {
    if ($my_translation_table[$msgid][1] == $msgctxt) {
      if ($my_translation_table[$msgid][2] == $domain) {
        return $my_translation_table[$msgid][0];
      }
    }
  }

  return $msgstr;
}

add_filter('gettext', 'my_gettext', 20, 3);
function my_gettext($msgstr, $msgid, $domain) {
  return my_gettext_with_context($msgstr, $msgid, '', $domain);
}

There is yet an­other type of get­text fil­ter call­backs. One can de­fine a trans­la­tion for a string in its sin­gu­lar or plural vari­ant, e.g. “1 month” or “2 months“. The call­backs are named ngettext and ngettext_with_context . In our ex­am­ple code, we will add them here in a some­what sloppy way, so a word of warn­ing might be ap­pro­pri­ate: keep in mind that a trans­la­tion might not only de­pend on the cases one or more than one, like e.g. 21st, 22nd, and 23rd. So the code shown here is over­sim­pli­fied, though it will work most cases.

add_filter( 'ngettext', 'my_ngettext', 20, 5 );
function my_ngettext($msgstr, $single, $plural, $number, $domain)
{
  if ($number == 1) {
    return my_gettext_with_context($msgstr, $single, '', $domain);
  } else {
    return my_gettext_with_context($msgstr, $plural, '', $domain);
  }
}

add_filter('ngettext_with_context', 'my_ngettext_with_context', 20, 6);
function my_ngettext_with_context($msgstr, $single, $plural, $number, $msgctxt, $domain)
{
  if ($number == 1) {
    return my_gettext_with_context($msgstr, $single, $msgctxt, $domain);
  } else {
    return my_gettext_with_context($msgstr, $plural, $msgctxt, $domain);
  }
}

So how do we know the id, the con­text and the do­main of a text string or label we want to change? We have to look into the lan­guage files or in the code. The lan­guage files in the Word­Press in­stal­la­tion are lo­cated under /wp-content/languages , /wp-content/languages/plugins , and /wp-content/languages/themes. Open the cor­re­spond­ing PO files and search for the string you want to change. If you are run­ning a Word­Press site in the de­fault US Eng­lish con­fig­u­ra­tion, there will be no de­fault lan­guage file. You can down­load ad­di­tional Word­Press lan­guage files like en_GB or en_CA from this lo­ca­tion and search them with your text ed­i­tor of choice to find the text you are look­ing for. In every other case, the sup­plier of your theme or plu­gin should pro­vide you with an ap­pro­pri­ate lan­guage file.

You might also want to check the source code of the theme or plu­gin in ques­tion in /wp-content/themes or /wp-content/plugins . The best idea might be to use a text ed­i­tor which of­fers a “Find in Files” func­tion. When read­ing the code, it might also be help­ful to know the PHP func­tions, that Word­Press uses for lo­cal­iza­tion. Below is a list of all rel­e­vant func­tions with in­for­ma­tion about their use of con­text or sin­gu­lar/plural vari­ants. Just click on the func­tion name to see its de­f­i­n­i­tion.

Func­tion Re­turn / Dis­play Es­cape func­tion Con­text Sin­gle / Plural
__() Re­turns trans­la­tion no no
_e() Dis­plays trans­la­tion no no
es­c_at­tr__() Re­turns trans­la­tion es­c_attr() no no
es­c_htm­l__() Re­turns trans­la­tion es­c_html() no no
es­c_at­tr_e() Dis­plays trans­la­tion es­c_attr() no no
es­c_htm­l_e() Dis­plays trans­la­tion es­c_html() no no
_n() Re­turns trans­la­tion no yes
_x() Re­turns trans­la­tion yes no
_ex() Dis­plays trans­la­tion yes no
es­c_at­tr_x() Re­turns trans­la­tion es­c_attr() yes no
es­c_htm­l_x() Re­turns trans­la­tion es­c_html() yes no
_nx() Re­turns trans­la­tion yes yes

To round it all up, we can ex­tend the code to use two dif­fer­ent lan­guages. To achieve this, we can in­tro­duce two dif­fer­ent trans­la­tion ar­rays and wrap them into an­other as­so­cia­tive array with the name of the lo­cale as the key. The code of the gettext_with_context call­back has to be slightly ex­tended:

$my_translation_table_en_US['Name'] = ['Name (optional)', '', 'default'];
$my_translation_table_en_US['Email'] = ['Email (optional, not shown)', '', 'default'];
$my_translation_table_en_US['Comment'] = ['Opinion', 'noun', 'default'];

$my_translation_table_de_DE['Name'] = ['Name (Optional)', '', 'default'];
$my_translation_table_de_DE['Email'] = ['Email (Optional, wird nicht angezeigt)', '', 'default'];
$my_translation_table_de_DE['Comment'] = ['Meinung', 'noun', 'default'];

$my_translation_table['en_US'] = $my_translation_table_en_US;
$my_translation_table['de_DE'] = $my_translation_table_de_DE;

add_filter( 'gettext_with_context', 'my_gettext_with_context', 20, 4 );
function my_gettext_with_context( $msgstr, $msgid, $msgctxt, $domain )
{
  global $my_translation_table;

  $locale = apply_filters('theme_locale', determine_locale());

  if (isset($my_translation_table[$locale][$msgid])) {
    if ($my_translation_table[$locale][$msgid][1] == $msgctxt) {
      if ($my_translation_table[$locale][$msgid][2] == $domain) {
        return $my_translation_table[$locale][$msgid][0];
      }
    }
  }

  return $msgstr;
}

You can view and down­load the code at https://github.com/d3v-​one/wp-​gettext-example.