Making a theme multilingual by adding a custom taxonomy to posts and pages called “Languages”?

Recently, I’ve been thinking of implementing an easy and simple localization feature in my
WordPress site.

I thought about adding a custom taxonomy to my posts, and pages called “Languages.”

Read More

So when you click the link of the language it just filters the posts with that Language term.

I’ve been using WPML and it is awesome, but I just wanted an embedded implementation for my theme.

Have anyone tried this before?

What could be the pros and downsides of doing this?

Related posts

Leave a Reply

2 comments

  1. I was facing a similar dilemma this week, and ended up creating a solution which I thought you might find useful.

    I personally dislike the use of taxonomies for this purpose. I think the only pro is that it’s already built-in, and maybe it gives you a good out-of-the-box permalink structure, but it feels like a corruption of the taxonomies original purpose.

    My approach is fairly self-sufficient and uses postmeta to store translations. All you would have to do to filter posts which have translations is to check for _translate_content_{$lang} post meta (e.g. _translate_content_fr_FR). I haven’t yet coded the front-end parts, but that should vary a lot according to what you want to do in your theme.

    Code is commented, but let me know if there is anything that’s not clear.

    Note: I do this for pages only, but it should be easy to add posts to it as well. Also, keep in mind that I haven’t tested this with revisions, which I don’t use.

    # Create a list of available languages for translation;
    # Note: the keys are WordPress locale codes, so we can easily integrate it
    function my_available_languages() {
        return array(
            'en_US' => array(
                'value' => 'en_US',
                'label' => 'English (U.S.)',
            ),
            'fr_FR' => array(
                'value' => 'fr_FR',
                'label' => 'Français',
            ),
            'de_DE' => array(
                'value' => 'de_DE',
                'label' => 'Deutsch',
            ),
            'es_ES' => array(
                'value' => 'es_ES',
                'label' => 'Español',
            ),  
        );
    }
    
    # Allow translations
    add_action('current_screen', 'enable_translation');
    function enable_translation($screen) {
        # Only process translations for existing posts
        if(in_array($screen->post_type, array('page')) && $screen->base == 'post' && $screen->action != 'add') {
            if(isset($_GET['translate']) && array_key_exists($_GET['translate'], my_available_languages())) {
    
                # Load translated fields instead of the original post content
                add_filter('title_edit_pre', create_function('', 'return get_translate_field("title", "' . $_GET['translate'] . '");'));
                add_filter('content_edit_pre', create_function('', 'return get_translate_field("content", "' . $_GET['translate'] . '");'));
                add_filter('excerpt_edit_pre', create_function('', 'return get_translate_field("excerpt", "' . $_GET['translate'] . '");'));
    
                # Add hidden field so that 'translate' param persists
                add_action('submitpage_box', 'doing_translate_hidden_field');
    
                # Remove unnecessary metaboxes, which are not pertinent for translation
                add_action('add_meta_boxes', 'redo_metaboxes_on_translate', 99, 2);
    
                # Reduce the layout columns options
                add_filter('screen_layout_columns', 'screen_layout_columns_translate_page');
                function screen_layout_columns_translate_page($columns) {
                    $columns['page'] = 1;
                    return $columns;
                }
    
                # Set one column default layout on dashboard
                add_filter('get_user_option_screen_layout_page', create_function('', 'return 1;'));
    
                # Notify user that he is viewing a translated version
                add_action('admin_notices', 'doing_translation_notification');
            }
        }
    }
    
    add_action('add_meta_boxes', 'add_translation_metabox', 98, 2);
    function add_translation_metabox($post_type, $post) {
        # Optional: add this box conditionally
        $types = array('page');
        if(in_array($post_type, $types)) {
            foreach($types as $type) {
                add_meta_box('translate_mgmt', __('Translations', 'text_domain'), 'output_translate_mgmt_metabox', $type, 'side', 'low');   
            }
        }
    }
    
    # Output a list of links of available languages for translation
    function output_translate_mgmt_metabox() {
        global $post;
        $default = 'en_US';
        # get_edit_post_link will return a filtered admin_url link, so we must attempt to remove the translate param, in case it's already added
        $link = remove_query_arg('translate', get_edit_post_link($post->ID));
        echo('<ul>');
        foreach(my_available_languages() as $code => $lang) {
            $href = $code != $default ? add_query_arg(array('translate' => $code), $link) : $link;
            echo('<li><a href="' . $href . '">' . $lang['label'] . '</a></li>');
        }
        echo('</ul>');
    }
    
    function redo_metaboxes_on_translate($post_type, $post) {
    
        # Add alternative save metabox
        add_meta_box('translate_save', __('Save', 'text_domain'), 'output_translate_save_metabox', 'page', 'normal', 'low');
    
        # Remove unnecessary metaboxes
        remove_meta_box('slugdiv', 'page', 'normal');   
        remove_meta_box('authordiv', 'page', 'normal');
        remove_meta_box('postcustom', 'page', 'normal');
        remove_meta_box('commentsdiv', 'page', 'normal');
        remove_meta_box('commentstatusdiv', 'page', 'normal');  
        remove_meta_box('submitdiv', 'page', 'side');
        remove_meta_box('pageparentdiv', 'page', 'side');   
        remove_meta_box('revisionsdiv', 'page', 'side');
        remove_meta_box('trackbacksdiv', 'page', 'side');
        remove_meta_box('postimagediv', 'page', 'side');
    }
    
    # Output a simpler, 'just save' button, so translators are not confronted with too many options that affect main post
    function output_translate_save_metabox() { ?>
        <input type="submit" class="ui_button big save" value="<?php _e('Save', 'text_domain'); ?>" />
    <?php
    }
    
    # Add a notice to the user (like the update WordPress nag) so that it's clear that he is currently viewing a translated version
    function doing_translation_notification() {
        global $post;
        $langs = my_available_languages();
        echo "<div class='update-nag'>" . sprintf(__('You are currently translating the page "%s" into %s.', 'text_domain'), get_the_title($post->ID), $langs[$_REQUEST['translate']]['label']) . "</div>";
    }
    
    # Add hidden input which will add 'translate' to the $_POST object, thus allowing it to persist after redirection
    # Note: I use jQuery to remove the slug below the title, but it's optional, I guess
    function doing_translate_hidden_field() { ?>
        <input type="hidden" name="translate" value="<?php echo($_REQUEST['translate']); ?>" />
        <script type="text/javascript">
            jQuery(function($){
                $('#edit-slug-box').remove();
            });
        </script>
    <?php
    }
    
    # Return translated content
    function get_translate_field($field, $lang) {
        global $post;
        return get_post_meta($post->ID, '_translate_' . $field . '_' . $lang, true);    
    }
    
    add_action('admin_init', 'add_translation_content_filters');
    function add_translation_content_filters() {
        $fields = array('title', 'content', 'excerpt'); // As named in filters, not on their form inputs / db
        foreach($fields as $field) {
            add_action($field . '_save_pre', create_function('$cont', 'return update_translation_on_save($cont, "' . $field . '");'), 1, 1);
        }
    }
    
    # If doing translation, update pertinent post meta instead of post row on database
    function update_translation_on_save($content, $field) {
        $db_fields = array(
            'title' => 'post_title',
            'content' => 'post_content',
            'excerpt' => 'post_excerpt',
        );
        global $post;
        if(is_object($post) && isset($_REQUEST['translate'])) {
            update_post_meta($post->ID, '_translate_' . $field . '_' . $_REQUEST['translate'], $content);
            $content = $post->{$db_fields[$field]};
        }
        return $content;
    }
    
    # Function to add 'translate' param to a given URL (to be used with filters) 
    function add_translate_param($link) {
        # Optional: don't add 'translate' on pages which are not for content management
        if(strpos($link, 'post.php') === false && strpos($link, 'edit.php') === false) {
            return $link;
        }
        return add_query_arg(array('translate' => isset($_REQUEST['translate']) ? $_REQUEST['translate'] : ''), $link);
    }
    
    if(isset($_REQUEST['translate'])) {
        # Make 'translate' URL parameter persistent 
        add_filter('admin_url', 'add_translate_param');
        add_filter('wp_redirect', 'add_translate_param');
        # Add 'doing_translate' body class
        add_filter('admin_body_class', create_function('$c', '$c .= " doing_translate"; return $c;'), 99);
    }