From 9ed0c3a934d0e28efb0bbf41f55757916c98543e Mon Sep 17 00:00:00 2001
From: Ere Maijala <ere.maijala@helsinki.fi>
Date: Tue, 8 Sep 2015 10:17:09 +0300
Subject: [PATCH] Fix interaction between text domains and TranslatableStrings.
 - Make translate() optionally accept an array of domain and string - Fix
 hierarchical facet display text handling to translate the result when
 required. - Added test coverage

---
 .../I18n/Translator/TranslatorAwareTrait.php  |  58 +++++----
 .../VuFind/src/VuFind/Search/Solr/Params.php  |   8 ++
 .../View/Helper/Root/TranslateTest.php        | 113 +++++++++++++++++-
 3 files changed, 155 insertions(+), 24 deletions(-)

diff --git a/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareTrait.php b/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareTrait.php
index 723560f53d2..e57a1260ad0 100644
--- a/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareTrait.php
+++ b/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareTrait.php
@@ -89,28 +89,35 @@ trait TranslatorAwareTrait
     /**
      * Translate a string (or string-castable object)
      *
-     * @param string|object $str     String to translate
-     * @param array         $tokens  Tokens to inject into the translated string
-     * @param string        $default Default value to use if no translation is found
-     * (null for no default).
+     * @param string|object|array $target  String to translate or an array of text
+     * domain and string to translate
+     * @param array               $tokens  Tokens to inject into the translated
+     * string
+     * @param string              $default Default value to use if no translation is
+     * found (null for no default).
      *
      * @return string
      */
-    public function translate($str, $tokens = [], $default = null)
+    public function translate($target, $tokens = [], $default = null)
     {
+        // Figure out the text domain for the string:
+        list($domain, $str) = $this->extractTextDomain($target);
+
         // Special case: deal with objects with a designated display value:
         if ($str instanceof \VuFind\I18n\TranslatableStringInterface) {
-            $translated = $this->translateString((string)$str, $tokens, $default);
+            // On this pass, don't use the $default, since we want to fail over
+            // to getDisplayString before giving up:
+            $translated = $this
+                ->translateString((string)$str, $tokens, null, $domain);
             if ($translated !== (string)$str) {
                 return $translated;
             }
-            return $this->translateString(
-                $str->getDisplayString(), $tokens, $default
-            );
+            // Override $domain/$str using getDisplayString() before proceeding:
+            list($domain, $str) = $this->extractTextDomain($str->getDisplayString());
         }
 
         // Default case: deal with ordinary strings (or string-castable objects):
-        return $this->translateString((string)$str, $tokens, $default);
+        return $this->translateString((string)$str, $tokens, $default, $domain);
     }
 
     /**
@@ -118,15 +125,15 @@ trait TranslatorAwareTrait
      *
      * @param string $str     String to translate
      * @param array  $tokens  Tokens to inject into the translated string
-     * @param string $default Default value to use if no translation is found (null
-     * for no default).
+     * @param string $default Default value to use if no translation is found
+     * (null for no default).
+     * @param string $domain  Text domain (omit for default)
      *
      * @return string
      */
-    protected function translateString($str, $tokens = [], $default = null)
-    {
-        // Figure out the text domain for the string:
-        list($domain, $str) = $this->extractTextDomain($str);
+    protected function translateString($str, $tokens = [], $default = null,
+        $domain = 'default'
+    ) {
 
         $msg = (null === $this->translator)
             ? $str : $this->translator->translate($str, $domain);
@@ -153,16 +160,27 @@ trait TranslatorAwareTrait
      * Given a translation string with or without a text domain, return an
      * array with the raw string and the text domain separated.
      *
-     * @param string $str String to parse
+     * @param string|object|array $target String to translate or an array of text
+     * domain and string to translate
      *
      * @return array
      */
-    protected function extractTextDomain($str)
+    protected function extractTextDomain($target)
     {
-        $parts = explode('::', $str);
+        $parts = is_array($target) ? $target : explode('::', $target);
+        if (count($parts) < 1 || count($parts) > 2) {
+            throw new \Exception('Unexpected value sent to translator!');
+        }
         if (count($parts) == 2) {
+            if (empty($parts[0])) {
+                $parts[0] = 'default';
+            }
+            if ($target instanceof \VuFind\I18n\TranslatableStringInterface) {
+                $class = get_class($target);
+                $parts[1] = new $class($parts[1], $target->getDisplayString());
+            }
             return $parts;
         }
-        return ['default', $str];
+        return ['default', is_array($target) ? $parts[0] : $target];
     }
 }
diff --git a/module/VuFind/src/VuFind/Search/Solr/Params.php b/module/VuFind/src/VuFind/Search/Solr/Params.php
index 1b27627baec..e76bc991e39 100644
--- a/module/VuFind/src/VuFind/Search/Solr/Params.php
+++ b/module/VuFind/src/VuFind/Search/Solr/Params.php
@@ -619,6 +619,14 @@ class Params extends \VuFind\Search\Base\Params
             $filter['displayText'] = $facetHelper->formatDisplayText(
                 $filter['displayText'], true, $separator
             );
+            if ($translate) {
+                $domain = $this->getOptions()->getTextDomainForTranslatedFacet(
+                    $field
+                );
+                $filter['displayText'] = $this->translate(
+                    [$domain, $filter['displayText']]
+                );
+            }
         }
 
         return $filter;
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/TranslateTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/TranslateTest.php
index 0c4347b9f79..9651f09bc22 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/TranslateTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/TranslateTest.php
@@ -27,6 +27,7 @@
  */
 namespace VuFindTest\View\Helper\Root;
 use VuFind\View\Helper\Root\Translate;
+use VuFind\I18n\TranslatableString;
 
 /**
  * Translate view helper Test Class (and by extension, the TranslatorAwareTrait)
@@ -61,10 +62,9 @@ class TranslateTest extends \PHPUnit_Framework_TestCase
     public function testTranslateWithTranslator()
     {
         $translate = new Translate();
-        $translator = $this->getMock('Zend\I18n\Translator\TranslatorInterface');
-        $translator->expects($this->once())->method('translate')
-            ->with($this->equalTo('foo'))->will($this->returnValue('%%token%%'));
-        $translate->setTranslator($translator);
+        $translate->setTranslator(
+            $this->getMockTranslator(['default' => ['foo' => '%%token%%']])
+        );
 
         // Simple case that tests default values and tokens in a single pass:
         $this->assertEquals('baz', $translate->__invoke(
@@ -72,6 +72,92 @@ class TranslateTest extends \PHPUnit_Framework_TestCase
         );
     }
 
+    /**
+     * Test translation of a TranslatableString object with a loaded translator
+     *
+     * @return void
+     */
+    public function testTranslateTranslatableStringWithTranslator()
+    {
+        $translate = new Translate();
+        $translate->setTranslator(
+            $this->getMockTranslator(['default' => ['foo' => '%%token%%']])
+        );
+
+        // Test a TranslatableString with a translation.
+        $str1 = new TranslatableString('foo', 'bar');
+        // Simple case that tests default values and tokens in a single pass:
+        $this->assertEquals('baz', $translate->__invoke(
+            $str1, ['%%token%%' => 'baz'], 'failure')
+        );
+
+        // Test a TranslatableString with a fallback.
+        $str2 = new TranslatableString('bar', 'foo');
+        // Simple case that tests default values and tokens in a single pass:
+        $this->assertEquals('baz', $translate->__invoke(
+            $str2, ['%%token%%' => 'baz'], 'failure')
+        );
+
+        // Test a TranslatableString with no fallback.
+        $str3 = new TranslatableString('xyzzy', 'bar');
+        // Simple case that tests default values and tokens in a single pass:
+        $this->assertEquals('failure', $translate->__invoke(
+            $str3, ['%%token%%' => 'baz'], 'failure')
+        );
+    }
+
+    /**
+     * Test translation of a TranslatableString object using text domains with a
+     * loaded translator
+     *
+     * @return void
+     */
+    public function testTranslateTranslatableStringAndTextDomainsWithTranslator()
+    {
+        $translate = new Translate();
+        $translate->setTranslator(
+            $this->getMockTranslator(
+                [
+                    'd1' => ['f1' => 'str1'],
+                    'd2' => ['f2' => 'str2'],
+                ]
+            )
+        );
+
+        // Primary string translatable
+        $str1 = new TranslatableString('d1::f1', 'd2::f2');
+        $this->assertEquals('str1', $translate->__invoke($str1));
+        // Secondary string translatable
+        $str2 = new TranslatableString('d1::f2', 'd2::f2');
+        $this->assertEquals('str2', $translate->__invoke($str2));
+        // No string translatable
+        $str3 = new TranslatableString('d1::f2', 'd2::f1');
+        $this->assertEquals('failure', $translate->__invoke($str3, [], 'failure'));
+    }
+
+    /**
+     * Test translation with a loaded translator and a text domain
+     *
+     * @return void
+     */
+    public function testTranslateTextDomainWithTranslator()
+    {
+        $translate = new Translate();
+        $translate->setTranslator(
+            $this->getMockTranslator(['zap' => ['foo' => '%%token%%']])
+        );
+
+        // This one will work -- TextDomain defined above
+        $this->assertEquals('baz', $translate->__invoke(
+            'zap::foo', ['%%token%%' => 'baz'], 'failure')
+        );
+
+        // This one will use incoming string -- TextDomain undefined
+        $this->assertEquals('failure', $translate->__invoke(
+            'undefined::foo', ['%%token%%' => 'baz'], 'failure')
+        );
+    }
+
     /**
      * Test locale retrieval without a loaded translator
      *
@@ -110,4 +196,23 @@ class TranslateTest extends \PHPUnit_Framework_TestCase
         $translate->setTranslator($translator);
         $this->assertEquals($translator, $translate->getTranslator());
     }
+
+    /**
+     * Get mock translator.
+     *
+     * @param array $translations Key => value translation map.
+     *
+     * @return \Zend\I18n\Translator\TranslatorInterface
+     */
+    protected function getMockTranslator($translations)
+    {
+        $callback = function ($str, $domain) use ($translations) {
+            return isset($translations[$domain][$str])
+                ? $translations[$domain][$str] : $str;
+        };
+        $translator = $this->getMock('Zend\I18n\Translator\TranslatorInterface');
+        $translator->expects($this->any())->method('translate')
+            ->will($this->returnCallback($callback));
+        return $translator;
+    }
 }
\ No newline at end of file
-- 
GitLab