Updates from December, 2015 Toggle Comment Threads | Keyboard Shortcuts

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

    File Organization 

    Even though MultilingualPress is available on WordPress.org and its SVN server, we use GitHub (i.e., Git) for development and version control. While the WordPress.org 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.

    Sources

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

    Assets

    The assets folder in the root (not to be confused with src/assets) contains the assets for the WordPress.org 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 WordPress.org plugin repository.

    Resources

    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 WordPress.org 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.

    Tests

    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 WordPress.org—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:

    Separation

    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).

    Assets

    We can have an assets folder in the root that contains the WordPress.org 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.

    Release

    Publishing a new release on WordPress.org 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 2:53 pm on 2015/02/11 Permalink | Reply
    Tags:   

    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' ) )
    	return;

    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' ) )
    	return;
    
    $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;

    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>',
                $translation->get_remote_url(),
                esc_attr( $translation->get_target_title() ),
                $current,
                $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.

     
  • Thorsten Frommen 9:37 am on 2014/10/20 Permalink | Reply  

    A basic plugin for MultilingualPress 

    Plugins for MultilingualPress should wait until MultilingualPress is actually loaded. That means they should use a hook provided by the main plugin to be sure that MultilingualPress is running and ready to be used. There are three main entry hooks:

    1. inpsyde_mlp_init: equals to plugins_loaded, 0 plus MultilingualPress loaded. Happens before scripts and stylesheets are registered. The only parameter is an instance of the class Inpsyde_Property_List which holds useful plugin information.
    2. inpsyde_mlp_loaded: Almost the same, but after the scripts are registered.
    3. mlp_and_wp_loaded: equals to wp_loaded, 0 plus MultilingualPress loaded. The first parameter is again the same Inpsyde_Property_List. When in doubt use this action.
      wp_loaded happens after WordPress has checked if the current blog was marked as spam. Unless you want to run your code on spam blogs, wait for this action.

    When we use type hints in our callback function, we should declare the dependency for the first parameter against the interface Inpsyde_Property_List_Interface, not against a concrete class.

    First example

    (All code examples here require at least PHP 5.3.)

    add_action(
        'mlp_and_wp_loaded',
        function( Inpsyde_Property_List_Interface $mlp_data ) {
        // your code here
    });

    The class Inpsyde_Property_List

    This class is very similar to a stdClass from the PHP core: just some key-value pairs. There are two important differences: an Inpsyde_Property_List can be made immutable, and it can have a parent class.

    To make an instance of this class immutable, we call its method freeze(). Once that is done, there is no way to change any data of this class. Any attempt to do that will return an instance of WP_Error. We can test that with the method is_frozen() which returns true or false, depending on the current state.
    The instance we get from MultilingualPress is always frozen. We can rely on its data, because no matter how many plugins access it, they cannot change it.

    What should we do if we want an instance with almost the same data, but some of them with different values? We create a new instance and use the existing one as its parent. Now we can change the values for our internal use and use the other values as if they were properties of our new instance.

    Usage example for Inpsyde_Property_List

    add_action(
        'mlp_and_wp_loaded',
        function( Inpsyde_Property_List_Interface $mlp_data ) {
    
            $ours = new Inpsyde_Property_List;
            $ours->set_parent( $mlp_data );
    
            $ours->plugin_url = plugins_url( '/', __FILE__ );
            $ours->css_url    = "{$ours->plugin_url}css/";
            $ours->js_url     = "{$ours->plugin_url}js/";
            $ours->image_url  = "{$ours->plugin_url}images/";
    
            $ours->freeze();
    });

    This approach avoids common problems with classic inheritance, like the fragile base class and statically linked dependencies. We could call it delayed inheritance. The concept is explained in Steve Yegge’s article The Universal Design Pattern.

    We will look at the various default properties from MultilingualPress in a separate article. For custom code, we need just one important property:

    The autoloader

    The property loader holds an instance of the class Inpsyde_Autoload, a very simple Collection Pipeline for instances of the Inpsyde_Autoload_Rule_Interface which handle the actual class loading. Sounds complicated, but it is dead simple.

    Let’s say our plugin is organized like this:

    - plugin.php // the main file
    - css/ // stylesheets
    - js/ // scripts
    - php/ // PHP classes
        - Plugin_Name_Controller.php
        - Plugin_Name_Model.php
        - Plugin_Name_View.php

    Now we want set up an autoloader to get our classes when we need them. There is one existing class for that, we can reuse it to create a new auto-load rule: Inpsyde_Directory_Load. It takes a path to a directory, and we pass its instance to the Inpsyde_Autoload instance provided by $mlp_data->loader.

    Usage example for Inpsyde_Autoload

    add_action(
        'mlp_and_wp_loaded',
        function( Inpsyde_Property_List_Interface $mlp_data ) {
    
            $load_rule = new Inpsyde_Directory_Load( __DIR__ . '/php' );
            $mlp_data->loader->add_rule( $load_rule );
    
            // We can just use our classes now, no 'require' needed.
            $model      = new Plugin_Name_Model( 'foo' );
            $view       = new Plugin_Name_View( $model );
            $controller = new Plugin_Name_Controller( $model, $view );
            $controller->setup();
    });

    All we have to take care of is that the class names match the file names. This works for interfaces and traits too. Inpsyde_Directory_Load works with flat directories only, it doesn’t search in child directories. It is very fast, because the complete directory is read just once. There is no separate file_exists() check for every class name.

    And that’s all we need for a start: Hook into mlp_and_wp_loaded, register the directory for the auto-loader with the help of the property list – and write awesome code!

     
  • Thorsten Frommen 11:03 am on 2014/10/17 Permalink | Reply  

    How to add icons to the language nav menu items 

    By default, our language navigation menus show the language name in its native spelling. This intended: We want to discourage the use of flags for languages. But we do offer the option to use a custom image for each site, and if you want to use that, you can add it to the link text.

    The hook for that is mlp_prepare_nav_menu_item_output. You get two variables as input:

    1. $item: The nav menu item, an instance of the class WP_Post with some extra properties.
    2. $translation: An instance of an implementation of the Mlp_Translation_Interface. If we couldn’t find a translation for some reason, this variable is NULL, so you have to test it before you work with it.

    The $translation has a method get_icon_url() which is again an object: an instance of the Mlp_Url_Interface. It can be casted to a string to get the URL. Another way is calling the method __toString() directly. If there is an icon, the escaped URL is returned. You get an empty string otherwise.

    Here is how you can use it:

    /**
     * Adds an icon to the menu text
     *
     * $item is passed as an object handle, so this function can change it directly
     * and doesn't have to return anything.
     * We still return a string value to make unit tests easier.
     *
     * @link    http://make.multilingualpress.pro/?p=167
     * @wp-hook mlp_prepare_nav_menu_item_output
     * @param   WP_Post                        $item
     * @param   Mlp_Translation_Interface|NULL $translation Might be NULL
     *                       if there is no translation for the item or
     *                       the other site is not linked to the current
     *                       site.
     * @return  string  When and why we stopped.
     */
    function mlp_add_icons_to_menu( WP_Post $item, $translation = NULL ) {
    
    	if ( ! is_object( $translation ) )
    		return 'no translation object';
    
    	if ( ! class_implements( $translation, 'Mlp_Translation_Interface' ) )
    		return 'invalid translation object';
    
    	if ( FALSE !== strpos( $item->title, '<img' ) )
    		return 'icon already present';
    
    	/* $translation->get_icon_url() returns an instance of
    	 * Mlp_Url_Interface, so we have to cast it to a string
    	 * before we check it with empty().
    	 * @see Mlp_Url_Interface
    	 */
    	$icon_url = (string) $translation->get_icon_url();
    
    	if ( empty ( $icon_url ) )
    		return 'no icon available';
    
    	$item->title = "<img src='$icon_url' alt=''> $item->title";
    
    	return 'success';
    }
    
    add_action(
    	'mlp_prepare_nav_menu_item_output', // hook
    	'mlp_add_icons_to_menu',            // callback
    	10,                                 // priority
    	2                                   // number of accepted arguments
    );

    Download as plugin: MultilingualPress Addon: Nav Menu Icons. Activate it as a network plugin, and it will just work without further configuration.
    You might have to adjust the image position in your stylesheet. Example:

    .mlp-language-nav-item img {
        vertical-align: middle;
    }

    Here is a screenshot of such a changed menu with our built-in flags. Again: please do not use these flags. :)

    Navigation menu with icons

    Always add an alt attribute, but leave its value empty if you still have text, so users with screen readers don’t have to waste their time with it. The item text is good enough.

     
    • Jeroen 9:07 am on 2014/11/11 Permalink | Reply

      Hi Thomas. I posted a question on the plugin support page of MLP. The question was about adding a language button in my menu. They directed me to this post you wrote. I succeeded in installing the plugin. But where in my WordPress dashboard can I edit something to make this work? And what editing is needed? Some help would be awesome ;-).

      • Jeroen 9:14 am on 2014/11/11 Permalink | Reply

        I’m so sorry. I just now see that network activate already did the job! It picks the flag-url from the site settings and adds the image to the top menu without needing to edit anything. Awesome add-on!!!
        Thank you so very much :-)

        • Thomas Scholz 10:17 am on 2014/11/11 Permalink | Reply

          Thanks for the feedback. The activation of the plugin deserved indeed a short explanation. I have added one, plus a suggestion for the theme’s stylesheet.

    • Gus 10:09 pm on 2014/11/20 Permalink | Reply

      Hi

      I am trying to insert the language selector plugin in a template monster header but it seems impossible…

      • Thomas Scholz 1:23 am on 2014/11/21 Permalink | Reply

        Please use our support forum to explain the problem, add your ode and the error messages that you get. I’m sure that we can find a way to solve it. :)

    • roberto 8:51 pm on 2015/02/10 Permalink | Reply

      Hi, i’ve tried many things but i’m getting confused. Please could you help me???. what I’m trying to do is to move both language buttons (with their icons) away from the “Primary Menu”, i would like to put it in the header or somewhere else.

      Here is the URL of my web project: http://vidaurrereisen.com/plasticwebs/

      Sorry if my english is a little poor, please try to explain it as simple as you can.

      thanks

  • Thorsten Frommen 3:31 am on 2014/03/30 Permalink | Reply
    Tags: http,   

    Language negotiation – how our redirect feature works 

    For our users, one of the most important features of Multilingual Press is the HTTP redirect. Each visitor should be sent to the page she understands best.

    This is hard.

    Example: Maryam (مري) visits your site for the first time. She understands Arabic, English and Latin. Arabic is her native language.
    You have three language versions of your content: English, Arabic and Russian. The original content is written in English, the other sites are translations.

    Which page should Maryam see? Arabic or English? It depends on two factors: The quality of your Arabic translation and Maryam’s capability to read English.

    Multilingual Press cannot judge the quality of your translation, but you can give each language a weight, or as we say: a priority. There is a field for that in our language manager.

    Screen shot: Language Manager priorities

    Maryam can do the same in her browser. If her English is not very good, she can add a weight to that preference, and her browser will send that in a q value:

    Accept-Language: ar,en;q=0.7,la;q=0.4

    We read that as:

    Arabic   ar = 1.0 (omitted values equal 1)
    English  en = 0.7
    Latin    la = 0.4

    Multilingual Press will now multiply your priority for each language with Maryam’s preference for that language. The highest number wins. This is called Language negotiation. If there are two highest numbers or the highest number matches the current language, or Maryam selects one language per widget (an URL with a noredirect parameter), she will not be redirected.

    Our algorithm is clever enough to handle subsets: If your only English content is in Belizean English (en-bz), but your visitor’s preference is Canadian English (en-ca), Multilingual Press will strip the second part and find the matching English content.

    What does that mean for you?

    You should always know the quality of your translations. Set the priority for the original language to the highest value, weight the translations lower.

    In our example: If you set the priority for English to 10 and for Arabic to 7, Maryam will not be redirected when she visits the English or the Arabic site. She will be sent to the Arabic site (her first preference) if she visits the Russian site.

    But if you set English to 10 and Arabic to 6, the result of Multilingual Press’ calculation will be:

    Arabic   ar = 1.0 x  6 = 6
    English  en = 0.7 x 10 = 7
    Latin    la = 0.4 (not calculated = 0)
    Russian  ru = 0.4 (not calculated = 0)

    Maryam will get the English site.

    We will also use your priorities to sort the languages in our switcher widget in one of the next releases.

    Conclusion: Set the language priorities carefully. They can decide how good your visitors will understand your message. But always provide a way for your visitors to override automatic content negotiation manually, because not everyone uses a browser which is configured to their needs, or the browser’s user interface is very poor. And sometimes your translation is not as good as you think it is.

    Screenshot: Google Chrome language settings

    Google Chrome language settings

    For developers

    You can override our selection per filter:

    /**
     * Change the URL for redirects. You might add support for other types
     * than singular posts and home here.
     * If you return an empty value, the redirect will not happen.
     * The result will be validated with esc_url().
     *
     * @param string $url
     * @param array  $match 'blog_id'  (int)    target blog ID
     *                      'language' (string) target language
     * @param int    $current_blog_id
     */
    $url = apply_filters( 'mlp_redirect_url', $url, $match, $current_blog_id );

    Links

     
c
Compose new post
j
Next post/Next comment
k
Previous post/Previous comment
r
Reply
e
Edit
o
Show/Hide comments
t
Go to top
l
Go to login
h
Show/Hide help
shift + esc
Cancel