Node translations with Panels and CTools

I wanted to create a node view containing both the original node and its translation, sort of like this neat page. I decided to build this page as a Panels node view template, but to reach the translation(s) I had to write a new CTools relationship plugin that I am sharing here:

// @file mymodule.module

 * Implementation of hook_ctools_plugin_directory().
 * It simply tells panels where to find the .inc files that define various
 * args, contexts, content_types.
function mymodule_ctools_plugin_directory($module, $plugin) {
  if (
$module == 'ctools' && !empty($plugin)) {

Place the following file in a plugins/relationships subfolder of your module:

// @file

 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
$plugin = array(
'title' => t('Node translation'),
'keyword' => 'translation',
'description' => t('Creates the translation of a node as a node context.'),
'required context' => new ctools_context_required(t('Node'), 'node'),
'context' => 'ctools_translation_from_node_context',
'settings form' => 'ctools_translation_from_node_settings_form',
'defaults' => array('language' => 'en', 'fallback' => FALSE),

 * Return a new context based on an existing context.
function ctools_translation_from_node_context($context, $conf) {
// If unset it wants a generic, unfilled context, which is just NULL.
if (empty($context->data)) {
ctools_context_create_empty('node', NULL);

  if (isset(
$context->data->nid)) {
$original = node_load($context->data->nid);
$tnids = translation_node_get_translations($original->tnid);
    if (empty(
$tnids[$conf['language']])) {
$conf['fallback'] ?
ctools_context_create('node', $original) :
ctools_context_create_empty('node', NULL);
$translation = node_load($tnids[$conf['language']]->nid);

// Send it to ctools.
return ctools_context_create('node', $translation);

ctools_translation_from_node_settings_form($conf) {
$form['language'] = array(
'#type' => 'select',
'#title' => t('Language'),
'#options' => locale_language_list(),
'#default_value' => $conf['language'],
$form['fallback'] = array(
'#type' => 'checkbox',
'#title' => t('Fallback to original node'),
'#description' => t('Use original node if desired translation is not found.'),
'#default_value' => $conf['fallback'],

To use this plugin, create a new variant of the Panels node view template. In the Contexts page, add a Node translation relationship for every language that you want to support. You can specify the desired language in the relationship settings. In your Content layout, you will then be able to use those relationships to access the node translations.

To go back to the original example above, we want the English version on the left and the Arabic on the right. To achieve this, I created two node translation relationships, one for English and the other for Arabic. I placed a node content panel in each of my two columns, and set the left panel to refer to the English translation and the right panel to the Arabic translation. Note that I didn't use the original "Node being viewed" content because there would be no way of telling if it's Arabic or English, given that this node template could be accessed via either versions.

What's missing is to replicate the bilingual comments view, and I'll share my solution once I implement it :-)


Looking forward to your

Looking forward to your solution of the bilingual comments view.. this is great

Submitted this plugin as a

Submitted this plugin as a patch to CTools.

Neat!. I love the example on

Neat!. I love the example on Meedan in English (ltr) and Arabic (rtl). Inspiring. Thanks for sharing.