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