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

- Patch #1445004 by Sutharsan, Gábor Hojtsy, roderik, andypost, brantwynn:...

- Patch #1445004 by Sutharsan, Gábor Hojtsy, roderik, andypost, brantwynn: Implement customized translation bit on translations.
parent 3610fd93
......@@ -23,11 +23,16 @@
* Drupal file object corresponding to the PO file to import.
* @param $langcode
* Language code.
* @param $mode
* Should existing translations be replaced LOCALE_IMPORT_KEEP or
* LOCALE_IMPORT_OVERWRITE.
* @param $overwrite_options
* An associative array indicating what data should be overwritten, if any.
* - not_customized: strings marked not customized should be overwritten.
* - customized: strings marked customized should be overwritten.
* @param $customized
* Whether the strings being imported should be saved as customized.
* Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED. All strings in the file
* will be saved with this customization flag.
*/
function _locale_import_po($file, $langcode, $mode) {
function _locale_import_po($file, $langcode, $overwrite_options, $customized = LOCALE_NOT_CUSTOMIZED) {
// Try to allocate enough time to parse and import the data.
drupal_set_time_limit(240);
......@@ -38,7 +43,7 @@ function _locale_import_po($file, $langcode, $mode) {
}
// Get strings from file (returns on failure after a partial import, or on success)
$status = _locale_import_read_po('db-store', $file, $mode, $langcode);
$status = _locale_import_read_po('db-store', $file, $overwrite_options, $langcode, $customized);
if ($status === FALSE) {
// Error messages are set in _locale_import_read_po().
return FALSE;
......@@ -80,13 +85,17 @@ function _locale_import_po($file, $langcode, $mode) {
* Storage operation type: db-store or mem-store.
* @param $file
* Drupal file object corresponding to the PO file to import.
* @param $mode
* Should existing translations be replaced LOCALE_IMPORT_KEEP or
* LOCALE_IMPORT_OVERWRITE.
* @param $overwrite_options
* An associative array indicating what data should be overwritten, if any.
* - not_customized: not customized strings should be overwritten.
* - customized: customized strings should be overwritten.
* @param $lang
* Language code.
* @param $customized
* Whether the strings being imported should be saved as customized.
* Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED.
*/
function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) {
function _locale_import_read_po($op, $file, $overwrite_options = NULL, $lang = NULL, $customized = LOCALE_NOT_CUSTOMIZED) {
// The file will get closed by PHP on returning from this function.
$fd = fopen($file->uri, 'rb');
......@@ -138,7 +147,7 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) {
}
elseif (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
// We are currently in string token, close it out.
_locale_import_one_string($op, $current, $mode, $lang, $file);
_locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized);
// Start a new entry for the comment.
$current = array();
......@@ -182,7 +191,7 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) {
if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
// We are currently in a message string, close it out.
_locale_import_one_string($op, $current, $mode, $lang, $file);
_locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized);
// Start a new context for the id.
$current = array();
......@@ -212,7 +221,7 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) {
if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
// We are currently in a message, start a new one.
_locale_import_one_string($op, $current, $mode, $lang, $file);
_locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized);
$current = array();
}
elseif (!empty($current['msgctxt'])) {
......@@ -326,7 +335,7 @@ function _locale_import_read_po($op, $file, $mode = NULL, $lang = NULL) {
// End of PO file, closed out the last entry.
if (($context == 'MSGSTR') || ($context == 'MSGSTR_ARR')) {
_locale_import_one_string($op, $current, $mode, $lang, $file);
_locale_import_one_string($op, $current, $overwrite_options, $lang, $file, $customized);
}
elseif ($context != 'COMMENT') {
_locale_import_message('The translation file %filename ended unexpectedly at line %line.', $file, $lineno);
......@@ -360,16 +369,20 @@ function _locale_import_message($message, $file, $lineno = NULL) {
* Operation to perform: 'db-store', 'db-report', 'mem-store' or 'mem-report'.
* @param $value
* Details of the string stored.
* @param $mode
* Should existing translations be replaced LOCALE_IMPORT_KEEP or
* LOCALE_IMPORT_OVERWRITE.
* @param $overwrite_options
* An associative array indicating what data should be overwritten, if any.
* - not_customized: not customized strings should be overwritten.
* - customized: customized strings should be overwritten.
* @param $lang
* Language to store the string in.
* @param $file
* Object representation of file being imported, only required when op is
* 'db-store'.
* @param $customized
* Whether the strings being imported should be saved as customized.
* Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED.
*/
function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NULL, $file = NULL) {
function _locale_import_one_string($op, $value = NULL, $overwrite_options = NULL, $lang = NULL, $file = NULL, $customized = LOCALE_NOT_CUSTOMIZED) {
$report = &drupal_static(__FUNCTION__, array('additions' => 0, 'updates' => 0, 'deletes' => 0, 'skips' => 0));
$header_done = &drupal_static(__FUNCTION__ . ':header_done', FALSE);
$strings = &drupal_static(__FUNCTION__ . ':strings', array());
......@@ -395,7 +408,7 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL
// 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'])) {
if (array_sum($overwrite_options) || empty($locale_plurals[$lang]['plurals'])) {
// Since we only need to parse the header if we ought to update the
// plural formula, only run this if we don't need to keep existing
// data untouched or if we don't have an existing plural formula.
......@@ -432,7 +445,8 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL
$value['msgid'],
$value['msgstr'],
$comments,
$mode
$overwrite_options,
$customized
);
}
} // end of db-store operation
......@@ -454,57 +468,76 @@ function _locale_import_one_string($op, $value = NULL, $mode = NULL, $lang = NUL
* Translation to language specified in $langcode.
* @param $location
* Location value to save with source string.
* @param $mode
* Import mode to use, LOCALE_IMPORT_KEEP or LOCALE_IMPORT_OVERWRITE.
* @param $overwrite_options
* An associative array indicating what data should be overwritten, if any.
* - not_customized: not customized strings should be overwritten.
* - customized: customized strings should be overwritten.
* @param $customized
* (optional) Whether the strings being imported should be saved as customized.
* Use LOCALE_CUSTOMIZED or LOCALE_NOT_CUSTOMIZED.
*
* @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) {
$lid = db_query("SELECT lid FROM {locales_source} WHERE source = :source AND context = :context", array(':source' => $source, ':context' => $context))->fetchField();
function _locale_import_one_string_db(&$report, $langcode, $context, $source, $translation, $location, $overwrite_options, $customized = LOCALE_NOT_CUSTOMIZED) {
// Initialize overwrite options if not set.
$overwrite_options += array(
'not_customized' => FALSE,
'customized' => FALSE,
);
// Look up the source string and any existing translation.
$string = db_query("SELECT s.lid, t.customized FROM {locales_source} s LEFT JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.source = :source AND s.context = :context", array(
':source' => $source,
':context' => $context,
':language' => $langcode,
))
->fetchObject();
if (!empty($translation)) {
// Skip this string unless it passes a check for dangerous code.
if (!locale_string_is_safe($translation)) {
watchdog('locale', 'Import of string "%string" was skipped because of disallowed or malformed HTML.', array('%string' => $translation), WATCHDOG_ERROR);
$report['skips']++;
$lid = 0;
return 0;
}
elseif ($lid) {
elseif (isset($string->lid)) {
// We have this source string saved already.
db_update('locales_source')
->fields(array(
'location' => $location,
))
->condition('lid', $lid)
->condition('lid', $string->lid)
->execute();
$exists = db_query("SELECT COUNT(lid) FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $langcode))->fetchField();
if (!$exists) {
if (!isset($string->customized)) {
// No translation in this language.
db_insert('locales_target')
->fields(array(
'lid' => $lid,
'lid' => $string->lid,
'language' => $langcode,
'translation' => $translation,
'customized' => $customized,
))
->execute();
$report['additions']++;
}
elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
elseif ($overwrite_options[$string->customized ? 'customized' : 'not_customized']) {
// Translation exists, only overwrite if instructed.
db_update('locales_target')
->fields(array(
'translation' => $translation,
'customized' => $customized,
))
->condition('language', $langcode)
->condition('lid', $lid)
->condition('lid', $string->lid)
->execute();
$report['updates']++;
}
return $string->lid;
}
else {
// No such source string in the database yet.
......@@ -521,23 +554,24 @@ function _locale_import_one_string_db(&$report, $langcode, $context, $source, $t
'lid' => $lid,
'language' => $langcode,
'translation' => $translation,
'customized' => $customized,
))
->execute();
$report['additions']++;
return $lid;
}
}
elseif ($mode == LOCALE_IMPORT_OVERWRITE) {
elseif (isset($string->lid) && isset($string->customized) && $overwrite_options[$string->customized ? 'customized' : 'not_customized']) {
// Empty translation, remove existing if instructed.
db_delete('locales_target')
->condition('language', $langcode)
->condition('lid', $lid)
->condition('lid', $string->lid)
->execute();
$report['deletes']++;
return $string->lid;
}
return $lid;
}
/**
......@@ -828,17 +862,67 @@ function _locale_import_parse_quoted($string) {
* @param $language
* Language object to generate the output for, or NULL if generating
* translation template.
* @param $options
* (optional) An associative array specifying what to include in the output:
* - customized: include customized strings (if TRUE)
* - uncustomized: include non-customized string (if TRUE)
* - untranslated: include untranslated source strings (if TRUE)
* Ignored if $language is NULL.
*
* @return
* An array of translated strings that can be used to generate an export.
*/
function _locale_export_get_strings($language = NULL) {
if (isset($language)) {
$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));
function _locale_export_get_strings($language = NULL, $options = array()) {
// Assume FALSE for all options if not provided by the API.
$options += array(
'customized' => FALSE,
'not_customized' => FALSE,
'not_translated' => FALSE,
);
if (array_sum($options) == 0) {
// If user asked to not include anything in the translation files,
// that would not make sense, so just fall back on providing a template.
$language = NULL;
}
// Build and execute query to collect source strings and translations.
$query = db_select('locales_source', 's');
if (!empty($language)) {
if ($options['not_translated']) {
// Left join to keep untranslated strings in.
$query->leftJoin('locales_target', 't', 's.lid = t.lid AND t.language = :language', array(':language' => $language->langcode));
}
else {
// Inner join to filter for only translations.
$query->innerJoin('locales_target', 't', 's.lid = t.lid AND t.language = :language', array(':language' => $language->langcode));
}
if ($options['customized']) {
if (!$options['not_customized']) {
// Filter for customized strings only.
$query->condition('t.customized', LOCALE_CUSTOMIZED);
}
// Else no filtering needed in this case.
}
else {
if ($options['not_customized']) {
// Filter for non-customized strings only.
$query->condition('t.customized', LOCALE_NOT_CUSTOMIZED);
}
else {
// Filter for strings without translation.
$query->isNull('t.translation');
}
}
$query->fields('t', array('translation'));
}
else {
$result = db_query("SELECT s.lid, s.source, s.context, s.location FROM {locales_source} s");
$query->leftJoin('locales_target', 't', 's.lid = t.lid');
}
$query->fields('s', array('lid', 'source', 'context', 'location'));
$result = $query->execute();
// Structure results in an array with metainformation on the strings.
$strings = array();
foreach ($result as $child) {
$strings[$child->lid] = array(
......
......@@ -73,26 +73,28 @@
');
/**
* Translation import mode overwriting all existing translations
* if new translated version available.
* Flag for locally not customized interface translation.
*
* Such translations are imported from .po files downloaded from
* localize.drupal.org for example.
*/
const LOCALE_IMPORT_OVERWRITE = 0;
const LOCALE_NOT_CUSTOMIZED = 0;
/**
* Translation import mode keeping existing translations and only
* inserting new strings.
* Flag for locally customized interface translation.
*
* Such translations are edited from their imported originals on the user
* interface or are imported as customized.
*/
const LOCALE_IMPORT_KEEP = 1;
const LOCALE_CUSTOMIZED = 1;
/**
* URL language negotiation: use the path prefix as URL language
* indicator.
* URL language negotiation: use the path prefix as URL language indicator.
*/
const LANGUAGE_NEGOTIATION_URL_PREFIX = 0;
/**
* URL language negotiation: use the domain as URL language
* indicator.
* URL language negotiation: use the domain as URL language indicator.
*/
const LANGUAGE_NEGOTIATION_URL_DOMAIN = 1;
......
......@@ -34,62 +34,80 @@ function locale_translate_import_form($form, &$form_state) {
else {
$default = key($existing_languages);
$language_options = array(
t('Already added languages') => $existing_languages,
t('Existing languages') => $existing_languages,
t('Languages not yet added') => language_admin_predefined_list()
);
}
$form['import'] = array('#type' => 'fieldset',
'#title' => t('Import translation'),
);
$form['import']['file'] = array('#type' => 'file',
'#title' => t('Language file'),
$form['file'] = array(
'#type' => 'file',
'#title' => t('Translation file'),
'#size' => 50,
'#description' => t('A Gettext Portable Object (<em>.po</em>) file.'),
);
$form['import']['langcode'] = array('#type' => 'select',
'#title' => t('Import into'),
$form['langcode'] = array(
'#type' => 'select',
'#title' => t('Language'),
'#options' => $language_options,
'#default_value' => $default,
'#description' => t('Choose the language you want to add strings into. If you choose a language which is not yet set up, it will be added.'),
);
$form['import']['mode'] = array('#type' => 'radios',
'#title' => t('Mode'),
'#default_value' => LOCALE_IMPORT_KEEP,
'#options' => array(
LOCALE_IMPORT_OVERWRITE => t('Strings in the uploaded file replace existing ones, new ones are added. The plural format is updated.'),
LOCALE_IMPORT_KEEP => t('Existing strings and the plural format are kept, only new strings are added.')
$form['customized'] = array(
'#title' => t('Treat imported strings as custom translations'),
'#type' => 'checkbox',
);
$form['overwrite_options'] = array(
'#type' => 'container',
'#tree' => TRUE,
);
$form['overwrite_options']['not_customized'] = array(
'#title' => t('Overwrite non-customized translations'),
'#type' => 'checkbox',
'#states' => array(
'checked' => array(
':input[name="customized"]' => array('checked' => TRUE),
),
),
);
$form['import']['submit'] = array('#type' => 'submit', '#value' => t('Import'));
$form['overwrite_options']['customized'] = array(
'#title' => t('Overwrite existing customized translations'),
'#type' => 'checkbox',
);
$form['actions'] = array(
'#type' => 'actions'
);
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Import')
);
return $form;
}
/**
* Process the locale import form submission.
* Processes the locale import form submission.
*/
function locale_translate_import_form_submit($form, &$form_state) {
$validators = array('file_validate_extensions' => array('po'));
// Ensure we have the file uploaded
// Ensure we have the file uploaded.
if ($file = file_save_upload('file', $validators)) {
// Add language, if not yet supported
drupal_static_reset('language_list');
$languages = language_list();
$langcode = $form_state['values']['langcode'];
if (!isset($languages[$langcode])) {
// Add language, if not yet supported.
$language = language_load($form_state['values']['langcode']);
if (empty($language)) {
include_once DRUPAL_ROOT . '/core/includes/standard.inc';
$predefined = standard_language_list();
$language = (object) array(
'langcode' => $langcode,
'langcode' => $form_state['values']['langcode'],
);
language_save($language);
drupal_set_message(t('The language %language has been created.', array('%language' => t($predefined[$langcode][0]))));
$language = language_save($language);
drupal_set_message(t('The language %language has been created.', array('%language' => t($language->name))));
}
$customized = $form_state['values']['customized'] ? LOCALE_CUSTOMIZED : LOCALE_NOT_CUSTOMIZED;
// Now import strings into the language
if ($return = _locale_import_po($file, $langcode, $form_state['values']['mode']) == FALSE) {
if ($return = _locale_import_po($file, $language->langcode, $form_state['values']['overwrite_options'], $customized) == FALSE) {
$variables = array('%filename' => $file->filename);
drupal_set_message(t('The translation import of %filename failed.', $variables), 'error');
watchdog('locale', 'The translation import of %filename failed.', $variables, WATCHDOG_ERROR);
......@@ -106,11 +124,9 @@ function locale_translate_import_form_submit($form, &$form_state) {
}
/**
* User interface for the translation export screen.
* Builds form to export Gettext translation files.
*/
function locale_translate_export_screen() {
// Get all enabled languages, except English, if we should not translate that.
drupal_static_reset('language_list');
function locale_translate_export_form($form, &$form_state) {
$languages = language_list(TRUE);
$language_options = array();
foreach ($languages as $langcode => $language) {
......@@ -118,65 +134,80 @@ function locale_translate_export_screen() {
$language_options[$langcode] = $language->name;
}
}
$language_default = language_default();
$output = '';
// Offer translation export if any language is set up.
if (!empty($language_options)) {
$elements = drupal_get_form('locale_translate_export_po_form', $language_options);
$output = drupal_render($elements);
if (empty($language_options)) {
$form['langcode'] = array(
'#type' => 'value',
'#value' => LANGUAGE_SYSTEM,
);
$form['langcode_text'] = array(
'#type' => 'item',
'#title' => t('Language'),
'#markup' => t('No language available. The export will only contain source strings.'),
);
}
else {
$form['langcode'] = array(
'#type' => 'select',
'#title' => t('Language'),
'#options' => $language_options,
'#default_value' => $language_default->langcode,
'#empty_option' => t('Source text only, no translations'),
'#empty_value' => LANGUAGE_SYSTEM,
);
$form['content_options'] = array(
'#type' => 'fieldset',
'#title' => t('Export options'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#tree' => TRUE,
'#states' => array(
'invisible' => array(
':input[name="langcode"]' => array('value' => LANGUAGE_SYSTEM),
),
),
);
$form['content_options']['not_customized'] = array(
'#type' => 'checkbox',
'#title' => t('Include non-customized translations'),
'#default_value' => TRUE,
);
$form['content_options']['customized'] = array(
'#type' => 'checkbox',
'#title' => t('Include customized translations'),
'#default_value' => TRUE,
);
$form['content_options']['not_translated'] = array(
'#type' => 'checkbox',
'#title' => t('Include untranslated text'),
'#default_value' => TRUE,
);
}
$elements = drupal_get_form('locale_translate_export_pot_form');
$output .= drupal_render($elements);
return $output;
}
/**
* Form to export PO files for the languages provided.
*
* @param $names
* An associate array with localized language names
*/
function locale_translate_export_po_form($form, &$form_state, $names) {
$form['export_title'] = array('#type' => 'item',
'#title' => t('Export translation'),
$form['actions'] = array(
'#type' => 'actions'
);
$form['langcode'] = array('#type' => 'select',
'#title' => t('Language name'),
'#options' => $names,
'#description' => t('Select the language to export in Gettext Portable Object (<em>.po</em>) format.'),
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Export')
);
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Export'));
return $form;
}
/**
* Translation template export form.
* Processes a translation (or template) export form submission.
*/
function locale_translate_export_pot_form() {
// Complete template export of the strings
$form['export_title'] = array('#type' => 'item',
'#title' => t('Export template'),
'#description' => t('Generate a Gettext Portable Object Template (<em>.pot</em>) file with all strings from the Drupal locale database.'),
);
$form['actions'] = array('#type' => 'actions');
$form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Export'));
// Reuse PO export submission callback.
$form['#submit'][] = 'locale_translate_export_po_form_submit';
return $form;
}
/**
* Process a translation (or template) export form submission.
*/
function locale_translate_export_po_form_submit($form, &$form_state) {
function locale_translate_export_form_submit($form, &$form_state) {
// If template is required, language code is not given.
$language = NULL;
if (isset($form_state['values']['langcode'])) {
$languages = language_list();
$language = $languages[$form_state['values']['langcode']];
if ($form_state['values']['langcode'] != LANGUAGE_SYSTEM) {
$language = language_load($form_state['values']['langcode']);
}
else {
$language = NULL;
}
_locale_export_po($language, _locale_export_po_generate($language, _locale_export_get_strings($language)));
$content_options = isset($form_state['values']['content_options']) ? $form_state['values']['content_options'] : array();
_locale_export_po($language, _locale_export_po_generate($language, _locale_export_get_strings($language, $content_options)));
}
/**
......@@ -267,7 +298,7 @@ function locale_translate_batch_import($filepath, &$context) {
// we can extract the language code to use for the import from the end.
if (preg_match('!(/|\.)([^\./]+)\.po$!', $filepath, $langcode)) {
$file = (object) array('filename' => drupal_basename($filepath), 'uri' => $filepath);
_locale_import_read_po('db-store', $file, LOCALE_IMPORT_KEEP, $langcode[2]);
_locale_import_read_po('db-store', $file, array(), $langcode[2]);
$context['results'][] = $filepath;