diff --git a/core/misc/drupal.js b/core/misc/drupal.js index 1905976..3435dc0 100644 --- a/core/misc/drupal.js +++ b/core/misc/drupal.js @@ -192,9 +192,9 @@ if (window.jQuery) { /** * Replace placeholders with sanitized values in a string. * - * @param str + * @param {String} str * A string with placeholders. - * @param args + * @param {Object} args * An object of replacements pairs to make. Incidences of any key in this * array are replaced with the corresponding value. Based on the first * character of the key, the value is escaped and/or themed: @@ -203,6 +203,9 @@ if (window.jQuery) { * - %variable: escape text and theme as a placeholder for user-submitted * content (checkPlain + Drupal.theme('placeholder')) * + * @return {String} + * Returns the replaced string. + * * @see Drupal.t() * @ingroup sanitization */ @@ -223,10 +226,61 @@ if (window.jQuery) { args[key] = Drupal.theme('placeholder', args[key]); break; } - str = str.replace(key, args[key]); } } - return str; + + return Drupal.stringReplace(str, args, null); + }; + + /** + * Replace substring. + * + * The longest keys will be tried first. Once a substring has been replaced, + * its new value will not be searched again. + * + * @param {String} str + * A string with placeholders. + * @param {Object} args + * Key-value pairs. + * @param {Array|null} keys + * Array of keys from the "args". Internal use only. + * + * @return {String} + * Returns the replaced string. + */ + Drupal.stringReplace = function (str, args, keys) { + if (str.length === 0) { + return str; + } + + // If the array of keys is not passed then collect the keys from the args. + if (!Array.isArray(keys)) { + keys = []; + for (var k in args) { + if (args.hasOwnProperty(k)) { + keys.push(k); + } + } + + // Order the keys by the character length. The shortest one is the first. + keys.sort(function (a, b) { return a.length - b.length; }); + } + + if (keys.length === 0) { + return str; + } + + // Take next longest one from the end. + var key = keys.pop(); + var fragments = str.split(key); + + if (keys.length) { + for (var i = 0; i < fragments.length; i++) { + fragments[i] = Drupal.stringReplace(fragments[i], args, keys); + } + } + + return fragments.join(args[key]); }; /** @@ -273,49 +327,48 @@ if (window.jQuery) { /** * Format a string containing a count of items. * - * This function ensures that the string is pluralized correctly. Since Drupal.t() is - * called by this function, make sure not to pass already-localized strings to it. + * This function ensures that the string is pluralized correctly. Since + * Drupal.t() is called by this function, make sure not to pass + * already-localized strings to it. * - * See the documentation of the server-side format_plural() function for further details. + * See the documentation of the server-side format_plural() function for + * further details. * - * @param count + * @param {Number} count * The item count to display. - * @param singular + * @param {String} singular * The string for the singular case. Please make sure it is clear this is - * singular, to ease translation (e.g. use "1 new comment" instead of "1 new"). - * Do not use @count in the singular string. - * @param plural - * The string for the plural case. Please make sure it is clear this is plural, - * to ease translation. Use @count in place of the item count, as in "@count - * new comments". - * @param args + * singular, to ease translation (e.g. use "1 new comment" instead of "1 + * new"). Do not use @count in the singular string. + * @param {String} plural + * The string for the plural case. Please make sure it is clear this is + * plural, to ease translation. Use @count in place of the item count, as in + * "@count new comments". + * @param {Object} args * An object of replacements pairs to make after translation. Incidences * of any key in this array are replaced with the corresponding value. * See Drupal.formatString(). * Note that you do not need to include @count in this array. * This replacement is done automatically for the plural case. - * @param options + * @param {Object} options * The options to pass to the Drupal.t() function. - * @return + * + * @return {String} * A translated string. */ Drupal.formatPlural = function (count, singular, plural, args, options) { args = args || {}; args['@count'] = count; + + var pluralDelimiter = Drupal.locale.pluralDelimiter; + // Determine the index of the plural form. var index = Drupal.locale.pluralFormula ? Drupal.locale.pluralFormula(args['@count']) : ((args['@count'] === 1) ? 0 : 1); + var translations = Drupal + .t(singular + pluralDelimiter + plural, args, options) + .split(pluralDelimiter); - if (index === 0) { - return Drupal.t(singular, args, options); - } - else if (index === 1) { - return Drupal.t(plural, args, options); - } - else { - args['@count[' + index + ']'] = args['@count']; - delete args['@count']; - return Drupal.t(plural.replace('@count', '@count[' + index + ']'), args, options); - } + return translations[index]; }; /** diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleJavascriptTranslation.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleJavascriptTranslation.php index 3d43b3d..8356693 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleJavascriptTranslation.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleJavascriptTranslation.php @@ -31,7 +31,6 @@ public static function getInfo() { } function testFileParsing() { - $filename = drupal_get_path('module', 'locale') . '/tests/locale_test.js'; // Parse the file to look for source strings. @@ -44,52 +43,46 @@ function testFileParsing() { 'type' => 'javascript', 'name' => $filename, )); + + $source_strings = array(); foreach ($strings as $string) { $source_strings[$string->source] = $string->context; } + + $etx = LOCALE_PLURAL_DELIMITER; // List of all strings that should be in the file. $test_strings = array( - "Standard Call t" => '', - "Whitespace Call t" => '', + 'Standard Call t' => '', + 'Whitespace Call t' => '', - "Single Quote t" => '', + 'Single Quote t' => '', "Single Quote \\'Escaped\\' t" => '', - "Single Quote Concat strings t" => '', + 'Single Quote Concat strings t' => '', - "Double Quote t" => '', + 'Double Quote t' => '', "Double Quote \\\"Escaped\\\" t" => '', - "Double Quote Concat strings t" => '', - - "Context !key Args t" => "Context string", - - "Context Unquoted t" => "Context string unquoted", - "Context Single Quoted t" => "Context string single quoted", - "Context Double Quoted t" => "Context string double quoted", - - "Standard Call plural" => '', - "Standard Call @count plural" => '', - "Whitespace Call plural" => '', - "Whitespace Call @count plural" => '', - - "Single Quote plural" => '', - "Single Quote @count plural" => '', - "Single Quote \\'Escaped\\' plural" => '', - "Single Quote \\'Escaped\\' @count plural" => '', - - "Double Quote plural" => '', - "Double Quote @count plural" => '', - "Double Quote \\\"Escaped\\\" plural" => '', - "Double Quote \\\"Escaped\\\" @count plural" => '', - - "Context !key Args plural" => "Context string", - "Context !key Args @count plural" => "Context string", - - "Context Unquoted plural" => "Context string unquoted", - "Context Unquoted @count plural" => "Context string unquoted", - "Context Single Quoted plural" => "Context string single quoted", - "Context Single Quoted @count plural" => "Context string single quoted", - "Context Double Quoted plural" => "Context string double quoted", - "Context Double Quoted @count plural" => "Context string double quoted", + 'Double Quote Concat strings t' => '', + + 'Context !key Args t' => 'Context string', + + 'Context Unquoted t' => 'Context string unquoted', + 'Context Single Quoted t' => 'Context string single quoted', + 'Context Double Quoted t' => 'Context string double quoted', + + "Standard Call plural{$etx}Standard Call @count plural" => '', + "Whitespace Call plural{$etx}Whitespace Call @count plural" => '', + + "Single Quote plural{$etx}Single Quote @count plural" => '', + "Single Quote \\'Escaped\\' plural{$etx}Single Quote \\'Escaped\\' @count plural" => '', + + "Double Quote plural{$etx}Double Quote @count plural" => '', + "Double Quote \\\"Escaped\\\" plural{$etx}Double Quote \\\"Escaped\\\" @count plural" => '', + + "Context !key Args plural{$etx}Context !key Args @count plural" => 'Context string', + + "Context Unquoted plural{$etx}Context Unquoted @count plural" => 'Context string unquoted', + "Context Single Quoted plural{$etx}Context Single Quoted @count plural" => 'Context string single quoted', + "Context Double Quoted plural{$etx}Context Double Quoted @count plural" => 'Context string double quoted', ); // Assert that all strings were found properly. @@ -97,12 +90,13 @@ function testFileParsing() { $args = array('%source' => $str, '%context' => $context); // Make sure that the string was found in the file. - $this->assertTrue(isset($source_strings[$str]), String::format("Found source string: %source", $args)); + $this->assertTrue(isset($source_strings[$str]), String::format('Found source string: %source', $args)); // Make sure that the proper context was matched. - $this->assertTrue(isset($source_strings[$str]) && $source_strings[$str] === $context, strlen($context) > 0 ? String::format("Context for %source is %context", $args) : String::format("Context for %source is blank", $args)); + $message = $context ? String::format('Context for %source is %context', $args) : String::format('Context for %source is blank', $args); + $this->assertTrue(isset($source_strings[$str]) && $source_strings[$str] === $context, $message); } - $this->assertEqual(count($source_strings), count($test_strings), "Found correct number of source strings."); + $this->assertEqual(count($source_strings), count($test_strings), 'Found correct number of source strings.'); } } diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 21748ae..8386be5 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -10,6 +10,7 @@ * object files are supported. */ +use Drupal\Component\Utility\Json; use Drupal\Core\Cache\Cache; use Drupal\Core\Language\Language; use Drupal\Component\Utility\Crypt; @@ -644,7 +645,7 @@ function locale_js_translate(array $files = array()) { } // Add the translation JavaScript file to the page. - $locale_javascripts = \Drupal::state()->get('translation.javascript') ?: array(); + $locale_javascripts = \Drupal::state()->get('locale.translation.javascript') ?: array(); $translation_file = NULL; if (!empty($files) && !empty($locale_javascripts[$language_interface->id])) { // Add the translation JavaScript file to the page. @@ -1168,6 +1169,19 @@ function _locale_refresh_configuration(array $langcodes, array $lids) { } /** + * Removes the quotes and string concatenations from the string. + * + * @param string $string + * Single or double quoted strings, optionally concatenated by plus (+) sign. + * + * @return string + * String with leading and trailing quotes removed. + */ +function _locale_strip_quotes($string) { + return implode('', preg_split('~(? $string) { $matches[] = array( - 'string' => $string, - 'context' => $t_matches[2][$key], + 'source' => _locale_strip_quotes($string), + 'context' => _locale_strip_quotes($t_matches[2][$key]), ); } // Add string from Drupal.formatPlural(). foreach ($plural_matches[1] as $key => $string) { $matches[] = array( - 'string' => $string, - 'context' => $plural_matches[3][$key], + 'source' => _locale_strip_quotes($string) . LOCALE_PLURAL_DELIMITER . _locale_strip_quotes($plural_matches[2][$key]), + 'context' => _locale_strip_quotes($plural_matches[3][$key]), ); - - // If there is also a plural version of this string, add it to the strings array. - if (isset($plural_matches[2][$key])) { - $matches[] = array( - 'string' => $plural_matches[2][$key], - 'context' => $plural_matches[3][$key], - ); - } } // Loop through all matches and process them. foreach ($matches as $match) { - - // Remove the quotes and string concatenations from the string and context. - $string = implode('', preg_split('~(?findString(array('source' => $string, 'context' => $context)); + $source = \Drupal::service('locale.storage')->findString($match); if (!$source) { // We don't have the source string yet, thus we insert it into the database. - $source = \Drupal::service('locale.storage')->createString(array( - 'source' => $string, - 'context' => $context, - )); + $source = \Drupal::service('locale.storage')->createString($match); } + // Besides adding the location this will tag it for current version. $source->addLocation('javascript', $filepath); $source->save(); @@ -1322,6 +1321,9 @@ function _locale_invalidate_js($langcode = NULL) { * * @param $langcode * The language, the translation file should be (re)created for. + * + * @return bool + * TRUE if translation file exists, FALSE otherwise. */ function _locale_rebuild_js($langcode = NULL) { $config = \Drupal::config('locale.settings'); @@ -1350,14 +1352,18 @@ function _locale_rebuild_js($langcode = NULL) { $data_hash = NULL; $data = $status = ''; if (!empty($translations)) { + $data = array( + 'pluralDelimiter: ' . Json::encode(LOCALE_PLURAL_DELIMITER), + ); - $data = "Drupal.locale = { "; $locale_plurals = \Drupal::state()->get('locale.translation.plurals') ?: array(); - if (!empty($locale_plurals[$language->id])) { - $data .= "'pluralFormula': function (\$n) { return Number({$locale_plurals[$language->id]['formula']}); }, "; + if (!empty($locale_plurals[$language->id]['formula'])) { + $data[] = "pluralFormula: function (\$n) { return Number({$locale_plurals[$language->id]['formula']}); }"; } - $data .= "'strings': " . drupal_json_encode($translations) . " };"; + $data[] = 'strings: ' . Json::encode($translations); + + $data = 'Drupal.locale = { ' . implode(', ', $data) . ' };'; $data_hash = Crypt::hashBase64($data); }