Third & GroveThird & Grove
Feb 2, 2017 - Mira Scarfiotti

Deploying string translations in a Drupal 7 multilingual site

 

When working on large multisite projects like Benefitcosmetics.com it’s often a good idea to have a “master” content hub to stage and deploy content from. If some of those sites are multilingual, this is when things get extra tricky.

On the Benefit Cosmetics site we leverage the Deploy module. This is a great module that uses services to send content to different remote endpoints. The problem we faced was with string translations. Since they are not entities they are not supported by the deploy module out of the box.

Thankfully we can use a third module that’s already installed on the platform called Element. My idea was to create a new element type called ‘t-string’ and use that as a helper to transport the string translation data over to the destination site. I decided the easiest way to save the translation information would be to serialize all the data from the locales_source and locales_target table related to the selected string and save it to a text field in the new element type. Now that we have an entity with the translation data, the Deploy module can ship it without any problem.

Ready to see some code? Here is how we add the deploy options to the form:

 

/**
* Implements hook form_FORM_ID_alter().
*/
function deploy_string_form_i18n_string_locale_translate_edit_form_alter(&$form, &$form_state, $form_id) {
if (module_exists('deploy_managed_ui')) {
deploy_managed_ui_form_elements($form, 'deploy_string_form_submit');
}
}

 

The second argument is the new submit handler. Look for the submit handler code and the utility function that creates the new t-string element.

 

/**
* Submit handler for edit link forms to add to deployment plan. We set uid to 1
* because that's the user that handles deployments from now on.
*/
function deploy_string_form_submit($form, &$form_state) {
foreach ($form_state['values']['deploy_managed_ui']['plans'] as $plan => $checked) {
if (!$checked) {
continue;
}
$lid = $form_state['values']['lid'];
$t_string = _deploy_string_entity_create($lid);
deploy_manager_add_to_plan($plan, 'element', $t_string);
}
}
/**
* Utility function to create the 't-string' entity to be deployed.
*
* @param string $lid
*
* @return object $t_string
*/
function _deploy_string_entity_create($lid) {
$data = db_query('SELECT * FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid WHERE s.lid = :lid', array(':lid' => $lid))->fetchAll();
if (empty($data)) {
return;
}
$values = array(
'title' => $lid . ' - ' . $data['0']->source,
'type' => 't_string',
'created' => time(),
'changed' => time(),
'uid' => '1',
);
$t_string = entity_create('element', $values);
$t_string = entity_metadata_wrapper('element', $t_string);
$t_string->field_t_string_data = serialize($data);
$t_string->save();
return $t_string->value();
}

 

As you can see, once the new element is created it's fed to the deploy plan with deploy_manager_add_to_plan.

Now you can go to the deployment page and send the new elements containing all the string translation data. But how do they get saved as translated strings on the destination site you ask? Here is more code. **Warning: this is a big block.**

 

/**
* Implements hook_entity_presave().
* When a 't_string' gets saved after deploy we add UUID compatibility and
* insert/update the translated string.
*/
function deploy_string_entity_presave($entity, $type) {
if ($type == 'element' && $entity->type == 't_string') {
if (empty($entity->uuid_services)) {
return;
}
$languages = locale_language_list();
$entity_w = entity_metadata_wrapper($type, $entity);
$data = $entity_w->field_t_string_data->value();
$data = unserialize($data);
foreach ($data as $value) {
// Only saves translated strings in available languages.
if (!isset($languages[$value->language])) {
continue;
}
$lid = _deploy_string_update_or_insert_string($value);
_deploy_string_update_or_insert_translation($value, $lid);
}
// Set t-string to empty so the table that creates conflicts stays empty.
$entity->field_t_string_data = array();
// No need to manually cler all the cache anymore.
cache_clear_all('locale:', 'cache', TRUE);
}
}
/**
* Utility function to update or insert a translatable string in the
* locales_source table.
*
* @param array $value
* @return string $lid
*/
function _deploy_string_update_or_insert_string($value) {
$fields = array(
'location' => $value->location,
'textgroup' => $value->textgroup,
'source' => $value->source,
'context' => $value->context,
'version' => $value->version,
'bc' => $value->bc,
);
$query = db_query('SELECT * FROM {locales_source} AS s WHERE s.source = :source and s.context = :context' , array(':source' => $value->source, ':context' => $value->context))->fetchAll();
// Check if translation exists and insert or update.
if (empty($query)) {
// Saves new translatable string.
$lid = db_insert('locales_source')
->fields($fields)->execute();
} else {
// Updates existing translatable string.
$lid = $query['0']->lid;
db_update('locales_source')
->fields($fields)
->condition('lid', $query['0']->lid)
->execute();
}
return $lid;
}
/**
* Utility function to update or insert a translatable string in the
* locales_target table.
*
* @param array $value
* @param string $lid
*/
function _deploy_string_update_or_insert_translation($value, $lid) {
// Setup fields to insert or update.
$fields = array(
'lid' => $lid,
'translation' => $value->translation,
'language' => $value->language,
'plid' => $value->plid,
'plural' => $value->plural,
'i18n_status' => $value->i18n_status,
);
// Check if translation exists and insert or update.
$query = db_query('SELECT * FROM {locales_target} AS t WHERE t.lid = :lid and t.language = :language', array(':lid' => $lid, ':language' => $fields['language']))->fetchAll();
if (empty($query)) {
db_insert('locales_target')
->fields($fields)->execute();
} else {
db_update('locales_target')
->fields($fields)
->condition('lid', $lid)
->condition('language', $fields['language'])
->execute();
}
}

We decided to use the entity_presave hook as it was more reliable and check for the uuid_services property to make sure these operations happen only after a deploy. We also had to empty the field_t_string_data field as it created primary key conflicts on cloned databases.

Overall this was a very interesting challenge and I'm happy with the solution that we found to it. As always, let me know if you have any questions or better ways to do it!