Commit 2e250c69 authored by Dries's avatar Dries
Browse files

- 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];
}
}
}
......
......@@ -173,7 +173,7 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) {
}
// Append the plural form to the current entry.
$current['msgid'] .= "\0" . $quoted;
$current['msgid'] .= LOCALE_PLURAL_DELIMITER . $quoted;
$context = 'MSGID_PLURAL';
}
......@@ -390,8 +390,10 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL
// Store the string we got in the database.
case 'db-store':
// We got header information.
if ($value['msgid'] == '') {
// If 'msgid' is empty, it means we got values for the header of the
// file as per the structure of the Gettext format.
$locale_plurals = variable_get('locale_translation_plurals', array());
if (($mode != LOCALE_IMPORT_KEEP) || empty($locale_plurals[$lang]['plurals'])) {
// Since we only need to parse the header if we ought to update the
......@@ -413,32 +415,25 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL
}
else {
// Some real string to import.
// Found a string to store, clean up and prepare the data.
$comments = _locale_import_shorten_comments(empty($value['#']) ? array() : $value['#']);
if (strpos($value['msgid'], "\0")) {
// This string has plural versions.
$english = explode("\0", $value['msgid'], 2);
$entries = array_keys($value['msgstr']);
for ($i = 3; $i <= count($entries); $i++) {
$english[] = $english[1];
}
$translation = array_map('_locale_import_append_plural', $value['msgstr'], $entries);
$english = array_map('_locale_import_append_plural', $english, $entries);
foreach ($translation as $key => $trans) {
if ($key == 0) {
$plid = 0;
}
$plid = _locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english[$key], $trans, $comments, $mode, $plid, $key);
}
if (is_array($value['msgstr'])) {
// Sort plural variants by their form index.
ksort($value['msgstr']);
// Serialize plural variants in one string by LOCALE_PLURAL_DELIMITER.
$value['msgstr'] = implode(LOCALE_PLURAL_DELIMITER, $value['msgstr']);
}
else {
// A simple string to import.
$english = $value['msgid'];
$translation = $value['msgstr'];
_locale_import_one_string_db($report, $lang, isset($value['msgctxt']) ? $value['msgctxt'] : '', $english, $translation, $comments, $mode);
}
_locale_import_one_string_db(
$report,
$lang,
isset($value['msgctxt']) ? $value['msgctxt'] : '',
$value['msgid'],
$value['msgstr'],
$comments,
$mode
);
}
} // end of db-store operation
}
......@@ -461,15 +456,11 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL
* Location value to save with source string.
* @param $mode
* Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE.
* @param $plid
* Optional plural ID to use.
* @param $plural
* Optional plural value to use.
*
* @return
* The string ID of the existing string modified or the new string added.
*/
function _locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $location, $mode, $plid = 0, $plural = 0) {
function _locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $location, $mode) {
$lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context", array(':source' => $source, ':context' => $context))->fetchField();
if (!empty($translation)) {
......@@ -497,8 +488,6 @@ function _locale_import_one_string_db(&$report, $langcode, $context, $source, $t
'lid' => $lid,
'language' => $langcode,
'translation' => $translation,
'plid' => $plid,
'plural' => $plural,
))
->execute();
......@@ -509,8 +498,6 @@ function _locale_import_one_string_db(&$report, $langcode, $context, $source, $t
db_update('locales_target')
->fields(array(
'translation' => $translation,
'plid' => $plid,
'plural' => $plural,
))
->condition('language', $langcode)
->condition('lid', $lid)
......@@ -534,8 +521,6 @@ function _locale_import_one_string_db(&$report, $langcode, $context, $source, $t
'lid' => $lid,
'language' => $langcode,
'translation' => $translation,
'plid' => $plid,
'plural' => $plural
))
->execute();
......@@ -547,8 +532,6 @@ function _locale_import_one_string_db(&$report, $langcode, $context, $source, $t
db_delete('locales_target')
->condition('language', $langcode)
->condition('lid', $lid)
->condition('plid', $plid)
->condition('plural', $plural)
->execute();
$report['deletes']++;
......@@ -790,27 +773,6 @@ function _locale_import_tokenize_formula($formula) {
return $tokens;
}
/**
* Adds count indices to a string.
*
* Callback for array_map() within _locale_import_one_string().
*
* @param $entry
* An array element.
* @param $key
* Index of the array element.
*/
function _locale_import_append_plural($entry, $key) {
// No modifications for 0, 1
if ($key == 0 || $key == 1) {
return $entry;
}
// First remove any possibly false indices, then add new ones
$entry = preg_replace('/(@count)\[[0-9]\]/', '\\1', $entry);
return preg_replace('/(@count)/', "\\1[$key]", $entry);
}
/**
* Generates a short, one-string version of the passed comment array.
*
......@@ -872,28 +834,19 @@ function _locale_import_parse_quoted($string) {
*/
function _locale_export_get_strings($language = NULL) {
if (isset($language)) {
$result = db_query("SELECT s.lid, s.source, s.context, s.location, t.translation, t.plid, t.plural FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language ORDER BY t.plid, t.plural", array(':language' => $language->langcode));
$result = db_query("SELECT s.lid, s.source, s.context, s.location, t.translation FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language", array(':language' => $language->langcode));
}
else {
$result = db_query("SELECT s.lid, s.source, s.context, s.location, t.plid, t.plural FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid ORDER BY t.plid, t.plural");
$result = db_query("SELECT s.lid, s.source, s.context, s.location FROM {locales_source} s");
}
$strings = array();
foreach ($result as $child) {
$string = array(
$strings[$child->lid] = array(
'comment' => $child->location,
'source' => $child->source,
'context' => $child->context,
'translation' => isset($child->translation) ? $child->translation : '',
);
if ($child->plid) {
// Has a parent lid. Since we process in the order of plids,
// we already have the parent in the array, so we can add the
// lid to the next plural version to it. This builds a linked
// list of plurals.
$string['child'] = TRUE;
$strings[$child->plid]['plural'] = $child->lid;
}
$strings[$child->lid] = $string;
}
return $strings;
}
......@@ -933,6 +886,12 @@ function _locale_export_po_generate($language = NULL, $strings = array(), $heade
$header .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
if (!empty($locale_plurals[$language->langcode]['formula'])) {
$header .= "\"Plural-Forms: nplurals=" . $locale_plurals[$language->langcode]['plurals'] . "; plural=" . strtr($locale_plurals[$language->langcode]['formula'], array('$' => '')) . ";\\n\"\n";
// Remember number of plural variants to optimize the export.
$nplurals = $locale_plurals[$language->langcode]['plurals'];
}
else {
// Remember we did not have a plural number for the export.
$nplurals = 0;
}
}
else {
......@@ -956,41 +915,38 @@ function _locale_export_po_generate($language = NULL, $strings = array(), $heade
$output = $header . "\n";
foreach ($strings as $lid => $string) {
// Only process non-children, children are output below their parent.
if (!isset($string['child'])) {
if ($string['comment']) {
$output .= '#: ' . $string['comment'] . "\n";
}
if (!empty($string['context'])) {
$output .= 'msgctxt ' . _locale_export_string($string['context']);
}
$output .= 'msgid ' . _locale_export_string($string['source']);
if (!empty($string['plural'])) {
$plural = $string['plural'];
$output .= 'msgid_plural ' . _locale_export_string($strings[$plural]['source']);
if (isset($language)) {
$translation = $string['translation'];
for ($i = 0; $i < $locale_plurals[$language->langcode]['plurals']; $i++) {
$output .= 'msgstr[' . $i . '] ' . _locale_export_string($translation);
if ($plural) {
$translation = _locale_export_remove_plural($strings[$plural]['translation']);
$plural = isset($strings[$plural]['plural']) ? $strings[$plural]['plural'] : 0;
}
else {
$translation = '';
}
if ($string['comment']) {
$output .= '#: ' . $string['comment'] . "\n";
}
if (!empty($string['context'])) {
$output .= 'msgctxt ' . _locale_export_string($string['context']);
}
if (strpos($string['source'], LOCALE_PLURAL_DELIMITER) !== FALSE) {
// Export plural string.
$export_array = explode(LOCALE_PLURAL_DELIMITER, $string['source']);
$output .= 'msgid ' . _locale_export_string($export_array[0]);
$output .= 'msgid_plural ' . _locale_export_string($export_array[1]);
if (isset($language)) {
$export_array = explode(LOCALE_PLURAL_DELIMITER, $string['translation']);
for ($i = 0; $i < $nplurals; $i++) {
if (isset($export_array[$i])) {
$output .= 'msgstr[' . $i . '] ' . _locale_export_string($export_array[$i]);
}
else {
$output .= 'msgstr[' . $i . '] ""' . "\n";
}
}
else {
$output .= 'msgstr[0] ""' . "\n";
$output .= 'msgstr[1] ""' . "\n";
}
}
else {
$output .= 'msgstr ' . _locale_export_string($string['translation']);
$output .= 'msgstr[0] ""' . "\n";
$output .= 'msgstr[1] ""' . "\n";
}
$output .= "\n";
}
else {
$output .= 'msgid ' . _locale_export_string($string['source']);
$output .= 'msgstr ' . _locale_export_string($string['translation']);
}
$output .= "\n";
}
return $output;
}
......@@ -1086,13 +1042,6 @@ function _locale_export_wrap($str, $len) {
return implode("\n", $return);
}
/**
* Removes plural index information from a string.
*/
function _locale_export_remove_plural($entry) {
return preg_replace('/(@count)\[[0-9]\]/', '\\1', $entry);
}
/**
* @} End of "locale-api-import-export"
*/
......@@ -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(