diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 5577be4..8cf8ad4 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -1487,8 +1487,6 @@ function bootstrap_hooks() { * @ingroup sanitization */ function t($string, array $args = array(), array $options = array()) { - static $custom_strings; - // Merge in default. if (empty($options['langcode'])) { $options['langcode'] = language(LANGUAGE_TYPE_INTERFACE)->langcode; @@ -1497,21 +1495,10 @@ function t($string, array $args = array(), array $options = array()) { $options['context'] = ''; } - // First, check for an array of customized strings. If present, use the array - // *instead of* database lookups. This is a high performance way to provide a - // handful of string replacements. See settings.php for examples. - // Cache the $custom_strings variable to improve performance. - if (!isset($custom_strings[$options['langcode']])) { - $custom_strings[$options['langcode']] = variable_get('locale_custom_strings_' . $options['langcode'], array()); - } - // Custom strings work for English too, even if locale module is disabled. - if (isset($custom_strings[$options['langcode']][$options['context']][$string])) { - $string = $custom_strings[$options['langcode']][$options['context']][$string]; - } - // Translate with locale module if enabled. - elseif ($options['langcode'] != LANGUAGE_SYSTEM && ($options['langcode'] != 'en' || variable_get('locale_translate_english', FALSE)) && function_exists('locale')) { - $string = locale($string, $options['context'], $options['langcode']); - } + $translation = drupal_container()->get('locale.translation') + ->translate($options['langcode'], $string, $options['context']); + $string = $translation === FALSE ? $string : $translation; + if (empty($args)) { return $string; } @@ -2480,6 +2467,14 @@ function drupal_container(Container $new_container = NULL, $rebuild = FALSE) { // Register the EntityManager. $container->register('plugin.manager.entity', 'Drupal\Core\Entity\EntityManager'); + + // Register a minimum translation service just in case a translation is + // attempted before language initialization. + $container + ->register('locale.translation.custom', 'Drupal\Core\Translation\CustomStrings'); + $container + ->register('locale.translation', 'Drupal\Core\Translation\TranslationFallback') + ->addArgument(new Reference('locale.translation.custom')); } return $container; } diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 2b117bc..fe377ac 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -322,11 +322,23 @@ function install_begin_request(&$install_state) { $container->register('config.factory', 'Drupal\Core\Config\ConfigFactory') ->addArgument(new Reference('config.storage')) ->addArgument(new Reference('dispatcher')); + $container + ->register('locale.translation.custom', 'Drupal\Core\Translation\CustomStrings'); drupal_container($container); } // Set up $language, so t() caller functions will still work. drupal_language_initialize(); + // Set up file based translation as fall back for localization. + drupal_container() + ->register('locale.translation.file', 'Drupal\Core\Translation\FileTranslation') + ->addArgument(variable_get('locale_translate_file_directory', conf_path() . '/files/translations')); + drupal_container() + ->register('locale.translation.custom', 'Drupal\Core\Translation\CustomStrings'); + drupal_container() + ->register('locale.translation', 'Drupal\Core\Translation\TranslationFallback') + ->addArgument(new Reference('locale.translation.custom')) + ->addArgument(new Reference('locale.translation.file')); require_once DRUPAL_ROOT . '/core/includes/ajax.inc'; // Override the module list with a minimal set of modules. @@ -1281,7 +1293,7 @@ function install_select_profile_form($form, &$form_state, $install_state) { * @see file_scan_directory() */ function install_find_translations() { - $files = install_find_translation_files(); + $files = drupal_container()->get('locale.translation.file')->findTranslationFiles(); // English does not need a translation file. array_unshift($files, (object) array('name' => 'en')); foreach ($files as $key => $file) { @@ -1297,26 +1309,6 @@ function install_find_translations() { } /** - * Finds installer translations either for a specific langcode or all languages. - * - * @param $langcode - * (optional) The language code corresponding to the language for which we - * want to find translation files. If omitted, information on all available - * files will be returned. - * - * @return - * An associative array of file information objects keyed by file URIs as - * returned by file_scan_directory(). - * - * @see file_scan_directory() - */ -function install_find_translation_files($langcode = NULL) { - $directory = variable_get('locale_translate_file_directory', conf_path() . '/files/translations'); - $files = file_scan_directory($directory, '!drupal-\d+\.\d+\.' . (!empty($langcode) ? preg_quote($langcode, '!') : '[^\.]+') . '\.po$!', array('recurse' => FALSE)); - return $files; -} - -/** * Selects which language to use during installation. * * @param $install_state diff --git a/core/includes/install.inc b/core/includes/install.inc index a2666ae..0be0305 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -6,7 +6,7 @@ */ use Drupal\Core\Database\Database; -use Drupal\locale\Gettext; +use Drupal\Core\Translation\FileTranslation; /** * Requirement severity -- Informational message only. @@ -740,53 +740,23 @@ function drupal_requirements_url($severity) { * Use st() if your code will only run during installation and never any other * time. Use get_t() if your code could run in either circumstance. * + * @todo Remove or keep as a t wrapper to mark config strings..? + * * @see t() * @see get_t() * @ingroup sanitization */ function st($string, array $args = array(), array $options = array()) { - static $strings = NULL; + static $langcode; global $install_state; - if (empty($options['context'])) { - $options['context'] = ''; - } - - if (!isset($strings)) { - $strings = array(); - if (isset($install_state['parameters']['langcode'])) { - // If the given langcode was selected, there should be at least one .po - // file with its name in the pattern drupal-$version.$langcode.po. - // This might or might not be the entire filename. It is also possible - // that multiple files end with the same suffix, even if unlikely. - $files = install_find_translation_files($install_state['parameters']['langcode']); - if (!empty($files)) { - // Register locale classes with the classloader. Locale module is not - // yet enabled at this stage, so this is not happening automatically. - drupal_classloader_register('locale', drupal_get_path('module', 'locale')); - $strings = Gettext::filesToArray($install_state['parameters']['langcode'], $files); - } - } + // If not set, initialize installation language if possible. + if (!isset($langcode) && isset($install_state['parameters']['langcode'])) { + $langcode = $install_state['parameters']['langcode']; } + $options += array('langcode' => $langcode); - require_once DRUPAL_ROOT . '/core/includes/theme.inc'; - // Transform arguments before inserting them - foreach ($args as $key => $value) { - switch ($key[0]) { - // Escaped only - case '@': - $args[$key] = check_plain($value); - break; - // Escaped and placeholder - case '%': - default: - $args[$key] = '' . check_plain($value) . ''; - break; - // Pass-through - case '!': - } - } - return strtr((!empty($strings[$options['context']][$string]) ? $strings[$options['context']][$string] : $string), $args); + return t($string, $args, $options); } /** diff --git a/core/lib/Drupal/Core/Translation/CustomStrings.php b/core/lib/Drupal/Core/Translation/CustomStrings.php new file mode 100644 index 0000000..e9fb81a --- /dev/null +++ b/core/lib/Drupal/Core/Translation/CustomStrings.php @@ -0,0 +1,25 @@ +directory = $directory; + } + + /** + * Overrides Drupal\Core\Language\StaticTranslation::loadLanguage(). + */ + protected function loadLanguage($langcode) { + // If the given langcode was selected, there should be at least one .po + // file with its name in the pattern drupal-$version.$langcode.po. + // This might or might not be the entire filename. It is also possible + // that multiple files end with the same suffix, even if unlikely. + $files = $this->findTranslationFiles($langcode); + + if (!empty($files)) { + return $this->filesToArray($langcode, $files); + } + else { + return array(); + } + } + + /** + * Finds installer translations either for a specific langcode or all languages. + * + * @param $langcode + * (optional) The language code corresponding to the language for which we + * want to find translation files. If omitted, information on all available + * files will be returned. + * + * @return + * An associative array of file information objects keyed by file URIs as + * returned by file_scan_directory(). + * + * @see file_scan_directory() + */ + public function findTranslationFiles($langcode = NULL) { + $files = file_scan_directory($this->directory, '!drupal-\d+\.\d+\.' . (!empty($langcode) ? preg_quote($langcode, '!') : '[^\.]+') . '\.po$!', array('recurse' => FALSE)); + return $files; + } + + /** + * Reads the given Gettext PO files into a data structure. + * + * @param string $langcode + * Language code string. + * @param array $files + * List of file objects with URI properties pointing to read. + * + * @return array + * Structured array as produced by a PoMemoryWriter. + * + * @see Drupal\Component\Gettext\PoMemoryWriter + */ + public static function filesToArray($langcode, array $files) { + $writer = new PoMemoryWriter(); + $writer->setLangcode($langcode); + foreach ($files as $file) { + $reader = new PoStreamReader(); + $reader->setURI($file->uri); + $reader->setLangcode($langcode); + $reader->open(); + $writer->writeItems($reader, -1); + } + return $writer->getData(); + } +} diff --git a/core/lib/Drupal/Core/Translation/StaticTranslation.php b/core/lib/Drupal/Core/Translation/StaticTranslation.php new file mode 100644 index 0000000..2b35b1b --- /dev/null +++ b/core/lib/Drupal/Core/Translation/StaticTranslation.php @@ -0,0 +1,64 @@ +translations = $translations; + } + + /** + * Implements Drupal\Core\Language\TranslationInterface::translate() + */ + public function translate($langcode, $string, $context) { + if (!isset($this->translations[$langcode])) { + $this->translations[$langcode] = $this->loadLanguage($langcode); + } + if (isset($this->translations[$langcode][$context][$string])) { + return $this->translations[$langcode][$context][$string]; + } + else { + return FALSE; + } + } + + /** + * Implements Drupal\Core\Language\TranslationInterface::reset() + */ + public function reset() { + $this->translations = array(); + } + + /** + * Add translations for new language. + */ + protected function loadLanguage($langcode) { + return array(); + } + +} diff --git a/core/lib/Drupal/Core/Translation/TranslationFallback.php b/core/lib/Drupal/Core/Translation/TranslationFallback.php new file mode 100644 index 0000000..0cad02e --- /dev/null +++ b/core/lib/Drupal/Core/Translation/TranslationFallback.php @@ -0,0 +1,61 @@ +translators = func_get_args(); + } + + /** + * Implements Drupal\Core\Translation\TranslationInterface::translate() + */ + public function translate($langcode, $string, $context) { + foreach ($this->translators as $translator) { + $translation = $translator->translate($langcode, $string, $context); + if ($translation !== FALSE) { + return $translation; + } + } + // No translator got a translation. + return FALSE; + } + + /** + * Implements Drupal\Core\Language\TranslationInterface::reset() + */ + public function reset() { + foreach ($this->translators as $translator) { + $translator->reset(); + } + } + +} diff --git a/core/lib/Drupal/Core/Translation/TranslationInterface.php b/core/lib/Drupal/Core/Translation/TranslationInterface.php new file mode 100644 index 0000000..ff76ead --- /dev/null +++ b/core/lib/Drupal/Core/Translation/TranslationInterface.php @@ -0,0 +1,38 @@ +setLangcode($langcode); - foreach ($files as $file) { - $reader = new PoStreamReader(); - $reader->setURI($file->uri); - $reader->setLangcode($langcode); - $reader->open(); - $writer->writeItems($reader, -1); - } - return $writer->getData(); - } - /** * Reads the given PO files into the database. * diff --git a/core/modules/locale/lib/Drupal/locale/LocaleBundle.php b/core/modules/locale/lib/Drupal/locale/LocaleBundle.php new file mode 100644 index 0000000..6fad4f1 --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/LocaleBundle.php @@ -0,0 +1,36 @@ +register('locale.storage', 'Drupal\locale\StringDatabaseStorage') + ->addArgument(new Reference('database')) + ->addArgument(array('target' => 'default')); + $container->register('locale.translation.lookup', 'Drupal\locale\LocaleTranslation') + ->addArgument(new Reference('locale.storage')); + // Replace the default translation service, using lookup as the fallback. + $container->register('locale.translation', 'Drupal\Core\Translation\TranslationFallback') + ->addArgument(new Reference('locale.translation.custom')) + ->addArgument(new Reference('locale.translation.lookup')); + } +} diff --git a/core/modules/locale/lib/Drupal/locale/LocaleLookup.php b/core/modules/locale/lib/Drupal/locale/LocaleLookup.php index 9dd04f6..2838524 100644 --- a/core/modules/locale/lib/Drupal/locale/LocaleLookup.php +++ b/core/modules/locale/lib/Drupal/locale/LocaleLookup.php @@ -29,7 +29,7 @@ class LocaleLookup extends CacheArray { protected $context; /** - * The locale storage + * The locale storage. * * @var Drupal\locale\StringStorageInterface */ @@ -74,13 +74,7 @@ protected function resolveCacheMiss($offset) { $value = TRUE; } $this->storage[$offset] = $value; - // Disabling the usage of string caching allows a module to watch for - // the exact list of strings used on a page. From a performance - // perspective that is a really bad idea, so we have no user - // interface for this. Be careful when turning this option off! - if (variable_get('locale_cache_strings', 1)) { - $this->persist($offset); - } + $this->persist($offset); return $value; } } diff --git a/core/modules/locale/lib/Drupal/locale/LocaleTranslation.php b/core/modules/locale/lib/Drupal/locale/LocaleTranslation.php new file mode 100644 index 0000000..62c772c --- /dev/null +++ b/core/modules/locale/lib/Drupal/locale/LocaleTranslation.php @@ -0,0 +1,73 @@ +storage = $storage; + } + + /** + * Implements Drupal\Core\Translation\TranslationInterface::translate() + */ + public function translate($langcode, $string, $context) { + // If the language is not suitable for locale module, just return. + if ($langcode == LANGUAGE_SYSTEM || ($langcode == 'en' && !variable_get('locale_translate_english', FALSE))) { + return FALSE; + } + // Strings are cached by langcode, context and roles, using instances of the + // LocaleLookup class to handle string lookup and caching. + if (!isset($this->translations[$langcode][$context])) { + $this->translations[$langcode][$context] = new LocaleLookup($langcode, $context, $this->storage); + } + $translation = $this->translations[$langcode][$context][$string]; + return $translation === TRUE ? FALSE : $translation; + } + + /** + * Implements Drupal\Core\Language\TranslationInterface::reset() + */ + public function reset() { + $this->translations = array(); + } + +} diff --git a/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php b/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php index 33138e7..0b0d664 100644 --- a/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php +++ b/core/modules/locale/lib/Drupal/locale/StringDatabaseStorage.php @@ -284,40 +284,19 @@ protected function dbStringTable($string) { * * @param Drupal\locale\StringInterface $string * The string object. - * @param string $table - * (optional) The table name. * * @return array * Array with key fields if the string has all keys, or empty array if not. */ - protected function dbStringKeys($string, $table = NULL) { - $table = $table ? $table : $this->dbStringTable($string); - if ($table && $schema = drupal_get_schema($table)) { - $keys = $schema['primary key']; - $values = $string->getValues($keys); - if (count($values) == count($keys)) { - return $values; - } + protected function dbStringKeys($string) { + if ($string->isSource()) { + $keys = array('lid'); } - return NULL; - } - - /** - * Gets field values from a string object that are in the database table. - * - * @param Drupal\locale\StringInterface $string - * The string object. - * @param string $table - * (optional) The table name. - * - * @return array - * Array with field values indexed by field name. - */ - protected function dbStringValues($string, $table = NULL) { - $table = $table ? $table : $this->dbStringTable($string); - if ($table && $schema = drupal_get_schema($table)) { - $fields = array_keys($schema['fields']); - return $string->getValues($fields); + elseif ($string->isTranslation()) { + $keys = array('lid', 'language'); + } + if (!empty($keys) && ($values = $string->getValues($keys)) && count($keys) == count($values)) { + return $values; } else { return array(); @@ -325,27 +304,6 @@ protected function dbStringValues($string, $table = NULL) { } /** - * Sets default values from storage. - * - * @param Drupal\locale\StringInterface $string - * The string object. - * @param string $table - * (optional) The table name. - */ - protected function dbStringDefaults($string, $table = NULL) { - $table = $table ? $table : $this->dbStringTable($string); - if ($table && $schema = drupal_get_schema($table)) { - $values = array(); - foreach ($schema['fields'] as $name => $info) { - if (isset($info['default'])) { - $values[$name] = $info['default']; - } - } - $string->setValues($values, FALSE); - } - } - - /** * Loads multiple string objects. * * @param array $conditions @@ -500,9 +458,16 @@ protected function dbStringSelect(array $conditions, array $options = array()) { * If the string is not suitable for this storage, an exception ithrown. */ protected function dbStringInsert($string) { - if (($table = $this->dbStringTable($string)) && ($fields = $this->dbStringValues($string, $table))) { - $this->dbStringDefaults($string, $table); - return $this->connection->insert($table, $this->options) + if ($string->isSource()) { + $string->setValues(array('context' => '', 'version' => 'none'), FALSE); + $fields = $string->getValues(array('source', 'context', 'version')); + } + elseif ($string->isTranslation()) { + $string->setValues(array('customized' => 0), FALSE); + $fields = $string->getValues(array('lid', 'language', 'translation', 'customized')); + } + if (!empty($fields)) { + return $this->connection->insert($this->dbStringTable($string), $this->options) ->fields($fields) ->execute(); } @@ -527,13 +492,17 @@ protected function dbStringInsert($string) { * If the string is not suitable for this storage, an exception is thrown. */ protected function dbStringUpdate($string) { - if (($table = $this->dbStringTable($string)) && ($keys = $this->dbStringKeys($string, $table)) && - ($fields = $this->dbStringValues($string, $table)) && ($values = array_diff_key($fields, $keys))) - { - return $this->connection->merge($table, $this->options) - ->key($keys) - ->fields($values) - ->execute(); + if ($string->isSource()) { + $values = $string->getValues(array('source', 'context', 'version')); + } + elseif ($string->isTranslation()) { + $values = $string->getValues(array('translation', 'customized')); + } + if (!empty($values) && $keys = $this->dbStringKeys($string)) { + return $this->connection->merge($this->dbStringTable($string), $this->options) + ->key($keys) + ->fields($values) + ->execute(); } else { throw new StringStorageException(format_string('The string cannot be updated: @string', array( diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleExportTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleExportTest.php index 7ae1bf7..816b726 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleExportTest.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleExportTest.php @@ -79,9 +79,11 @@ function testExportTranslation() { ), t('Import')); drupal_unlink($name); - // We can't import a string with an empty translation, but calling - // locale() for an new string creates an entry in the locales_source table. - locale('February', NULL, 'fr'); + // Create string without translation in the locales_source table. + locale_storage() + ->createString() + ->setString('February') + ->save(); // Export only customized French translations. $this->drupalPost('admin/config/regional/translate/export', array( diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 3ebe97d..8f3a711 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -16,6 +16,7 @@ use Drupal\locale\StringDatabaseStorage; use Drupal\locale\TranslationsStream; use Drupal\Core\Database\Database; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; /** * Regular expression pattern used to localize JavaScript strings. @@ -291,76 +292,21 @@ function locale_translatable_language_list() { } /** - * Provides interface translation services. - * - * This function is called from t() to translate a string if needed. - * - * @param $string - * A string to look up translation for. If omitted, all the - * cached strings will be returned in all languages already - * used on the page. - * @param $context - * The context of this string. - * @param $langcode - * Language code to use for the lookup. - */ -function locale($string = NULL, $context = NULL, $langcode = NULL) { - $language_interface = language(LANGUAGE_TYPE_INTERFACE); - - // Use the advanced drupal_static() pattern, since this is called very often. - static $drupal_static_fast; - if (!isset($drupal_static_fast)) { - $drupal_static_fast['locale'] = &drupal_static(__FUNCTION__, array('cache' => array(), 'exists' => NULL)); - } - $locale_t = &$drupal_static_fast['locale']['cache']; - $locale_exists = &$drupal_static_fast['locale']['exists']; - - // Check whether Locale module is actually installed and operational. - // The mere existence of locale() does not imply that Locale module is - // actually enabled and its database tables are installed. Since PHP code - // cannot be unloaded, this is typically the case in the environment that - // is executing a test. - if (!isset($locale_exists)) { - $locale_exists = function_exists('module_exists') && module_exists('locale'); - } - if (!$locale_exists) { - return $string; - } - - if (!isset($string)) { - // Return all cached strings if no string was specified - return $locale_t; - } - - $langcode = isset($langcode) ? $langcode : $language_interface->langcode; - - // Strings are cached by langcode, context and roles, using instances of the - // LocaleLookup class to handle string lookup and caching. - if (!isset($locale_t[$langcode][$context]) && isset($language_interface)) { - $locale_t[$langcode][$context] = new LocaleLookup($langcode, $context, locale_storage()); - } - return ($locale_t[$langcode][$context][$string] === TRUE ? $string : $locale_t[$langcode][$context][$string]); -} - -/** - * Reset static variables used by locale(). - */ -function locale_reset() { - drupal_static_reset('locale'); -} - -/** - * Gets the locale storage controller class . + * Gets the locale storage controller. * * @return Drupal\locale\StringStorageInterface */ function locale_storage() { - $storage = &drupal_static(__FUNCTION__); - if (!isset($storage)) { - $options = array('target' => 'default'); - $storage = new StringDatabaseStorage(Database::getConnection($options['target']), $options); + try { + return drupal_container()->get('locale.storage'); + } + catch (InvalidArgumentException $e) { + // The locale storage is not initialized yet, which happens when + // regenerating JavaScript strings during updates. + $storage = new StringDatabaseStorage(drupal_container()->get('database'), array('target' => 'default')); + drupal_container()->set('locale.storage', $storage); + return $storage; } - return $storage; } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/InstallerLanguageTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/InstallerLanguageTest.php index 9960d4c..19d78cf 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Common/InstallerLanguageTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Common/InstallerLanguageTest.php @@ -8,6 +8,7 @@ namespace Drupal\system\Tests\Common; use Drupal\simpletest\WebTestBase; +use Drupal\Core\Translation\FileTranslation; /** * Tests installer language detection. @@ -41,9 +42,9 @@ function testInstallerTranslationFiles() { 'hu' => array('drupal-8.0.hu.po'), 'it' => array(), ); - + $file_translation = new FileTranslation(variable_get('locale_translate_file_directory')); foreach ($expected_translation_files as $langcode => $files_expected) { - $files_found = install_find_translation_files($langcode); + $files_found = $file_translation->findTranslationFiles($langcode); $this->assertTrue(count($files_found) == count($files_expected), format_string('@count installer languages found.', array('@count' => count($files_expected)))); foreach ($files_found as $file) { $this->assertTrue(in_array($file->filename, $files_expected), format_string('@file found.', array('@file' => $file->filename)));