Skip to content
Snippets Groups Projects
Commit 2edd9168 authored by Angie Byron's avatar Angie Byron
Browse files

Issue #2122175 by dclavain, SiliconMind, tim.plunkett, YesCT: String...

Issue #2122175 by dclavain, SiliconMind, tim.plunkett, YesCT: String translation does not honor language fallback.
parent 0cf0a527
No related branches found
No related tags found
No related merge requests found
......@@ -2,14 +2,15 @@
/**
* @file
* Contains \Drupal\locale\Locale\Lookup.
* Contains \Drupal\locale\LocaleLookup.
*/
namespace Drupal\locale;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheCollector;
use Drupal\Core\DestructableInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Lock\LockBackendInterface;
/**
......@@ -52,6 +53,20 @@ class LocaleLookup extends CacheCollector {
*/
protected $lock;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Constructs a LocaleLookup object.
*
......@@ -65,11 +80,17 @@ class LocaleLookup extends CacheCollector {
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct($langcode, $context, StringStorageInterface $string_storage, CacheBackendInterface $cache, LockBackendInterface $lock) {
public function __construct($langcode, $context, StringStorageInterface $string_storage, CacheBackendInterface $cache, LockBackendInterface $lock, ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager) {
$this->langcode = $langcode;
$this->context = (string) $context;
$this->stringStorage = $string_storage;
$this->configFactory = $config_factory;
$this->languageManager = $language_manager;
// Add the current user's role IDs to the cache key, this ensures that, for
// example, strings for admin menu items and settings forms are not cached
......@@ -99,18 +120,46 @@ protected function resolveCacheMiss($offset) {
'source' => $offset,
'context' => $this->context,
'version' => \Drupal::VERSION
))->addLocation('path', request_uri())->save();
))->addLocation('path', $this->requestUri())->save();
$value = TRUE;
}
// If there is no translation available for the current language then use
// language fallback to try other translations.
if ($value === TRUE) {
$fallbacks = $this->languageManager->getFallbackCandidates($this->langcode, array('operation' => 'locale_lookup', 'data' => $offset));
if (!empty($fallbacks)) {
foreach($fallbacks as $langcode) {
$translation = $this->stringStorage->findTranslation(array(
'language' => $langcode,
'source' => $offset,
'context' => $this->context,
));
if ($translation && !empty($translation->translation)) {
$value = $translation->translation;
break;
}
}
}
}
$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 (\Drupal::config('locale.settings')->get('cache_strings')) {
if ($this->configFactory->get('locale.settings')->get('cache_strings')) {
$this->persist($offset);
}
return $value;
}
/**
* Wraps request_uri().
*/
protected function requestUri($omit_query_string = FALSE) {
return request_uri($omit_query_string);
}
}
......@@ -11,11 +11,9 @@
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DestructableInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Lock\LockBackendAbstract;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\StringTranslation\Translator\TranslatorInterface;
use Drupal\locale\StringStorageInterface;
use Drupal\locale\LocaleLookup;
/**
* String translator using the locale module.
......@@ -69,6 +67,13 @@ class LocaleTranslation implements TranslatorInterface, DestructableInterface {
*/
protected $translateEnglish;
/**
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Constructs a translator using a string storage.
*
......@@ -80,12 +85,15 @@ class LocaleTranslation implements TranslatorInterface, DestructableInterface {
* The lock backend.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
*/
public function __construct(StringStorageInterface $storage, CacheBackendInterface $cache, LockBackendInterface $lock, ConfigFactoryInterface $config_factory) {
public function __construct(StringStorageInterface $storage, CacheBackendInterface $cache, LockBackendInterface $lock, ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager) {
$this->storage = $storage;
$this->cache = $cache;
$this->lock = $lock;
$this->configFactory = $config_factory;
$this->languageManager = $language_manager;
}
/**
......@@ -99,7 +107,7 @@ public function getStringTranslation($langcode, $string, $context) {
// 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, $this->cache, $this->lock);
$this->translations[$langcode][$context] = new LocaleLookup($langcode, $context, $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager);
}
$translation = $this->translations[$langcode][$context]->get($string);
return $translation === TRUE ? FALSE : $translation;
......
......@@ -12,7 +12,7 @@ services:
arguments: ['@database']
string_translator.locale.lookup:
class: Drupal\locale\LocaleTranslation
arguments: ['@locale.storage', '@cache.cache', '@lock', '@config.factory']
arguments: ['@locale.storage', '@cache.cache', '@lock', '@config.factory', '@language_manager']
tags:
- { name: string_translator }
- { name: needs_destruction }
<?php
/**
* @file
* Contains \Drupal\locale\Tests\LocaleLookupTest.
*/
namespace Drupal\locale\Tests;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\locale\LocaleLookup;
use Drupal\Tests\UnitTestCase;
/**
* Tests locale translation language fallback.
*
* @group Drupal
* @group Locale
*
* @coversDefaultClass \Drupal\locale\LocaleLookup
*/
class LocaleLookupTest extends UnitTestCase {
/**
* A mocked storage to use when instantiating LocaleTranslation objects.
*
* @var \Drupal\locale\StringStorageInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $storage;
/**
* A mocked cache object.
*
* @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $cache;
/**
* A mocked lock object.
*
* @var \Drupal\Core\Lock\LockBackendInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $lock;
/**
* A mocked user object built from AccountInterface.
*
* @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $user;
/**
* A mocked config factory built with UnitTestCase::getConfigFactoryStub().
*
* @var \Drupal\Core\Config\ConfigFactory|\PHPUnit_Framework_MockObject_MockBuilder
*/
protected $configFactory;
/**
* A mocked language manager built from LanguageManagerInterface.
*
* @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $languageManager;
/**
* {@inheritdoc}
*/
public static function getInfo() {
return array(
'name' => 'Locale language fallback tests',
'description' => 'Test locale module language fallback implementation.',
'group' => 'locale',
);
}
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->storage = $this->getMock('Drupal\locale\StringStorageInterface');
$this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
$this->lock = $this->getMock('Drupal\Core\Lock\LockBackendInterface');
$this->lock->expects($this->never())
->method($this->anything());
$this->user = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->user->expects($this->any())
->method('getRoles')
->will($this->returnValue(array('anonymous')));
$this->configFactory = $this->getConfigFactoryStub(array('locale.settings' => array('cache_strings' => FALSE)));
$this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
$container = new ContainerBuilder();
$container->set('current_user', $this->user);
\Drupal::setContainer($container);
}
/**
* Tests locale lookups without fallback.
*
* @covers ::resolveCacheMiss()
*/
public function testResolveCacheMissWithoutFallback() {
$args = array(
'language' => 'en',
'source' => 'test',
'context' => 'irrelevant',
);
$result = (object) array(
'translation' => 'test',
);
$this->cache->expects($this->once())
->method('get')
->with('locale:en:irrelevant:0', FALSE);
$this->storage->expects($this->once())
->method('findTranslation')
->with($this->equalTo($args))
->will($this->returnValue($result));
$locale_lookup = $this->getMockBuilder('Drupal\locale\LocaleLookup')
->setConstructorArgs(array('en', 'irrelevant', $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager))
->setMethods(array('persist', 'requestUri'))
->getMock();
$locale_lookup->expects($this->never())
->method('persist');
$this->assertSame('test', $locale_lookup->get('test'));
}
/**
* Tests locale lookups with fallback.
*
* Note that context is irrelevant here. It is not used but it is required.
*
* @covers ::resolveCacheMiss()
*
* @dataProvider resolveCacheMissWithFallbackProvider
*/
public function testResolveCacheMissWithFallback($langcode, $string, $context, $expected) {
// These are fake words!
$translations = array(
'en' => array(
'test' => 'test',
'fake' => 'fake',
'missing pl' => 'missing pl',
'missing cs' => 'missing cs',
'missing both' => 'missing both',
),
'pl' => array(
'test' => 'test po polsku',
'fake' => 'ściema',
'missing cs' => 'zaginiony czech',
),
'cs' => array(
'test' => 'test v české',
'fake' => 'falešný',
'missing pl' => 'chybějící pl',
),
);
$this->storage->expects($this->any())
->method('findTranslation')
->will($this->returnCallback(function ($argument) use ($translations) {
if (isset($translations[$argument['language']][$argument['source']])) {
return (object) array('translation' => $translations[$argument['language']][$argument['source']]);
}
return TRUE;
}));
$this->languageManager->expects($this->any())
->method('getFallbackCandidates')
->will($this->returnCallback(function ($langcode) {
switch ($langcode) {
case 'pl':
return array('cs', 'en');
case 'cs':
return array('en');
default:
return array();
}
}));
$this->cache->expects($this->once())
->method('get')
->with('locale:' . $langcode . ':' . $context . ':0', FALSE);
$locale_lookup = new LocaleLookup($langcode, $context, $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager);
$this->assertSame($expected, $locale_lookup->get($string));
}
/**
* Provides test data for testResolveCacheMissWithFallback().
*/
public function resolveCacheMissWithFallbackProvider() {
return array(
array('cs', 'test', 'irrelevant', 'test v české'),
array('cs', 'fake', 'irrelevant', 'falešný'),
array('cs', 'missing pl', 'irrelevant', 'chybějící pl'),
array('cs', 'missing cs', 'irrelevant', 'missing cs'),
array('cs', 'missing both', 'irrelevant', 'missing both'),
// Testing PL with fallback to cs, en.
array('pl', 'test', 'irrelevant', 'test po polsku'),
array('pl', 'fake', 'irrelevant', 'ściema'),
array('pl', 'missing pl', 'irrelevant', 'chybějící pl'),
array('pl', 'missing cs', 'irrelevant', 'zaginiony czech'),
array('pl', 'missing both', 'irrelevant', 'missing both'),
);
}
/**
* Tests locale lookups with persistent tracking.
*
* @covers ::resolveCacheMiss()
*/
public function testResolveCacheMissWithPersist() {
$args = array(
'language' => 'en',
'source' => 'test',
'context' => 'irrelevant',
);
$result = (object) array(
'translation' => 'test',
);
$this->storage->expects($this->once())
->method('findTranslation')
->with($this->equalTo($args))
->will($this->returnValue($result));
$this->configFactory = $this->getConfigFactoryStub(array('locale.settings' => array('cache_strings' => TRUE)));
$locale_lookup = $this->getMockBuilder('Drupal\locale\LocaleLookup')
->setConstructorArgs(array('en', 'irrelevant', $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager))
->setMethods(array('persist', 'requestUri'))
->getMock();
$locale_lookup->expects($this->once())
->method('persist');
$this->assertSame('test', $locale_lookup->get('test'));
}
/**
* Tests locale lookups without a found translation.
*
* @covers ::resolveCacheMiss()
*/
public function testResolveCacheMissNoTranslation() {
$string = $this->getMock('Drupal\locale\StringInterface');
$string->expects($this->once())
->method('addLocation')
->will($this->returnSelf());
$this->storage->expects($this->once())
->method('findTranslation')
->will($this->returnValue(NULL));
$this->storage->expects($this->once())
->method('createString')
->will($this->returnValue($string));
$locale_lookup = $this->getMockBuilder('Drupal\locale\LocaleLookup')
->setConstructorArgs(array('en', 'irrelevant', $this->storage, $this->cache, $this->lock, $this->configFactory, $this->languageManager))
->setMethods(array('persist', 'requestUri'))
->getMock();
$locale_lookup->expects($this->never())
->method('persist');
$this->assertTrue($locale_lookup->get('test'));
}
}
......@@ -25,6 +25,13 @@ class LocaleTranslationTest extends UnitTestCase {
*/
protected $storage;
/**
* A mocked language manager built from LanguageManagerInterface.
*
* @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $languageManager;
public static function getInfo() {
return array(
'name' => 'Locale translation tests',
......@@ -40,13 +47,14 @@ protected function setUp() {
$this->storage = $this->getMock('Drupal\locale\StringStorageInterface');
$this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
$this->lock = $this->getMock('Drupal\Core\Lock\LockBackendInterface');
$this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
}
/**
* Tests for \Drupal\locale\LocaleTranslation::destruct()
*/
public function testDestruct() {
$translation = new LocaleTranslation($this->storage, $this->cache, $this->lock, $this->getConfigFactoryStub());
$translation = new LocaleTranslation($this->storage, $this->cache, $this->lock, $this->getConfigFactoryStub(), $this->languageManager);
// Prove that destruction works without errors when translations are empty.
$this->assertAttributeEmpty('translations', $translation);
$translation->destruct();
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment