diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index fdcb01b..156ab5c 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; } @@ -2468,6 +2455,14 @@ function drupal_container(Container $new_container = NULL, $rebuild = FALSE) { // Register the KeyValueStore factory. $container ->register('keyvalue', 'Drupal\Core\KeyValueStore\KeyValueFactory'); + + // 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; } @@ -2642,13 +2637,9 @@ function drupal_installation_attempted() { * @ingroup sanitization */ function get_t() { - static $t; - // This is not converted to drupal_static because there is no point in - // resetting this as it can not change in the course of a request. - if (!isset($t)) { - $t = drupal_installation_attempted() ? 'st' : 't'; - } - return $t; + // Use always t() which will work nicely on any situation. + // @todo Remove all this logic, maybe keep st() to mark installer strings. + return 't'; } /** diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 0fcdd94..09568f9 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. @@ -1274,7 +1286,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) { @@ -1290,26 +1302,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 e74e06b..c0191e7 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. @@ -724,53 +724,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 cd2f9c0..f2a3da2 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 */ @@ -76,13 +76,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..a7ba30b --- /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/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 3d3ce1f..75803f8 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; } /** @@ -1037,6 +983,7 @@ function _locale_rebuild_js($langcode = NULL) { * Implements hook_language_init(). */ function locale_language_init() { + //new Drupal\locale\LocaleTranslation(); // Add locale helper to configuration subscribers. drupal_container()->get('dispatcher')->addSubscriber(new LocaleConfigSubscriber()); } 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)));