Commit 2e250c69 authored by Dries's avatar Dries

- Patch #532512 by Gábor Hojtsy, andypost, no_commit_credit: fixed Plural...

- Patch #532512 by Gábor Hojtsy, andypost, no_commit_credit: fixed Plural string storage is broken, editing UI is missing.
parent 368cf30d
......@@ -31,6 +31,7 @@ Drupal 8.0, xxxx-xx-xx (development version)
* Made interface translation directly accessible from language list.
* Centralized interface translation import to one directory.
* Drupal can now be translated to English and English can be deleted.
* Added support for singular/plural discovery and translation.
* Improved content language support:
* Freely orderable language selector in forms.
* Made it possible to assign language to taxonomy terms, vocabularies
......
......@@ -162,6 +162,15 @@
*/
const DRUPAL_CACHE_GLOBAL = 0x0008;
/**
* The delimiter used to split plural strings.
*
* This is the ETX (End of text) character and is used as a minimal means to
* separate singular and plural variants in source and translation text. It
* was found to be the most compatible delimiter for the supported databases.
*/
const LOCALE_PLURAL_DELIMITER = "\03";
/**
* Adds content to a specified region.
*
......@@ -1719,27 +1728,34 @@ function format_xml_elements($array) {
*/
function format_plural($count, $singular, $plural, array $args = array(), array $options = array()) {
$args['@count'] = $count;
// Join both forms to search a translation.
$tranlatable_string = implode(LOCALE_PLURAL_DELIMITER, array($singular, $plural));
// Translate as usual.
$translated_strings = t($tranlatable_string, $args, $options);
// Split joined translation strings into array.
$translated_array = explode(LOCALE_PLURAL_DELIMITER, $translated_strings);
if ($count == 1) {
return t($singular, $args, $options);
return $translated_array[0];
}
// Get the plural index through the gettext formula.
// @todo implement static variable to minimize function_exists() usage.
$index = (function_exists('locale_get_plural')) ? locale_get_plural($count, isset($options['langcode']) ? $options['langcode'] : NULL) : -1;
// If the index cannot be computed, use the plural as a fallback (which
// allows for most flexiblity with the replaceable @count value).
if ($index < 0) {
return t($plural, $args, $options);
if ($index == 0) {
// Singular form.
return $translated_array[0];
}
else {
switch ($index) {
case "0":
return t($singular, $args, $options);
case "1":
return t($plural, $args, $options);
default:
unset($args['@count']);
$args['@count[' . $index . ']'] = $count;
return t(strtr($plural, array('@count' => '@count[' . $index . ']')), $args, $options);
if (isset($translated_array[$index])) {
// N-th plural form.
return $translated_array[$index];
}
else {
// If the index cannot be computed or there's no translation, use
// the second plural form as a fallback (which allows for most flexiblity
// with the replaceable @count value).
return $translated_array[1];
}
}
}
......
This diff is collapsed.
......@@ -176,20 +176,8 @@ function locale_schema() {
'default' => '',
'description' => 'Language code. References {language}.langcode.',
),
'plid' => array(
'type' => 'int',
'not null' => TRUE, // This should be NULL for no referenced string, not zero.
'default' => 0,
'description' => 'Parent lid (lid of the previous string in the plural chain) in case of plural strings. References {locales_source}.lid.',
),
'plural' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Plural index number in case of plural strings.',
),
),
'primary key' => array('language', 'lid', 'plural'),
'primary key' => array('language', 'lid'),
'foreign keys' => array(
'locales_source' => array(
'table' => 'locales_source',
......@@ -198,8 +186,6 @@ function locale_schema() {
),
'indexes' => array(
'lid' => array('lid'),
'plid' => array('plid'),
'plural' => array('plural'),
),
);
......@@ -379,6 +365,123 @@ function locale_update_8004() {
}
}
/**
* Update plural interface translations to new format.
*
* See http://drupal.org/node/532512#comment-5679184 for the details of the
* structures handled in this update.
*/
function locale_update_8005() {
// Collect all LIDs that are sources to plural variants.
$results = db_query("SELECT lid, plid FROM {locales_target} WHERE plural <> 0");
$plural_lids = array();
foreach ($results as $row) {
// Need to collect both LID and PLID. The LID for the first (singular)
// string can only be retrieved from the first plural's PLID given no
// other indication. The last plural variant is never referenced, so we
// need to store the LID directly for that. We never know whether we are
// on the last plural though, so we always remember LID too.
$plural_lids[] = $row->lid;
$plural_lids[] = $row->plid;
}
$plural_lids = array_unique($plural_lids);
// Look up all translations for these source strings. Ordering by language
// will group the strings by language, the 'plid' order will get the
// strings in singular/plural order and 'plural' will get them in precise
// sequential order needed.
$results = db_query("SELECT s.lid, s.source, t.translation, t.plid, t.plural, t.language FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid WHERE s.lid IN (:lids) ORDER BY t.language, t.plid, t.plural", array(':lids' => $plural_lids));
// Collect the strings into an array and combine values as we go.
$strings = array();
$parents_to_sources = array();
$remove_lids = array();
foreach ($results as $child) {
$strings[$child->language][$child->lid] = array(
'source' => array($child->source),
'translation' => array($child->translation),
);
if (empty($child->plid)) {
// Non-children strings point to themselves as parents. This makes it
// easy to look up the utmost parents for any plurals.
$parents_to_sources[$child->lid] = $child->lid;
}
else {
// Children strings point to their utmost parents. Because we get data
// in PLID order, we can ensure that all previous parents have data now,
// so we can just copy the parent's data about their parent, etc.
$parents_to_sources[$child->lid] = $parents_to_sources[$child->plid];
// Append translation to the utmost parent's translation string.
$utmost_parent = &$strings[$child->language][$parents_to_sources[$child->plid]];
// Drop the Drupal-specific numbering scheme from the end of plural
// formulas.
$utmost_parent['translation'][] = str_replace('@count[' . $child->plural .']', '@count', $child->translation);
if (count($utmost_parent['source']) < 2) {
// Append source to the utmost parent's source string only if it is the
// plural variant. Further Drupal specific plural variants are not to be
// retained for source strings.
$utmost_parent['source'][] = $child->source;
}
// All plural variant LIDs are to be removed with their translations.
// Only the singular LIDs will be kept.
$remove_lids[] = $child->lid;
}
}
// Do updates for all source strings and all translations.
$updated_sources = array();
foreach ($strings as $langcode => $translations) {
foreach($translations as $lid => $translation) {
if (!in_array($lid, $updated_sources)) {
// Only update source string if not yet updated. We merged these within
// the translation lookups because plural information was only avilable
// with the translation, but we don't need to save it again for every
// language.
db_update('locales_source')
->fields(array(
'source' => implode(LOCALE_PLURAL_DELIMITER, $translation['source']),
))
->condition('lid', $lid)
->execute();
$updated_sources[] = $lid;
}
db_update('locales_target')
->fields(array(
'translation' => implode(LOCALE_PLURAL_DELIMITER, $translation['translation']),
))
->condition('lid', $lid)
->condition('language', $langcode)
->execute();
}
}
// Remove all plural LIDs from source and target. only keep those which were
// originally used for the singular strings (now updated to contain the
// serialized version of plurals).
$remove_lids = array_unique($remove_lids);
db_delete('locales_source')
->condition('lid', $remove_lids, 'IN')
->execute();
db_delete('locales_target')
->condition('lid', $remove_lids, 'IN')
->execute();
// Drop the primary key because it contains 'plural'.
db_drop_primary_key('locales_target');
// Remove the 'plid' and 'plural' columns and indexes.
db_drop_index('locales_target', 'plid');
db_drop_field('locales_target', 'plid');
db_drop_index('locales_target', 'plural');
db_drop_field('locales_target', 'plural');
// Add back a primary key without 'plural'.
db_add_primary_key('locales_target', array('language', 'lid'));
}
/**
* @} End of "addtogroup updates-7.x-to-8.x"
* The next series of updates should start at 9000.
......
......@@ -92,7 +92,7 @@ function _locale_translate_seek() {
$rows = array();
foreach ($strings as $lid => $string) {
$rows[] = array(
array('data' => check_plain(truncate_utf8($string['source'], 150, FALSE, TRUE)) . '<br /><small>' . $string['location'] . '</small>'),
array('data' => check_plain(truncate_utf8(str_replace(LOCALE_PLURAL_DELIMITER, ', ', $string['source']), 150, FALSE, TRUE)) . '<br /><small>' . $string['location'] . '</small>'),
$string['context'],
array('data' => _locale_translate_language_list($string['languages'], $limit_language), 'align' => 'center'),
array('data' => l(t('edit'), "admin/config/regional/translate/edit/$lid", array('query' => drupal_get_destination())), 'class' => array('nowrap')),
......@@ -278,13 +278,37 @@ function locale_translate_edit_form($form, &$form_state, $lid) {
drupal_set_message(t('String not found.'), 'error');
drupal_goto('admin/config/regional/translate/translate');
}
// Add original text to the top and some values for form altering.
$form['original'] = array(
'#type' => 'item',
'#title' => t('Original text'),
'#markup' => check_plain(wordwrap($source->source, 0)),
);
// Split source to work with plural values.
$source_array = explode(LOCALE_PLURAL_DELIMITER, $source->source);
if (count($source_array) == 1) {
// Add original text value and mark as non-plural.
$form['plural'] = array(
'#type' => 'value',
'#value' => 0
);
$form['original'] = array(
'#type' => 'item',
'#title' => t('Original text'),
'#markup' => check_plain($source_array[0]),
);
}
else {
// Add original text value and mark as plural.
$form['plural'] = array(
'#type' => 'value',
'#value' => 1
);
$form['original_singular'] = array(
'#type' => 'item',
'#title' => t('Original singular form'),
'#markup' => check_plain($source_array[0]),
);
$form['original_plural'] = array(
'#type' => 'item',
'#title' => t('Original plural form'),
'#markup' => check_plain($source_array[1]),
);
}
if (!empty($source->context)) {
$form['context'] = array(
'#type' => 'item',
......@@ -307,22 +331,68 @@ function locale_translate_edit_form($form, &$form_state, $lid) {
if (!locale_translate_english()) {
unset($languages['en']);
}
$form['translations'] = array('#tree' => TRUE);
// Store languages to iterate for validation and submission of the form.
$form_state['langcodes'] = array_keys($languages);
$plural_formulas = variable_get('locale_translation_plurals', array());
$form['translations'] = array(
'#type' => 'vertical_tabs',
'#tree' => TRUE
);
// Approximate the number of rows to use in the default textarea.
$rows = min(ceil(str_word_count($source->source) / 12), 10);
$rows = min(ceil(str_word_count($source_array[0]) / 12), 10);
foreach ($languages as $langcode => $language) {
$form['translations'][$langcode] = array(
'#type' => 'textarea',
'#type' => 'fieldset',
'#title' => $language->name,
'#rows' => $rows,
'#default_value' => '',
);
if (empty($form['plural']['#value'])) {
$form['translations'][$langcode][0] = array(
'#type' => 'textarea',
'#title' => $language->name,
'#rows' => $rows,
'#default_value' => '',
);
}
else {
// Dealing with plural strings.
if (isset($plural_formulas[$langcode]['plurals']) && $plural_formulas[$langcode]['plurals'] > 1) {
// Add a textarea for each plural variant.
for ($i = 0; $i < $plural_formulas[$langcode]['plurals']; $i++) {
$form['translations'][$langcode][$i] = array(
'#type' => 'textarea',
'#title' => ($i == 0 ? t('Singular form') : format_plural($i, 'First plural form', '@count. plural form')),
'#rows' => $rows,
'#default_value' => '',
);
}
}
else {
// Fallback for unknown number of plurals.
$form['translations'][$langcode][0] = array(
'#type' => 'textarea',
'#title' => t('Sigular form'),
'#rows' => $rows,
'#default_value' => '',
);
$form['translations'][$langcode][1] = array(
'#type' => 'textarea',
'#title' => t('Plural form'),
'#rows' => $rows,
'#default_value' => '',
);
}
}
}
// Fetch translations and fill in default values in the form.
$result = db_query("SELECT DISTINCT translation, language FROM {locales_target} WHERE lid = :lid", array(':lid' => $lid));
foreach ($result as $translation) {
$form['translations'][$translation->language]['#default_value'] = $translation->translation;
$translation_array = explode(LOCALE_PLURAL_DELIMITER, $translation->translation);
for ($i = 0; $i < count($translation_array); $i++) {
$form['translations'][$translation->language][$i]['#default_value'] = $translation_array[$i];
}
}
$form['actions'] = array('#type' => 'actions');
......@@ -334,10 +404,12 @@ function locale_translate_edit_form($form, &$form_state, $lid) {
* Validate string editing form submissions.
*/
function locale_translate_edit_form_validate($form, &$form_state) {
foreach ($form_state['values']['translations'] as $key => $value) {
if (!locale_string_is_safe($value)) {
form_set_error('translations', t('The submitted string contains disallowed HTML: %string', array('%string' => $value)));
watchdog('locale', 'Attempted submission of a translation string with disallowed HTML: %string', array('%string' => $value), WATCHDOG_WARNING);
foreach ($form_state['langcodes'] as $langcode) {
foreach ($form_state['values']['translations'][$langcode] as $key => $value) {
if (!locale_string_is_safe($value)) {
form_set_error("translations][$langcode][$key", t('The submitted string contains disallowed HTML: %string', array('%string' => $value)));
watchdog('locale', 'Attempted submission of a translation string with disallowed HTML: %string', array('%string' => $value), WATCHDOG_WARNING);
}
}
}
}
......@@ -349,9 +421,19 @@ function locale_translate_edit_form_validate($form, &$form_state) {
*/
function locale_translate_edit_form_submit($form, &$form_state) {
$lid = $form_state['values']['lid'];
foreach ($form_state['values']['translations'] as $key => $value) {
$translation = db_query("SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $key))->fetchField();
if (!empty($value)) {
foreach ($form_state['langcodes'] as $langcode) {
// Serialize plural variants in one string by LOCALE_PLURAL_DELIMITER.
$value = implode(LOCALE_PLURAL_DELIMITER, $form_state['values']['translations'][$langcode]);
$translation = db_query("SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchField();
// No translation when all strings are empty.
$has_translation = FALSE;
foreach ($form_state['values']['translations'][$langcode] as $string) {
if (!empty($string)) {
$has_translation = TRUE;
break;
}
}
if ($has_translation) {
// Only update or insert if we have a value to use.
if (!empty($translation)) {
db_update('locales_target')
......@@ -359,7 +441,7 @@ function locale_translate_edit_form_submit($form, &$form_state) {
'translation' => $value,
))
->condition('lid', $lid)
->condition('language', $key)
->condition('language', $langcode)
->execute();
}
else {
......@@ -367,7 +449,7 @@ function locale_translate_edit_form_submit($form, &$form_state) {
->fields(array(
'lid' => $lid,
'translation' => $value,
'language' => $key,
'language' => $langcode,
))
->execute();
}
......@@ -376,12 +458,12 @@ function locale_translate_edit_form_submit($form, &$form_state) {
// Empty translation entered: remove existing entry from database.
db_delete('locales_target')
->condition('lid', $lid)
->condition('language', $key)
->condition('language', $langcode)
->execute();
}
// Force JavaScript translation file recreation for this language.
_locale_invalidate_js($key);
_locale_invalidate_js($langcode);
}
drupal_set_message(t('The string has been saved.'));
......
This diff is collapsed.
......@@ -183,6 +183,19 @@
'weight' => '0',
'javascript' => '',
))
->values(array(
'language' => 'hr',
'name' => 'Croatian',
'native' => 'Hrvatski',
'direction' => '0',
'enabled' => '1',
'plurals' => '3',
'formula' => '((($n%10)==1)&&(($n%100)!=11))?(0):((((($n%10)>=2)&&(($n%10)<=4))&&((($n%100)<10)||(($n%100)>=20)))?(1):2));',
'domain' => '',
'prefix' => '',
'weight' => '0',
'javascript' => '',
))
->execute();
// Add locales_source table from locale.install schema and fill with some
......@@ -413,6 +426,30 @@
'context' => '',
'version' => 'none',
))
->values(array(
'lid' => '22',
'location' => '',
'textgroup' => 'default',
'source' => '1 byte',
'context' => '',
'version' => 'none',
))
->values(array(
'lid' => '23',
'location' => '',
'textgroup' => 'default',
'source' => '@count bytes',
'context' => '',
'version' => 'none',
))
->values(array(
'lid' => '24',
'location' => '',
'textgroup' => 'default',
'source' => '@count[2] bytes',
'context' => '',
'version' => 'none',
))
->execute();
// Add locales_target table from locale.install schema.
......@@ -472,6 +509,49 @@
'module' => 'locale',
'name' => 'locales_target',
));
db_insert('locales_target')->fields(array(
'lid',
'translation',
'language',
'plid',
'plural',
))
->values(array(
'lid' => 22,
'translation' => '1 byte',
'language' => 'ca',
'plid' => 0,
'plural' => 0,
))
->values(array(
'lid' => 23,
'translation' => '@count bytes',
'language' => 'ca',
'plid' => 22,
'plural' => 1,
))
->values(array(
'lid' => 22,
'translation' => '@count bajt',
'language' => 'hr',
'plid' => 0,
'plural' => 0,
))
->values(array(
'lid' => 23,
'translation' => '@count bajta',
'language' => 'hr',
'plid' => 22,
'plural' => 1,
))
->values(array(
'lid' => 24,
'translation' => '@count[2] bajtova',
'language' => 'hr',
'plid' => 23,
'plural' => 2,
))
->execute();
// Set up variables needed for language support.
db_insert('variable')->fields(array(
......
......@@ -104,6 +104,23 @@ class LanguageUpgradePathTestCase extends UpgradePathTestCase {
// renamed.
$current_weights = variable_get('locale_language_negotiation_methods_weight_language_interface', array());
$this->assertTrue(serialize($expected_weights) == serialize($current_weights), t('Language negotiation method weights upgraded.'));
// Look up migrated plural string.
$source_string = db_query('SELECT * FROM {locales_source} WHERE lid = 22')->fetchObject();
$this->assertEqual($source_string->source, implode(LOCALE_PLURAL_DELIMITER, array('1 byte', '@count bytes')));
$translation_string = db_query("SELECT * FROM {locales_target} WHERE lid = 22 AND language = 'hr'")->fetchObject();
$this->assertEqual($translation_string->translation, implode(LOCALE_PLURAL_DELIMITER, array('@count bajt', '@count bajta', '@count bajtova')));
$this->assertTrue(!isset($translation_string->plural), 'Chained plural indicator removed.');
$this->assertTrue(!isset($translation_string->plid), 'Chained plural indicator removed.');
$source_string = db_query('SELECT * FROM {locales_source} WHERE lid IN (23, 24)')->fetchObject();
$this->assertTrue(empty($source_string), 'Individual plural variant source removed');
$translation_string = db_query("SELECT * FROM {locales_target} WHERE lid IN (23, 24)")->fetchObject();
$this->assertTrue(empty($translation_string), 'Individual plural variant translation removed');
$translation_string = db_query("SELECT * FROM {locales_target} WHERE lid = 22 AND language = 'ca'")->fetchObject();
$this->assertEqual($translation_string->translation, implode(LOCALE_PLURAL_DELIMITER, array('1 byte', '@count bytes')));
}
/**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment