includes/locale.inc | 122 ++++++++++++++++++++++++---------- misc/drupal.js | 24 +++++-- modules/locale/locale.test | 73 +++++++++++++-------- modules/locale/tests/locale_test.js | 7 ++ 4 files changed, 155 insertions(+), 71 deletions(-) diff --git a/includes/locale.inc b/includes/locale.inc index ae1322c..9c202e2 100644 --- a/includes/locale.inc +++ b/includes/locale.inc @@ -43,6 +43,33 @@ define('LOCALE_LANGUAGE_NEGOTIATION_SESSION', 'locale-session'); define('LOCALE_JS_STRING', '(?:(?:\'(?:\\\\\'|[^\'])*\'|"(?:\\\\"|[^"])*")(?:\s*\+\s*)?)+'); /** + * Regular expression pattern used to match simple JS object literal. + * This will fail on an object with nested objects. + */ +define('LOCALE_JS_OBJECT', '\{.*?\}'); + +/** + * Regular expression to match an object containing a key 'context' + * with a string value, which is captured. Will fail if there + * are nested objects. + */ +define('LOCALE_JS_OBJECT_CONTEXT', ' + \{ # match object literal start + .*? # match anything, non-greedy + (?: # match a form of "context" + \'context\' + | + "context" + | + context + ) + \s*:\s* # match key-value separator ":" + (' . LOCALE_JS_STRING . ') # match context string + .*? # match anything, non-greedy + \} # match end of object literal +'); + +/** * Translation import mode overwriting all existing translations * if new translated version available. */ @@ -510,6 +537,9 @@ function _locale_parse_js_file($filepath) { [^\w]Drupal\s*\.\s*t\s* # match "Drupal.t" with whitespace \(\s* # match "(" argument list start (' . LOCALE_JS_STRING . ')\s* # capture string argument + (?:,\s*' . LOCALE_JS_OBJECT . '\s* # optionally capture str args + (?:,\s*' . LOCALE_JS_OBJECT_CONTEXT . '\s*) # optionally capture context + ?)? # close optional args [,\)] # match ")" or "," to finish ~sx', $file, $t_matches); @@ -537,54 +567,74 @@ function _locale_parse_js_file($filepath) { (?:\s*\+\s*)? # match "+" with possible whitespace, for str concat )+ # match multiple because we supports concatenating strs )\s* # end capturing of plural string argument + (?:,\s*' . LOCALE_JS_OBJECT . '\s* # optionally capture string args + (?:,\s*' . LOCALE_JS_OBJECT_CONTEXT . '\s*)? # optionally capture context + )? [,\)] ~sx', $file, $plural_matches); + $matches = array(); - // Loop through all matches and process them. - $all_matches = array_merge($plural_matches[1], $t_matches[1]); - foreach ($all_matches as $key => $string) { - $strings = array($string); + // Add strings from Drupal.t(). + foreach ($t_matches[1] as $key => $string) { + $matches[] = array( + 'string' => $string, + 'context' => $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], + ); // If there is also a plural version of this string, add it to the strings array. if (isset($plural_matches[2][$key])) { - $strings[] = $plural_matches[2][$key]; + $matches[] = array( + 'string' => $plural_matches[2][$key], + 'context' => $plural_matches[3][$key], + ); } + } - foreach ($strings as $key => $string) { - // Remove the quotes and string concatenations from the string. - $string = implode('', preg_split('~(? $string))->fetchObject(); - if ($source) { - // We already have this source string and now have to add the location - // to the location column, if this file is not yet present in there. - $locations = preg_split('~\s*;\s*~', $source->location); - - if (!in_array($filepath, $locations)) { - $locations[] = $filepath; - $locations = implode('; ', $locations); - - // Save the new locations string to the database. - db_update('locales_source') - ->fields(array( - 'location' => $locations, - )) - ->condition('lid', $source->lid) - ->execute(); - } - } - else { - // We don't have the source string yet, thus we insert it into the database. - db_insert('locales_source') + // Loop through all matches and process them. + foreach ($matches as $key => $match) { + + // Remove the quotes and string concatenations from the string and context. + $string = implode('', preg_split('~(? $string, ':context' => $context))->fetchObject(); + if ($source) { + // We already have this source string and now have to add the location + // to the location column, if this file is not yet present in there. + $locations = preg_split('~\s*;\s*~', $source->location); + + if (!in_array($filepath, $locations)) { + $locations[] = $filepath; + $locations = implode('; ', $locations); + + // Save the new locations string to the database. + db_update('locales_source') ->fields(array( - 'location' => $filepath, - 'source' => $string, - 'context' => '', + 'location' => $locations, )) + ->condition('lid', $source->lid) ->execute(); } } + else { + // We don't have the source string yet, thus we insert it into the database. + db_insert('locales_source') + ->fields(array( + 'location' => $filepath, + 'source' => $string, + 'context' => $context, + )) + ->execute(); + } } } @@ -640,11 +690,11 @@ function _locale_rebuild_js($langcode = NULL) { // Construct the array for JavaScript translations. // Only add strings with a translation to the translations array. - $result = db_query("SELECT s.lid, s.source, t.translation FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.location LIKE '%.js%'", array(':language' => $language->language)); + $result = db_query("SELECT s.lid, s.source, s.context, t.translation FROM {locales_source} s INNER JOIN {locales_target} t ON s.lid = t.lid AND t.language = :language WHERE s.location LIKE '%.js%'", array(':language' => $language->language)); $translations = array(); foreach ($result as $data) { - $translations[$data->source] = $data->translation; + $translations[$data->context][$data->source] = $data->translation; } // Construct the JavaScript file, if there are translations. diff --git a/misc/drupal.js b/misc/drupal.js index 7e2cc4d..7ae737c 100644 --- a/misc/drupal.js +++ b/misc/drupal.js @@ -177,13 +177,21 @@ Drupal.formatString = function(str, 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(). + * + * @param options + * - 'context' (defaults to the empty context): The context the source string + * belongs to. + * * @return * The translated string. */ -Drupal.t = function (str, args) { +Drupal.t = function (str, args, options) { + options = options || {}; + options.context = options.context || ''; + // Fetch the localized version of the string. - if (Drupal.locale.strings && Drupal.locale.strings[str]) { - str = Drupal.locale.strings[str]; + if (Drupal.locale.strings && Drupal.locale.strings[options.context] && Drupal.locale.strings[options.context][str]) { + str = Drupal.locale.strings[options.context][str]; } if (args) { @@ -216,25 +224,27 @@ Drupal.t = function (str, args) { * 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 + * The options to pass to the Drupal.t() function. * @return * A translated string. */ -Drupal.formatPlural = function (count, singular, plural, args) { +Drupal.formatPlural = function (count, singular, plural, args, options) { var args = args || {}; args['@count'] = count; // Determine the index of the plural form. var index = Drupal.locale.pluralFormula ? Drupal.locale.pluralFormula(args['@count']) : ((args['@count'] == 1) ? 0 : 1); if (index == 0) { - return Drupal.t(singular, args); + return Drupal.t(singular, args, options); } else if (index == 1) { - return Drupal.t(plural, args); + return Drupal.t(plural, args, options); } else { args['@count[' + index + ']'] = args['@count']; delete args['@count']; - return Drupal.t(plural.replace('@count', '@count[' + index + ']'), args); + return Drupal.t(plural.replace('@count', '@count[' + index + ']'), args, options); } }; diff --git a/modules/locale/locale.test b/modules/locale/locale.test index 82e7694..af6c192 100644 --- a/modules/locale/locale.test +++ b/modules/locale/locale.test @@ -199,43 +199,60 @@ class LocaleJavascriptTranslationTest extends DrupalWebTestCase { // Get all of the source strings that were found. $source_strings = db_select('locales_source', 's') - ->fields('s', array('source', 'lid')) + ->fields('s', array('source', 'context')) ->condition('s.location', $filename) ->execute() ->fetchAllKeyed(); // 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 \\'Escaped\\' t", - "Single Quote Concat strings t", + "Single Quote t" => '', + "Single Quote \\'Escaped\\' t" => '', + "Single Quote Concat strings t" => '', - "Double Quote t", - "Double Quote \\\"Escaped\\\" t", - "Double Quote Concat strings t", + "Double Quote t" => '', + "Double Quote \\\"Escaped\\\" t" => '', + "Double Quote Concat strings t" => '', - "Standard Call plural", - "Standard Call @count plural", - "Whitespace Call plural", - "Whitespace Call @count plural", + "Context Unquoted t" => "Context string unquoted", + "Context Single Quoted t" => "Context string single quoted", + "Context Double Quoted t" => "Context string double quoted", - "Single Quote plural", - "Single Quote @count plural", - "Single Quote \\'Escaped\\' plural", - "Single Quote \\'Escaped\\' @count plural", + "Standard Call plural" => '', + "Standard Call @count plural" => '', + "Whitespace Call plural" => '', + "Whitespace Call @count plural" => '', - "Double Quote plural", - "Double Quote @count plural", - "Double Quote \\\"Escaped\\\" plural", - "Double Quote \\\"Escaped\\\" @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 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", ); // Assert that all strings were found properly. - foreach ($test_strings as $str) { - $this->assertTrue(isset($source_strings[$str]), t("Found source string: %source", array('%source' => $str))); + foreach ($test_strings as $str => $context) { + $args = array('%source' => $str, '%context' => $context); + + // Make sure that the string was found in the file. + $this->assertTrue(isset($source_strings[$str]), t("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 ? t("Context for %source is %context", $args) : t("Context for %source is blank", $args)); } $this->assertEqual(count($source_strings), count($test_strings), t("Found correct number of source strings.")); diff --git a/modules/locale/tests/locale_test.js b/modules/locale/tests/locale_test.js index 251d115..b4894af 100644 --- a/modules/locale/tests/locale_test.js +++ b/modules/locale/tests/locale_test.js @@ -16,6 +16,9 @@ Drupal.t("Double Quote t"); Drupal.t("Double Quote \"Escaped\" t"); Drupal.t("Double Quote " + "Concat " + "strings " + "t"); +Drupal.t("Context Unquoted t", {}, {context: "Context string unquoted"}); +Drupal.t("Context Single Quoted t", {}, {'context': "Context string single quoted"}); +Drupal.t("Context Double Quoted t", {}, {"context": "Context string double quoted"}); Drupal.formatPlural(1, "Standard Call plural", "Standard Call @count plural"); Drupal @@ -33,3 +36,7 @@ Drupal.formatPlural(1, 'Single Quote \'Escaped\' plural', 'Single Quote \'Escape Drupal.formatPlural(1, "Double Quote plural", "Double Quote @count plural"); Drupal.formatPlural(1, "Double Quote \"Escaped\" plural", "Double Quote \"Escaped\" @count plural"); + +Drupal.formatPlural(1, "Context Unquoted plural", "Context Unquoted @count plural", {}, {context: "Context string unquoted"}); +Drupal.formatPlural(1, "Context Single Quoted plural", "Context Single Quoted @count plural", {}, {'context': "Context string single quoted"}); +Drupal.formatPlural(1, "Context Double Quoted plural", "Context Double Quoted @count plural", {}, {"context": "Context string double quoted"});