From 75a2e7cf0aca8ad646d752223fa8dc02a8ab95c0 Mon Sep 17 00:00:00 2001
From: Ere Maijala <ere.maijala@helsinki.fi>
Date: Thu, 2 Jul 2020 11:17:26 -0400
Subject: [PATCH] Expand capabilities of TranslatableStringInterface - Support
 non-translatable content - Support nested objects

---
 .../src/VuFind/I18n/TranslatableString.php    | 19 +++-
 .../I18n/TranslatableStringInterface.php      |  7 ++
 .../I18n/Translator/TranslatorAwareTrait.php  | 19 +++-
 .../I18n/TranslatableStringTest.php           | 70 +++++++++++++
 .../View/Helper/Root/TranslateTest.php        | 97 ++++++++++++++++++-
 5 files changed, 208 insertions(+), 4 deletions(-)
 create mode 100644 module/VuFind/tests/unit-tests/src/VuFindTest/I18n/TranslatableStringTest.php

diff --git a/module/VuFind/src/VuFind/I18n/TranslatableString.php b/module/VuFind/src/VuFind/I18n/TranslatableString.php
index 1c6ceca53ec..0bc1943a0d4 100644
--- a/module/VuFind/src/VuFind/I18n/TranslatableString.php
+++ b/module/VuFind/src/VuFind/I18n/TranslatableString.php
@@ -52,16 +52,23 @@ class TranslatableString implements TranslatableStringInterface
      */
     protected $displayString;
 
+    /**
+     * Whether translation is allowed
+     */
+    protected $translatable;
+
     /**
      * Constructor
      *
      * @param string $string        Original string
      * @param string $displayString Translatable display string
+     * @param bool   $translatable  Whether translation is allowed
      */
-    public function __construct($string, $displayString)
+    public function __construct($string, $displayString, $translatable = true)
     {
         $this->string = (string)$string;
         $this->displayString = $displayString;
+        $this->translatable = $translatable;
     }
 
     /**
@@ -84,4 +91,14 @@ class TranslatableString implements TranslatableStringInterface
     {
         return $this->displayString;
     }
+
+    /**
+     * Checks if the string can be translated
+     *
+     * @return bool
+     */
+    public function isTranslatable()
+    {
+        return $this->translatable;
+    }
 }
diff --git a/module/VuFind/src/VuFind/I18n/TranslatableStringInterface.php b/module/VuFind/src/VuFind/I18n/TranslatableStringInterface.php
index 53d60ab905b..d258cc904df 100644
--- a/module/VuFind/src/VuFind/I18n/TranslatableStringInterface.php
+++ b/module/VuFind/src/VuFind/I18n/TranslatableStringInterface.php
@@ -45,4 +45,11 @@ interface TranslatableStringInterface
      * @return string
      */
     public function getDisplayString();
+
+    /**
+     * Checks if the string can be translated
+     *
+     * @return bool
+     */
+    public function isTranslatable();
 }
diff --git a/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareTrait.php b/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareTrait.php
index 87bb415a778..57b9145c813 100644
--- a/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareTrait.php
+++ b/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareTrait.php
@@ -106,6 +106,9 @@ trait TranslatorAwareTrait
 
         // Special case: deal with objects with a designated display value:
         if ($str instanceof \VuFind\I18n\TranslatableStringInterface) {
+            if (!$str->isTranslatable()) {
+                return $str->getDisplayString();
+            }
             // On this pass, don't use the $default, since we want to fail over
             // to getDisplayString before giving up:
             $translated = $this
@@ -114,7 +117,17 @@ trait TranslatorAwareTrait
                 return $translated;
             }
             // Override $domain/$str using getDisplayString() before proceeding:
-            list($domain, $str) = $this->extractTextDomain($str->getDisplayString());
+            $str = $str->getDisplayString();
+            // Also the display string can be a TranslatableString. This makes it
+            // possible have multiple levels of translatable values while still
+            // providing a sane default string if translation is not found. Used at
+            // least with hierarchical facets where translation key can be the exact
+            // facet value (e.g. "0/Book/") or a displayable value (e.g. "Book").
+            if ($str instanceof \VuFind\I18n\TranslatableStringInterface) {
+                return $this->translate($str, $tokens, $default);
+            } else {
+                list($domain, $str) = $this->extractTextDomain($str);
+            }
         }
 
         // Default case: deal with ordinary strings (or string-castable objects):
@@ -204,7 +217,9 @@ trait TranslatorAwareTrait
             }
             if ($target instanceof \VuFind\I18n\TranslatableStringInterface) {
                 $class = get_class($target);
-                $parts[1] = new $class($parts[1], $target->getDisplayString());
+                $parts[1] = new $class(
+                    $parts[1], $target->getDisplayString(), $target->isTranslatable()
+                );
             }
             return $parts;
         }
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/TranslatableStringTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/TranslatableStringTest.php
new file mode 100644
index 00000000000..1c0e1d01c1b
--- /dev/null
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/TranslatableStringTest.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * TranslatableString Test Class
+ *
+ * Note that most tests using TranslatableString are in
+ * VuFindTest\View\Helper\Root\TranslateTest
+ *
+ * PHP version 7
+ *
+ * Copyright (C) The National Library of Finland 2020.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ */
+namespace VuFindTest\I18n\Translator\Loader;
+
+use VuFind\I18n\TranslatableString;
+
+/**
+ * TranslatableString Test Class
+ *
+ * Note that most tests using TranslatableString are in
+ * VuFindTest\View\Helper\Root\TranslateTest
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ */
+class TranslatableStringTest extends \VuFindTest\Unit\TestCase
+{
+    /**
+     * Test standalone behavior.
+     *
+     * @return void
+     */
+    public function testWithoutTranslate()
+    {
+        $s = new TranslatableString('foo', 'bar');
+        $this->assertEquals('foo', (string)$s);
+        $this->assertEquals('bar', $s->getDisplayString());
+        $this->assertTrue($s->isTranslatable());
+
+        $s = new TranslatableString('foo', new TranslatableString('bar', 'baz'));
+        $this->assertEquals('foo', (string)$s);
+        $this->assertEquals('bar', (string)$s->getDisplayString());
+        $this->assertEquals('baz', $s->getDisplayString()->getDisplayString());
+        $this->assertTrue($s->isTranslatable());
+
+        $s = new TranslatableString('foo', 'bar', false);
+        $this->assertFalse($s->isTranslatable());
+    }
+}
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 3a42c737dca..aa689c9cc6d 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
@@ -127,6 +127,25 @@ class TranslateTest extends \PHPUnit\Framework\TestCase
         );
     }
 
+    /**
+     * Test TranslatableString default values.
+     *
+     * @return void
+     */
+    public function testTranslateTranslatableStringDefaultValues()
+    {
+        $translate = new Translate();
+        $translate->setTranslator(
+            $this->getMockTranslator(['default' => []])
+        );
+
+        $s = new TranslatableString('foo', 'bar');
+        $this->assertEquals('bar', $translate->__invoke($s));
+
+        $s = new TranslatableString('foo', new TranslatableString('bar', 'baz'));
+        $this->assertEquals('baz', $translate->__invoke($s));
+    }
+
     /**
      * Test translation of a TranslatableString object with a loaded translator
      *
@@ -136,7 +155,12 @@ class TranslateTest extends \PHPUnit\Framework\TestCase
     {
         $translate = new Translate();
         $translate->setTranslator(
-            $this->getMockTranslator(['default' => ['foo' => '%%token%%']])
+            $this->getMockTranslator(
+                [
+                    'default' => ['foo' => '%%token%%'],
+                    'other' => ['foo' => 'Foo', 'bar' => 'Bar']
+                ]
+            )
         );
 
         // Test a TranslatableString with a translation.
@@ -165,6 +189,22 @@ class TranslateTest extends \PHPUnit\Framework\TestCase
                 $str3, ['%%token%%' => 'baz'], 'failure'
             )
         );
+
+        // Test a TranslatableString with another TranslatableString as a fallback.
+        $str4 = new TranslatableString(
+            'xyzzy', new TranslatableString('bar', 'baz')
+        );
+        $this->assertEquals('baz', $translate->__invoke($str4));
+        $str5 = new TranslatableString(
+            'xyzzy', new TranslatableString('foo', 'baz')
+        );
+        $this->assertEquals('%%token%%', $translate->__invoke($str5));
+
+        // Test a TranslatableString with translation forbidden
+        $str6 = new TranslatableString('foo', 'bar', false);
+        $this->assertEquals('bar', $translate->__invoke($str6));
+        $str7 = new TranslatableString('foo', '', false);
+        $this->assertEquals('', $translate->__invoke($str7));
     }
 
     /**
@@ -194,6 +234,40 @@ class TranslateTest extends \PHPUnit\Framework\TestCase
         // No string translatable
         $str3 = new TranslatableString('d1::f2', 'd2::f1');
         $this->assertEquals('failure', $translate->__invoke($str3, [], 'failure'));
+
+        // Secondary string a translatable TranslatableString
+        $str4 = new TranslatableString(
+            'd1::f2', new TranslatableString('d2::f2', 'd3::f3')
+        );
+        $this->assertEquals('str2', $translate->__invoke($str4));
+        // Secondary string a TranslatableString with no translation
+        $str5 = new TranslatableString(
+            'd1::f2', new TranslatableString('d2::f1', 'failure')
+        );
+        $this->assertEquals('failure', $translate->__invoke($str5));
+        // Secondary string a non-translatable TranslatableString
+        $str6 = new TranslatableString(
+            'd1::f2', new TranslatableString('d2::f2', 'failure', false)
+        );
+        $this->assertEquals('failure', $translate->__invoke($str6));
+
+        // Three levels of TranslatableString with the last one translatable
+        $str7 = new TranslatableString(
+            'd1::f2',
+            new TranslatableString(
+                'd3::f3', new TranslatableString('d2::f2', 'failure')
+            )
+        );
+        $this->assertEquals('str2', $translate->__invoke($str7));
+
+        // Three levels of TranslatableString with no translation
+        $str8 = new TranslatableString(
+            'd1::f2',
+            new TranslatableString(
+                'd3::f3', new TranslatableString('d3::f2', 'failure')
+            )
+        );
+        $this->assertEquals('failure', $translate->__invoke($str8));
     }
 
     /**
@@ -223,6 +297,27 @@ class TranslateTest extends \PHPUnit\Framework\TestCase
         );
     }
 
+    /**
+     * Test nested translation with potential text domain conflict
+     *
+     * @return void
+     */
+    public function testTranslateNestedTextDomainWithConflict()
+    {
+        $translations = [
+            'd1' => ['foo' => 'bar', 'failure' => 'success'],
+            'd2' => ['baz' => 'xyzzy', 'failure' => 'mediocrity'],
+        ];
+        $translate = new Translate();
+        $translate->setTranslator(
+            $this->getMockTranslator($translations)
+        );
+        $str = new TranslatableString(
+            'd1::baz', new TranslatableString('d2::foo', 'failure')
+        );
+        $this->assertEquals('failure', $translate->__invoke($str));
+    }
+
     /**
      * Test locale retrieval without a loaded translator
      *
-- 
GitLab