WordPress Development Stack Exchange is a question and answer site for WordPress developers and administrators. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

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

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?

share|improve this question
I saw a plugin that ads a taxonomy for language wordpress.org/extend/plugins/language-taxonomy but i'm not sure if i would go in either way for a multilingual site. – Bainternet Jan 24 '11 at 19:09

The Pro: You can reuse native WP code.

Contra: You have to write your own taxonomy selector to avoid posts tagged with more than one language. The plugin Posts 2 Posts from @scribu should guide you to a more flexible approach.

share|improve this answer
To add to that if you want a language switch button at the top of a page (for example 'show this page in French') then you have to find a way to associate that particular post (in English) with a corresponding post (in French). One method would be scribu's Posts 2 Posts as toscho mentions. Consider using a custom non-hierarchical (ie 'tags') taxo as an alternative way to relate posts, where each tag has a unique slug that would correspond to some 'universal' language-independent identifier for that particular content. – Tom Auger Aug 11 '11 at 16:05

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

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'); ?>" />

# 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">

# 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);
share|improve this answer

Your Answer


By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.