Skip to content

Instantly share code, notes, and snippets.

@kailoon
Last active July 1, 2023 08:54
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save kailoon/01fa8e95d2e910e666c6 to your computer and use it in GitHub Desktop.
Save kailoon/01fa8e95d2e910e666c6 to your computer and use it in GitHub Desktop.
<?php
/**
* Escape all translations with
*/
__( ‘Some String’, ‘text-domain’ ); _e( ‘Some string’, ‘text-domain’ );.
/**
* When there is no HTML use:
*/
esc_html__( ‘Some String’, ‘text-domain’ ); esc_html_e( ‘Some String’, ‘text-domain’ );
/**
* For some HTML:
*/
wp_kses( __( ‘Some String something’, ‘text-domain’ ), $allowed_html_array );
@varunsridharan
Copy link

I'll just leave this for other authors receiving this reject reason:

All strings must be escaped with esc_html__

Have a look here:

* https://vip.wordpress.com/2014/06/20/the-importance-of-escaping-all-the-things/

* [Automattic/_s#556](https://github.com/Automattic/_s/issues/556)

TL;DR
The new standard is: __() and _e() can only be used if you wrap them in wp_kses()

@pyronaur

i am little confused about it.

Is this an official requirement by WordPress.org ? or just by envato ?

@pyronaur
Copy link

It's a community-accepted standard by now to escape everything. I'm not sure if the WordPress.org theme repository requires it at the moment, but it's almost an universal recommendation these days to escape everything.

You can't go wrong with escaping :)

@ajayshrivas
Copy link

ajayshrivas commented Jun 24, 2022

  1. STILL. All theme text strings are to be translatable and properly escaped.

https://gist.github.com/kailoon/01fa8e95d2e910e666c6

function themecheck_add_page() {
$page = add_theme_page( 'Envato Theme Check', 'Envato Theme Check', 'manage_options', 'envato_theme_check', array( $this, 'themecheck_do_page' ) );
add_action('admin_print_styles-' . $page, array( $this, 'load_styles' ) );
}

https://github.com/envato/envato-theme-check/blob/master/theme-check.php
https://developer.wordpress.org/reference/functions/add_menu_page/
/**

  • Create admin Page to list unsubscribed emails.
    */
    // Hook for adding admin menus
    add_action('admin_menu', 'wpdocs_unsub_add_pages');

// action function for above hook

/**

  • Adds a new top-level page to the administration menu.
    */
    function wpdocs_unsub_add_pages() {
    add_menu_page(
    __( 'Unsub List', 'textdomain' ),
    __( 'Unsub Emails','textdomain' ),
    'manage_options',
    'wpdocs-unsub-email-list',
    'wpdocs_unsub_page_callback',
    ''
    );
    }

/**

  • Disply callback for the Unsub page.
    */
    function wpdocs_unsub_page_callback() {
    echo 'Unsubscribe Email List';
    }
    What is right way ?

If you’re a thorough plugin developer you’re internationalizing (i18n) all your strings. WordPress includes several functions that allow the programmer to easily make his plugin usable natively worldwide:

__()
_e()
_x()
_ex()
_n()
In some older plugins you may find the function _c() which is deprecated and replaced by _x() since version 2.9. If you’re not familiar with i18n of your plugin you should take a look at the Codex.

All these functions translate a string to the language defined in wp-config.php. So what’s the difference between them?

__() and _e() are the simplest functions: they return or echo the translated string. Their usage should be obvious: one string, one translation.
Not so obvious is the function of _x(). Suggest you have two strings in two different contexts in your plugin which are totally the same in your language. Are you sure they are also the same in the remaining 3000+ languages spoken on earth? Even Sebastian Heine is unlikely to know it so you shouldn’t be sure neither.
Fortunately the creators of WordPress have a solution in their bag: _x().This function contains an additional parameter:

string _x (string $text, string $context, [string $domain = 'default'])
By using the parameter $context you can differentiate identical strings in different contexts. Simple, eh? The remaining problem is to define two different string in the .po file. That’s not too difficult neither:

msgctxt "test1"
msgid "testing"
msgstr "context1"

msgctxt "test2"
msgid "testing"
msgstr "context2"
The magic token ist msgctxt which you might translate to “message context”. The code

echo 'Context: test1 -> ' . _x('testing', 'test1', 'test');
echo '
Context: test2 -> ' . _x('testing', 'test2', 'test');
would create the output

Context: test1 -> context1
Context: test2 -> context2
The remaining function _ex is a combination of _e and _x: it echoes a translated string using a context.

If you’re using a visual editor for your .pot files you should be aware that not every editor is capable of handling context definitions and might destroy your data.

Last but not least _n() for retrieving the plural or single form based on the amount.

_n( $single, $plural, $number, $domain = 'default' )
If the domain is not set in the $l10n list, then a comparison will be made and either $plural or $single parameters returned. The function returned via the filter ngettext and about this you can filter the returned strings.

Update (10 JUN 2011): The above explanation of _n() seems to have confused some people. Maybe a few examples will clarify the usage of the function.

A simple example of _n() would be:

$domain = 'test';
$comment_count = 1;
echo _n('comment', 'comments', $comment_count, $domain) . '
';
$comment_count = 2;
echo _n('comment', 'comments', $comment_count, $domain);
the corresponding German (de_DE) language file entries are:

msgid "comment"
msgid_plural "comments"
msgstr[0] "Kommentar"
msgstr[1] "Kommentare"
and the output:

Kommentar
Kommentare
which are the correct singular and plural translations.

If you’re planning to output the value of the numbers you have to use sprintf(). Here’s an example:

$approved = 1;
echo sprintf( _n( '%s comment approved', '%s comments approved', $approved, 'test' ), $approved);
echo '
';
$approved = 2;
echo sprintf( _n( '%s comment approved', '%s comments approved', $approved, 'test' ), $approved );
The code might look a bit confusing so let’s rewrite it:

$approved = 1;
$text = _n( '%s comment approved', '%s comments approved', $approved, 'test' );
echo sprintf($text, $approved);
echo '
';

$approved = 2;
$text = _n( '%s comment approved', '%s comments approved', $approved, 'test' );
echo sprintf($text, $approved);
First a correctly translated singular or plural string is stored in $text. Then this string is evaluated by sprintf() which replaces the specifier “%s” with the value of $approved.

Using the following language file definitions

msgid "%s comment approved"
msgid_plural "%s comments approved"
msgstr[0] "%s Kommentar genehmigt"
msgstr[1] "%s Kommentare genehmigt"
the output is

1 Kommentar genehmigt
2 Kommentare genehmigt
Of course there’s also a combination of _n() and _x() called _nx()

function _nx($single, $plural, $number, $context, $domain = 'default')
TA

/***********************************************************************************************************/

In a talk at the WordCamp Spain online 2021 Adriá Cobo asked me one interesting question about these 2 functions (e() and esc_html_()):

If everything that is printed must be escaped, what is the point of _e()? Is it a legacy code?

In this post, I try to answer this question.

The _e( string $text, string $domain = ‘default’ ) function displays translated text. Its parameters are:

$text: Text to translate.
$domain: Text domain. Unique identifier for retrieving translated strings.
It was introduced in the 1.2.0 version.

The esc_html_e( string $text, string $domain = ‘default’ ) function displays translated text that has been escaped for safe use in HTML output. It has the same parameters that the _e() function.

It was introduced in the 2.8.0 version with other escape functions.

First, I want to talk about the use of these functions. As you can see, both functions have the same parameters, and both display the translated text, but the esc_html_e() function escapes the translated text. This is the main difference. Why we should use the escaped version? Mainly because translations are done by third parties, both professionals and volunteers, who may introduce different layout problems (a new HTML tag, an unclosed tag) or security problems (the execution of a script). We cannot rely on the work of third parties, as they can accidentally or intentionally introduce problems through the translations: treat internationalized strings like you would any other untrusted input. More about this on the internationalization security guide in the Plugin Handbook.

So why do we have an unescaped function?

I think the main cause is due to historical reasons, because the e() function was released on May 22, 2004 and the esc_html_e() function was released on June 11, 2009, so all the code wrote between these 2 dates use the unsecure function. One possible solution could be to set a deprecation date, so after a certain date, this function would not work. But this would surely bring a lot of broken themes and plugins. Even, WordPress core uses 246 times the _e() function, while the esc_html_e() function is only used 31 times.

In resume, always use the escaped functions, and be advised that the unsecured functions will be here with us a long time, maybe forever.

All this stuff applies to other translation functions:

() ⇾ esc_html()
_e() ⇾ esc_html_e()
_x() ⇾ esc_html_x()
() ⇾ esc_attr()
_e() ⇾ esc_attr_e()
x() ⇾ esc_attr_x()
More info:

Internationalization Security.
When outputting a static string to the page, is it necessary to escape the output?
Using _e() or __() to translate text with div

echo '

';
esc_html_e( 'static string', 'text-domain' );
echo '

';
If you want to translate posts and other database user entered content, use a translation plugin, not the localisation API.

But Why?
You should never use variables as the first parameter of a localisation API call.

Aside from being extreme bad practice, it means the string will not get picked up by tools, so it's impossible to generate translation files to translaters.

It does not matter how you pass $var to the function, it will always be incorrect. Do not pass variables to localisation functions in WordPress. These functions are intended for static strings, not dynamic values.

If you want to localise content from the database, this API is not the way to do it.

HTML tags
Localised strings shouldn't include H2 tags and other HTML tags, but instead provide the text content that goes in those tags. There are rare cases when you do, in which case __() and wp_kses_post or sprintf can be useful.

_e() vs __()
_e() is equivalent to echo __()

Why are _e and __ wrong?
Because neither do escaping.

This is the canonical correct answer:

echo '

';
esc_html_e( 'static string', 'text-domain' );
echo '

';
esc_html_e( is shorthand for echo esc_html( __(. We know that inside the H2 tag there will be text, but no tags, so we use esc_html to escape any HTML that appears

If this were an attribute of a tag, then it would be attribute=""

So then, why does __( etc exist? Because escaping has to happen as late as possible, and only once. Otherwise we run the risk of double escaping strings.

Fixing The printf example
We can escape the localised string prior to passing it to printf allowing it to be escaped:

printf( '

%s

', esc_html__( 'static string','text-domain' ) );
So How Do You Translate Dynamic Content?
If you want to enable posts/terms/titles/etc in multiple languages, you need a translation plugin. The i18n APIs are for hardcoded static strings, not database or generated content.

@vkurko
Copy link

vkurko commented Jul 10, 2022

@kailoon

If I need to pass translations using wp_localize_script() to a JS application written in Vue.js or Svelte, where those texts would be escaped during rendering anyway, would it be a problem for an Envato reviewer the usage of __() function?


Let me explain in more detail.

Let's take a simple template that displays the contents of a variable:

// In Vue
<div>{{i18n.text}}</div>

// In Svelte
<div>{i18n.text}</div>

I would pass the text to JS like this:

wp_localize_script( 'my-app.js', 'i18n', array(
  'text' => __( 'Say "hello"', 'textdomain' )
) );

Since both Vue and Svelte render the expression as text and not as html, we will get the desired result (XSS vulnerability or broken html is ruled out in this case):

image

If I use esc_html__ instead of __():

wp_localize_script( 'my-app.js', 'i18n', array(
  'text' => esc_html__( 'Say "hello"', 'textdomain' )
) );

Then we get the wrong result because there is redundant escaping on the PHP side:

image

Sorry to have to ask this question here, but maybe it will be helpful to other developers who, like me, are trying to go through the review process of their plugin and are wondering why they are getting a soft rejection.

Thanks in advance for your reply.

@ajayshrivas
Copy link

Hello vkurko,
https://developer.wordpress.org/reference/functions/esc_html__/

"If I use esc_html__ instead of (): " You can user esc_html

esc_html() escapes a string so that it is not parsed as HTML. Characters like < are converted to <, for example. This will look the same to the reader, but it means that if the value being output is <script> then it won't be interpreted by the browser as an actual script tag.

Use this function whenever the value being output should not contain HTML.

esc_attr() escapes a string so that it's safe to use in an HTML attribute, like class="" for example. This prevents a value from breaking out of the HTML attribute. For example, if the value is "><script>alert();</script> and you tried to output it in an HTML attribute it would close the current HTML tag and open a script tag. This is unsafe. By escaping the value it won't be able to close the HTML attribute and tag and output unsafe HTML.

Use this function when outputting a value inside an HTML attribute.

esc_url() escapes a string to make sure that it's a valid URL.

Use this function when outputting a value inside an href="" or src="" attribute.

esc_textarea() escapes a value so that it's safe to use in a <textarea> element. By escaping a value with this function it prevents a value being output inside a <textarea< from closing the <textarea> element and outputting its own HTML.

Use this function when outputting a value inside a <textarea> element.

esc_html() and esc_attr() also have versions ending in __(), _e() and _x(). These are for outputting translatable strings.

WordPress has functions, __(), _e() and _x(), for outputting text that can be translated. __() returns a translatable string, _e() echoes a translatable string, and _x() returns a translatable string with a given context. You've probably seen them before.

Since you can't necessarily trust a translation file to contain safe values, using these functions when outputting a translatable string ensures that the strings being output can't cause the same issue described above.

Use these functions when outputting translatable strings.

esc_html would be used inside of html for example between a

tag

esc_attr would be used for escaping [attribute values](https://wp-qa.com/filter-woocommerce-archive-pages-with-product-attribute-values-by-default) on html tags like so:

applying _e to the end is for using it with text domains and will automatically echo it for you e.g:

@vkurko
Copy link

vkurko commented Jul 11, 2022

You can user esc_html

No, I can't, because the result will be the same. The problem is that escaping cannot be double. If it happens in a JS application, then you shouldn't escape texts in PHP.

Are you familiar with React, Vue or Svelte? In these frameworks, text values are rendered exactly as texts, and to render html, you need to use special syntax. Thus, you cannot escape texts in PHP because they will appear escaped on the page.

Please look at this example:
https://svelte.dev/repl/12ead03462944e4f9b2811025d37075a?version=3.49.0

It displays 2 strings, the first one with untouched text, the second one after being processed by esc_html() on the PHP side. It is clear that in the second case, we get the wrong result due to redundant escaping in PHP.

@ngothoai
Copy link

ngothoai commented Jul 1, 2023

  1. All theme text strings are to be translatable and properly escaped. https://gist.github.com/kailoon/01fa8e95d2e910e666c6 example(s) from your code and there are more: https://envato.d.pr/fWcY4T
    @kailoon could you explain for me clear more about this point. What's it wrong?
    Envato required using function esc_html__() inserted __()?

Exactly, We can not use (), Only verify esc_html()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment