Recent Updates Toggle Comment Threads | Keyboard Shortcuts

  • Thorsten Frommen 11:53 am on 2015/12/17 Permalink | Reply  

    MultilingualPress 2.3.2 

    Today, we released version 2.3.2 of MultilingualPress. This is a patch release that fixes four (potential) bugs. Thank you very much for all your contributions.

    Relevant Changes

    • Fix leftover entry from site option included in languages data.
    • Fix potentially invisibe plugin activation row on Add New Site page.
    • Run post_name through urldecode to account for non-ASCII characters.
    • Fix incorrect ml_type value of duplicated custom post type posts.

    Developers: As we would like to use the official GlotPress for translatingMultilingualPress, we will (have to) change the plugin text domain from multilingualpress to multilingual-press with the next (major) release. So, in case you are doing crazy things with our translations (which you basically should really not), please be informed.


    MultilingualPress 2.3.2 is available on our GitHub repository and in the official plugin directory.

  • Thorsten Frommen 12:48 pm on 2015/12/15 Permalink | Reply  

    File Organization 

    Even though MultilingualPress is available on and its SVN server, we use GitHub (i.e., Git) for development and version control. While the SVN repository and the generated ZIP file only contain what the end-user really needs for using MultilingualPress, the GitHub repository also includes additional development files (e.g., resources, and tests). In this article, I would like to present and discuss the file organization of MultilingualPress’s development repository.

    File Organization

    File organization in the development repository.

    The Development Repository

    In the GitHub repository root, you can find individual folders for assets, resources, sources, and tests. And then there is a bunch of files, for instance, several config and markdown files, and a multilingual-press.php file. None of them is required to use the plugin—yes, not even the PHP file.

    So, let’s have a closer look at the folders and files. For the sake of a better understanding, however, we don’t do this in alphabetic order, but in a somewhat logical order.


    The src folder contains what is required to use MultilingualPress, and thus will be shipped to and by In this folder, we currently find assets (images, scripts, and styles), language files (which soon will be removed in favor of the official GlotPress project for MultilingualPress), the license and readme files, and the actual PHP source files.


    The assets folder in the root (not to be confused with src/assets) contains the assets for the SVN repository (i.e., banner and icon images, and screenshots). These files are not required for using the plugin, but only for the official plugin page in the plugin repository.


    For both the back end and the front end, there is only a single CSS file (in a minified version, and in an unminified, debugging-friendly version). The CSS development, however, does not happen by using a single large unminified file, and then compressing it. Instead, the basis for the plugin styles consists of several .scss files that are structured in the resources/scss folder. We use a few reusable modules, and split individual styles into partial files according to their individual context (in terms of plugin features or modules).

    Similar to styles, the shipped plugin scripts are just a single file for each back end and front end (again, in minified and unminified versions). The development of the script files happens on the basis of a base file (e.g., resources/js/admin.js) and several other independent files in a folder named after the base file (i.e., resources/js/admin for resources/js/admin.js).

    Both the assets as well as the plugin images are all shipped in a compressed and web-optimized version. The high-resolution/high-quality originals live in the resources/assets and resources/images folder, respectively.


    Unsurprisingly, the tests folder contains tests. Currently, there are PHPUnit tests for PHP files only, but once we are done with refactoring all JavaScript files there will be QUnit tests, too. The tests are separated into folders named after the individual testing tool (e.g., phpunit for the PHPUnit unit testing framework), and then further structured according to the different test levels. So, QUnit unit tests for JavaScript live in tests/qunit/unit, while tests/phpunit/integration contains PHP integration tests using PHPUnit.

    The Proxy File

    To make working with the development repository as easy as possible, there is a proxy (main plugin) file living in the root. It is named exactly like the (real) main plugin file located in the src folder, and it pretty much only calls (i.e., requires) the real thing. By doing this, you can just clone the GitHub repository into a project’s plugin directory and simply use the plugin as if installed from—no need to copy the contents of the src folder to a new multilingual-press folder, or anything like that.

    The Advantages

    There are several advantages of organizing a plugin like this. Here are the most obvious ones:


    Apart from possible config files, which are located in the root, you always know where to look when you search something. If you want tests, look inside the tests folder. If you don’t want tests at all, you only have to remove the tests folder. When working on styles and/or JavaScript of some (maybe new) module, you don’t have to fight your way through a huge file, but can use a small file dedicated to the very module. You always have the original resource files for anything you ship in a modified version (e.g., images).


    We can have an assets folder in the root that contains the assets, while the actual plugin assets are located in src/assets. No name conflict, no need to make up a name for what actually should be assets anyway.


    Publishing a new release on is as simple as this:

    • clone the repository (GitHub);
    • remove everything from trunk (SVN);
    • copy the contents of the src folder to trunk
    • add a new tag folder (SVN), in case you use tags (which you should);
    • add all new files to SVN;
    • commit.

    Easy, right?

    The Disadvantages

    So far, we’re unaware of disadvantages. Are there any?

  • Thorsten Frommen 7:00 pm on 2015/12/07 Permalink | Reply  

    MultilingualPress 2.3.1 

    Today, we released version 2.3.1 of MultilingualPress. The release is a patch that fixes fixes two potential bugs in case the wp-includes dir has been customized.

    Relevant Changes

    • Fix potentially invalid semi-hard-coded paths.


    MultilingualPress 2.3.1 is available on our GitHub repository and in the official WordPress plugin directory.

  • Thorsten Frommen 12:12 pm on 2015/12/06 Permalink | Reply  

    MultilingualPress 2.3.0 “Saint Nicholas” 

    Today, on Saint Nicholas’ Day, we released version 2.3 of MultilingualPress. The release includes a lot of improvements and great new features. For a full list of all changes, please refer to the changelog. Thank you very much for all your contributions.

    Relevant Changes

    • Hide Redirect UI if the Redirect feature is disabled.
    • Fix missing noredirect query var for all URLs of linked elements.
    • New setting: Fire plugin activation hooks for active plugins when a site has been duplicated.
    • Feature: Show sites with their alternative language title in the admin bar.
    • There are lots of other, minor improvements, too many to list them all.


    MultilingualPress 2.3 is available on our GitHub repository and in the official WordPress plugin directory.

  • Thorsten Frommen 1:11 pm on 2015/09/08 Permalink | Reply  

    MultilingualPress 2.2.3 

    Today, we released version 2.2.3 of MultilingualPress. The release is a patch that fixes a bug with the Translation meta box. Thank you very much for all your contributions.

    Relevant Changes

    • Bugfix Translation meta box not visible.


    MultilingualPress 2.2.3 is available on our GitHub repository and in the official WordPress plugin directory.

  • Thorsten Frommen 8:32 pm on 2015/09/03 Permalink | Reply  

    MultilingualPress 2.2.2 

    Today, we released version 2.2.2 of MultilingualPress. The release is a patch that fixes a UI bug introduced in version 2.2.1, and allows to use MultilingualPress via symlink. Thank you very much for all your contributions.

    Relevant Changes

    • Bugfix term auto-selecting, again.
    • Use realpath() for plugin file in requirements check to allow for symlinked plugin folder.


    MultilingualPress 2.2.2 is available on our GitHub repository and in the official WordPress plugin directory.

  • Thorsten Frommen 10:01 am on 2015/08/31 Permalink | Reply  

    MultilingualPress 2.2.1 

    Today, we released version 2.2.1 of MultilingualPress. The release is a patch that fixes a UI bug introduced in version 2.2, and two other (possible) problems. Thank you very much for all your contributions.

    Relevant Changes

    • Bugfix auto-selecting the first remote term without relationships.
    • Handle deletion of post relations no matter from what site.
    • Improve validity check for table names (don’t be more restrictive than WP core).


    MultilingualPress 2.2.1 is available on our GitHub repository and in the official WordPress plugin directory.

  • Thorsten Frommen 1:55 pm on 2015/08/27 Permalink | Reply  

    MultilingualPress 2.2.0 “Michael Ende” 

    Today, we (finally) released version 2.2 of MultilingualPress. The release includes a lot of improvements and great new features. For a full list of all changes, please refer to the changelog. Thank you very much for all your contributions.

    Since the story of MultilingualPress 2.2 is somewhat like the Neverending Story, it was pretty obvious to dedicate this version to Michael Ende.

    Relevant Changes

    • MultilingualPress Pro and Free are merged now.
    • If specified, the Custom Name is used in get_name().
    • Site Relations do not exclude non-public sites anymore.
    • When duplicating an existing site, you can now set the search engine visibility of the new site.
    • In the Network admin, the site table now lists the Site Language.
    • The html tag now includes the current blog language. Also, the content and the quicklinks include the according hreflang attribute value.
    • While doing AJAX, do not redirect, no matter if admin-ajax.php or not.
    • Translations now can be saved as long as either a title or content is given. This is how WordPress core behaves.
    • The (new) Advanced Translator now includes post slug and post excerpt fields.
    • Translation meta boxes are only visible to users, who have the required capability to edit the according translation post.
    • When saving a post with unsaved Relationship changes, a confirmation pops up.
    • Make no-redirect links set the session to not redirect.
    • You can now remove all terms of a remote post in the Advanced Translator.
    • Cache various internal queries.
    • We introduced several filters. For a comprehensive documentation, please refer to the according Wiki page.
    • The long deprecated filters mlp_pre_save_postdata and mlp_pre_update_post have been removed.
    • The function get_blog_language() has been deprecated in favor of mlp_get_blog_language().
    • There are hundreds of other, minor improvements, too many to list them all.


    MultilingualPress 2.2 is available on our GitHub repository and in the official WordPress plugin directory.

  • Thomas Scholz 2:53 pm on 2015/02/11 Permalink | Reply

    How to get translations programmatically 

    The most important API in MultilingualPress for you is probably the Language API. This API has a method get_translations() that you can use to get a prepared set of translations for posts of any post type, terms of any taxonomy, a search term, the front page or a blog page.

    You can access that API with a filter:

    $mlp_language_api = apply_filters( 'mlp_language_api', NULL );

    MultilingualPress will transform that NULL value now into an instance of the class Mlp_Language_Api. In other words: The variable $mlp_language_api is an object now. But you should still test that, just in case the user has deactivated MultilingualPress:

    $mlp_language_api = apply_filters( 'mlp_language_api', NULL );
    if ( ! is_a( $mlp_language_api, 'Mlp_Language_Api_Interface' ) )

    As you can see, you should test against the Interface Mlp_Language_Api_Interface, not against the concrete class. This enables other plugins to replace our implementation with a custom translation handler.

    Today, we are looking just at $mlp_language_api->get_translations( $args );

    Arguments for Mlp_Language_Api::get_translations()

    $args is an array, we can pass some options here to tweak the results.

    Name Type Description
    site_id int Base site. Usually the current site.
    content_id int post or term_taxonomy ID, not term ID.
    type string Either post, term, post_type_archive, search or front_page.
    strict bool When TRUE (default) only matching exact translations will be included.
    search_term string If you want to translate a search.
    post_type string For post type archives.
    include_base bool Include the base site in returned list.

    All parameters are optional. MultilingualPress will try to find proper values for them. We recommend to set the content_id for terms and posts though, because that is not always available, at least not in a reliable way.

    Now let’s see how our code could look like:

    $mlp_language_api = apply_filters( 'mlp_language_api', NULL );
    if ( ! is_a( $mlp_language_api, 'Mlp_Language_Api_Interface' ) )
    $args = array (
    	'strict'               => TRUE,
    	'include_base'         => TRUE
    /** @var Mlp_Language_Api_Interface $mlp_language_api */
    $translations = $mlp_language_api->get_translations( $args );
    if ( empty ( $translations ) )

    Note that $mlp_language_api->get_translations( $args ) will return an empty array if there are no translations even when we set include_base to TRUE.

    Now, let’s say the translations are not empty. We get an array of objects, each an instance of Mlp_Translation which implements the Mlp_Translation_Interface. That sounds complicated, but it just means that we have a set of methods on each object to get information about the translation.

    Methods for Mlp_Translation

    Method Return type Description
    get_source_site_id() int The site ID the translation is based on.
    get_target_site_id() int The ID of the site where the translation can be found.
    get_page_type() string Either post, term, post_type_archive, search or front_page.
    get_icon_url() Mlp_Url_Interface An object, an instance of a class implementing the Mlp_Url_Interface. It has a magic method __toString(), so we can cast it to a string and get an escaped URL.
    get_target_title() string The title of the translation, for example the post title or the term name.
    get_target_content_id() int The term_taxonomy_id or the post id. This is empty for other translation types like post type archives or search.
    get_remote_url() string The URL for the translation.
    get_language() Mlp_Language_Interface An object, an instance of a class implementing the Mlp_Language_Interface.

    The Mlp_Translation::get_language() object deserves an explanation. It has three public methods.

    Methods for Mlp_Language

    Method Return type Description
    get_priority() int A number between 0 and 10. See the post about Language negotiation for an explanation.
    is_rtl() bool Whether the translation is in a right-to-left language (like Hebrew) or not.
    get_name( $name ) string Different representations of the language. Default is the language in its native writing, eg. Deutsch for German. We strongly recommend to use that, because that’s most easily to recognize for your readers.
    Other allowed parameters are english to get the English name, http to get the HTTP value (for example de-AT) or custom to get the custom name you have set in the site properties.
    You can also use language_short to get just the first part of a language code with subsets, eg. just de.

    Example: Add translation links to the post content

    Let’s see what we can do with all this code. The following example adds very simple translation links to the post content. It uses the first part of the language code and sets it to uppercase. The images are used too, if they are available.

    add_filter( 'the_content', function( $content ) {
        if ( ! is_singular() )
            return $content;
        $mlp_language_api = apply_filters( 'mlp_language_api', NULL );
        if ( ! is_a( $mlp_language_api, 'Mlp_Language_Api_Interface' ) )
            return $content;
        $args = array (
            'strict'               => TRUE,
            'include_base'         => TRUE
        /** @var Mlp_Language_Api_Interface $mlp_language_api */
        $translations = $mlp_language_api->get_translations( $args );
        if ( empty ( $translations ) )
            return $content;
        $links = array();
        /** @type Mlp_Translation_Interface $translation */
        foreach ( $translations as $translation ) {
            $current = $img = '';
            if ( $translation->get_target_site_id() === get_current_blog_id() )
                $current = ' class="current"';
            $img_url = $translation->get_icon_url();
            if ( '' !== (string) $img_url )
                $img = "<img src='$img_url' alt=''> ";
            $text = $translation->get_language()->get_name( 'language_short' );
            $text = mb_strtoupper( $text, 'UTF-8' );
            $links[] = sprintf(
                '<a href="%1$s" title="%2$s" %3$s>%4$s</a>',
                esc_attr( $translation->get_target_title() ),
                $img . $text
        $links = '<p class="translations">'
            . join( ' <span class="separator">|</span> ', $links )
            . '</p>';
        return $content . $links;

    The result should look like this: Screenshot

    Theme integration

    You can use such a function in other places too, of course. In a theme you should add a custom action wherever you need it and assign a callback handler to that action. This way, your theme will not break when the user deactivates MultilingualPress.

    So in a template file add this line:

    do_action( 'translation_box' );

    And in your functions.php create a callback function and register it for that action:

    add_action( 'translation_box', 'show_mlp_translation' );
    function show_mlp_translation() {
        // find and print translation links

    Any questions or suggestions? Or do you have used this tutorial successfully? Please let me know.

  • Thomas Scholz 1:17 pm on 2014/10/28 Permalink | Reply  

    How to disable broken save_post callbacks 

    Many plugins are not multisite aware. This is rarely a problem, because each site in a network works almost like a normal site in a single-site installation. But sometimes … things go really wrong.

    There is a hook, the action save_post, that can be called multiple times when a post is saved: one time for each site. Many plugins are not aware of this, they run their own code on every call to save_post without a check for the site context. The result is that they are either deleting user data on the other sites, or they overwrite existing data.

    This happens when MultilingualPress updates the post translations.

    Site A save_post - MultilingualPress {
        creates or updates translations:
        - switch_to_blog( Site B) -> save_post
        - switch_to_blog( Site C) -> save_post
        - switch_to_blog( Site D) -> save_post
        return to Site A

    MultilingualPress removes all POST data before the other save_post actions are called, and it restores it when it switches back. Normal plugins use a nonce as a basic security check before they try to save anything. Since we remove the nonce along with the POST data, they will not do anything. So far, so good.

    Unfortunately, some plugin author don’t use nonces. They just try to save their data, without proper context checks. There is nothing we can do about that.

    But you can. You have to find the code (function or class method) that is registered for the save_post action and then remove it earlier. There is another hook that runs right before save_post: the filter wp_insert_post_data. You can hook your own custom callback to that filter, check if the current context is in a switched site and then remove the “evil” callback. Don’t forget to return the filtered value, that’s necessary for filters.

    Here is an example for the Custom Sidebars plugin:

    add_filter( 'wp_insert_post_data', function( $data ) {
        if ( is_multisite()
            && ms_is_switched()
            && class_exists( 'CustomSidebarsEditor' )
        ) {
            $cse = CustomSidebarsEditor::instance();
            remove_action( 'save_post', array( $cse, 'store_replacements' ) );
        return $data;

    If you are a plugin or theme author and want to be sure that your code is safe to run in a multisite: please contact me. I,or one of my colleagues, will look at it and tell you what needs to be changed.

Compose new post
Next post/Next comment
Previous post/Previous comment
Show/Hide comments
Go to top
Go to login
Show/Hide help
shift + esc