[TASK] EXT:form - simplify translation file includes 08/52008/25
authorRalf Zimmermann <ralf.zimmermann@tritum.de>
Sat, 11 Mar 2017 22:11:42 +0000 (23:11 +0100)
committerFrank Nägler <frank.naegler@typo3.org>
Fri, 24 Mar 2017 14:35:09 +0000 (15:35 +0100)
With this patch, an integrator has prototype wide translation settings
for the 4 aspects of the form framework. Furthermore, the integrator is
able to define multiple translation files to avoid copying the whole
default translation files or using locallangXMLOverride.

Resolves: #80241
Releases: master
Change-Id: I96ff6afec42159fbbd9c9fcd9d4540e12e1221cd
Reviewed-on: https://review.typo3.org/52008
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Mona Muzaffar <mona.muzaffar@gmx.de>
Tested-by: Mona Muzaffar <mona.muzaffar@gmx.de>
Reviewed-by: Bjoern Jacob <bjoern.jacob@tritum.de>
Tested-by: Bjoern Jacob <bjoern.jacob@tritum.de>
Reviewed-by: Frank Nägler <frank.naegler@typo3.org>
Tested-by: Frank Nägler <frank.naegler@typo3.org>
14 files changed:
typo3/sysext/core/Documentation/Changelog/master/Important-80241-ExtFormSimplifyTranslationHandling.rst [new file with mode: 0644]
typo3/sysext/form/Classes/Controller/FormEditorController.php
typo3/sysext/form/Classes/Controller/FormManagerController.php
typo3/sysext/form/Classes/Domain/Finishers/AbstractFinisher.php
typo3/sysext/form/Classes/Hooks/DataStructureIdentifierHook.php
typo3/sysext/form/Classes/Mvc/Configuration/InheritancesResolverService.php
typo3/sysext/form/Classes/Service/TranslationService.php
typo3/sysext/form/Classes/ViewHelpers/TranslateElementErrorViewHelper.php [new file with mode: 0644]
typo3/sysext/form/Configuration/Yaml/BaseSetup.yaml
typo3/sysext/form/Configuration/Yaml/FormEngineSetup.yaml
typo3/sysext/form/Resources/Private/Frontend/Partials/Field/Field.html
typo3/sysext/form/Tests/Unit/Domain/Finishers/AbstractFinisherTest.php
typo3/sysext/form/Tests/Unit/Service/Fixtures/locallang_additional_text.xlf [new file with mode: 0644]
typo3/sysext/form/Tests/Unit/Service/TranslationServiceTest.php

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Important-80241-ExtFormSimplifyTranslationHandling.rst b/typo3/sysext/core/Documentation/Changelog/master/Important-80241-ExtFormSimplifyTranslationHandling.rst
new file mode 100644 (file)
index 0000000..d647f77
--- /dev/null
@@ -0,0 +1,138 @@
+.. include:: ../../Includes.txt
+
+=================================================================
+Task: #80241 - EXT:form simplify translation handling
+=================================================================
+
+See :issue:`80241`
+
+Description
+===========
+
+If an integrator wants to add translations for new form elements he can only
+define a new translation file which must contain all translation keys.
+This patch makes it possible to define multiple translation files.
+
+Before this patch:
+
+.. code-block:: typoscript
+
+    TYPO3:
+      CMS:
+        Form:
+          prototypes:
+            standard:
+              formElementsDefinition:
+                Form:
+                  renderingOptions:
+                    translation:
+                      translationFile: 'EXT:form/Resources/Private/Language/locallang.xlf'
+
+After this patch:
+
+.. code-block:: typoscript
+
+    TYPO3:
+      CMS:
+        Form:
+          prototypes:
+            standard:
+              formElementsDefinition:
+                Form:
+                  renderingOptions:
+                    translation:
+                      translationFile:
+                        10: 'EXT:form/Resources/Private/Language/locallang.xlf'
+                        20: 'EXT:my_ext/Resources/Private/Language/locallang.xlf'
+
+The translation keys will be searched within the referenced files.
+The search order is from the key with the highest number to the lowest.
+If a translation key is found within one of these files the search will stop.
+
+This makes it possible to only define new keys within the custom translations
+and use the default form translations as well.
+The default settings keep the translationFile property as string because
+of backward compatibility.
+
+Before this patch the "BaseFormElementMixin" inherits the "translationSettingsMixin".
+Thus, the "renderingOptions.translation..." are copied to each form element.
+This is inconvenient if an integrator defines his own prototype which inherits from
+the standard prototype because he must redefine the "renderingOptions.translation..."
+options for each form element.
+
+Since there already is a fallback strategy to the "renderingOptions.translation..."
+options from the root form element - if this option is not set within the
+child form elements - we can simply apply the "translationSettingsMixin"
+to the "Form" element and remove it from the "BaseFormElementMixin".
+Now, the rendering options are only set for the "Form" element and rules
+as a prototype wide frontend translation setting.
+
+This patch adds a fallback for the form engine translation if there is no
+"translationFile" setting within the "FormEngine" option.
+
+.. code-block:: typoscript
+
+    TYPO3:
+      CMS:
+        Form:
+          prototypes:
+            standard:
+              formEngine:
+                translationFile:
+                  10: 'EXT:form/Resources/Private/Language/Database.xlf'
+                  20: 'EXT:ext_form_example1484232130/Resources/Private/Language/Database.xlf'
+
+Now, there is one prototype wide form engine (plugin settings) translation setting.
+
+
+Summary
+-------
+
+With this patch, an integrator has prototype wide translation settings
+for the 4 aspects of the form framework. Furthermore, the integrator is
+able to define multiple translation files to avoid copying the whole
+default translation files or using locallangXMLOverride.
+
+.. code-block:: typoscript
+
+    TYPO3:
+      CMS:
+        Form:
+          formManager:
+            selectablePrototypesConfiguration:
+              1484232130:
+                translationFile:
+                  # translations for the form managers "new form" modal
+                  10: 'EXT:form/Resources/Private/Language/Database.xlf'
+                  20: 'EXT:my_ext/Resources/Private/Language/Database.xlf'
+
+          prototypes:
+            <prototypeName>:
+              formEditor:
+                translationFile:
+                  # translations for the form editor
+                  10: 'EXT:form/Resources/Private/Language/Database.xlf'
+                  20: 'EXT:my_ext/Resources/Private/Language/Database.xlf'
+
+              formEngine:
+                translationFile:
+                  # translations for the form plugin (finisher overrides)
+                  10: 'EXT:form/Resources/Private/Language/Database.xlf'
+                  20: 'EXT:my_ext/Resources/Private/Language/Database.xlf'
+
+              formElementsDefinition:
+                Form:
+                  renderingOptions:
+                    translation:
+                      translationFile:
+                        # translations for the frontend
+                        10: 'EXT:form/Resources/Private/Language/locallang.xlf'
+                        20: 'EXT:my_ext/Resources/Private/Language/locallang.xlf'
+
+
+Impact
+======
+
+Easier to use, less maintenance. 
+
+.. index:: Backend, Frontend, ext:form
\ No newline at end of file
index b991851..de2af3e 100644 (file)
@@ -208,16 +208,15 @@ class FormEditorController extends AbstractBackendController
                 $formElementsByGroup[$formElementConfiguration['group']] = [];
             }
 
+            $formElementConfiguration = TranslationService::getInstance()->translateValuesRecursive(
+                $formElementConfiguration,
+                $this->prototypeConfiguration['formEditor']['translationFile']
+            );
+
             $formElementsByGroup[$formElementConfiguration['group']][] = [
                 'key' => $formElementName,
                 'cssKey' => preg_replace('/[^a-z0-9]/', '-', strtolower($formElementName)),
-                'label' => TranslationService::getInstance()->translate(
-                    $formElementConfiguration['label'],
-                    null,
-                    $this->prototypeConfiguration['formEditor']['translationFile'],
-                    null,
-                    $formElementConfiguration['label']
-                ),
+                'label' => $formElementConfiguration['label'],
                 'sorting' => $formElementConfiguration['groupSorting'],
                 'iconIdentifier' => $formElementConfiguration['iconIdentifier'],
             ];
@@ -234,16 +233,15 @@ class FormEditorController extends AbstractBackendController
             });
             unset($formElementsByGroup[$groupName]['sorting']);
 
+            $groupConfiguration = TranslationService::getInstance()->translateValuesRecursive(
+                $groupConfiguration,
+                $this->prototypeConfiguration['formEditor']['translationFile']
+            );
+
             $formGroups[] = [
                 'key' => $groupName,
                 'elements' => $formElementsByGroup[$groupName],
-                'label' => TranslationService::getInstance()->translate(
-                    $groupConfiguration['label'],
-                    null,
-                    $this->prototypeConfiguration['formEditor']['translationFile'],
-                    null,
-                    $groupConfiguration['label']
-                ),
+                'label' => $groupConfiguration['label'],
             ];
         }
 
index 9c155b5..a6a5c53 100644 (file)
@@ -210,21 +210,14 @@ class FormManagerController extends AbstractBackendController
 
             $this->formPersistenceManager->delete($formPersistenceIdentifier);
         } else {
+            $controllerConfiguration = TranslationService::getInstance()->translateValuesRecursive(
+                $this->formSettings['formManager']['controller'],
+                $this->formSettings['formManager']['translationFile']
+            );
+
             $this->addFlashMessage(
-                TranslationService::getInstance()->translate(
-                    $this->formSettings['formManager']['controller']['deleteAction']['errorMessage'],
-                    [$formPersistenceIdentifier],
-                    $this->formSettings['formManager']['translationFile'],
-                    null,
-                    $this->formSettings['formManager']['controller']['deleteAction']['errorMessage']
-                ),
-                TranslationService::getInstance()->translate(
-                    $this->formSettings['formManager']['controller']['deleteAction']['errorTitle'],
-                    null,
-                    $this->formSettings['formManager']['translationFile'],
-                    null,
-                    $this->formSettings['formManager']['controller']['deleteAction']['errorTitle']
-                ),
+                sprintf($controllerConfiguration['deleteAction']['errorMessage'], $formPersistenceIdentifier),
+                $controllerConfiguration['deleteAction']['errorTitle'],
                 AbstractMessage::ERROR,
                 true
             );
index 808b35d..79b818c 100644 (file)
@@ -199,15 +199,17 @@ abstract class AbstractFinisher implements FinisherInterface
             return $value;
         }, $optionValue);
 
-        if (isset($this->options['translation']['translationFile'])) {
-            $optionValue = TranslationService::getInstance()->translateFinisherOption(
-                $formRuntime,
-                $this->finisherIdentifier,
-                $optionName,
-                $optionValue,
-                $this->options['translation']
-            );
-        }
+        $renderingOptions = is_array($this->options['translation'])
+                            ? $this->options['translation']
+                            : [];
+
+        $optionValue = TranslationService::getInstance()->translateFinisherOption(
+            $formRuntime,
+            $this->finisherIdentifier,
+            $optionName,
+            $optionValue,
+            $renderingOptions
+        );
 
         if (empty($optionValue)) {
             if ($defaultValue !== null) {
index 776e321..0079ff7 100644 (file)
@@ -156,7 +156,12 @@ class DataStructureIdentifierHook
                 ])
             );
 
-            $translationFile = $finishersDefinition[$finisherIdentifier]['FormEngine']['translationFile'];
+            if (isset($finishersDefinition[$finisherIdentifier]['FormEngine']['translationFile'])) {
+                $translationFile = $finishersDefinition[$finisherIdentifier]['FormEngine']['translationFile'];
+            } else {
+                $translationFile = $prototypeConfiguration['formEngine']['translationFile'];
+            }
+
             $finishersDefinition[$finisherIdentifier]['FormEngine'] = TranslationService::getInstance()->translateValuesRecursive(
                 $finishersDefinition[$finisherIdentifier]['FormEngine'],
                 $translationFile
index 7925c15..fe97228 100644 (file)
@@ -230,6 +230,13 @@ class InheritancesResolverService
                 $inheritedConfiguration = $inheritedConfiguration[$key];
             }
 
+            if ($inheritedConfiguration === null) {
+                throw new CycleInheritancesException(
+                    $inheritancePath . ' does not exist within the configuration',
+                    1489260796
+                );
+            }
+
             $inheritedConfigurations = $this->mergeRecursiveWithOverrule(
                 $inheritedConfigurations,
                 $inheritedConfiguration
index dfced35..e1da16a 100644 (file)
@@ -173,18 +173,35 @@ class TranslationService implements SingletonInterface
      * Recursively translate values.
      *
      * @param array $array
-     * @param string $translationFile
+     * @param array|string|null $translationFile
      * @return array the modified array
      * @internal
      */
-    public function translateValuesRecursive(array $array, string $translationFile = null): array
+    public function translateValuesRecursive(array $array, $translationFile = null): array
     {
         $result = $array;
         foreach ($result as $key => $value) {
             if (is_array($value)) {
                 $result[$key] = $this->translateValuesRecursive($value, $translationFile);
             } else {
-                $result[$key] = $this->translate($value, null, $translationFile, null, $value);
+                $translationFiles = null;
+                if (is_string($translationFile)) {
+                    $translationFiles = [$translationFile];
+                } elseif (is_array($translationFile)) {
+                    $translationFiles = $this->sortArrayWithIntegerKeysDescending($translationFile);
+                }
+
+                if ($translationFiles) {
+                    foreach ($translationFiles as $_translationFile) {
+                        $translatedValue = $this->translate($value, null, $_translationFile, null);
+                        if (!empty($translatedValue)) {
+                            $result[$key] = $translatedValue;
+                            break;
+                        }
+                    }
+                } else {
+                    $result[$key] = $this->translate($value, null, $translationFile, null, $value);
+                }
             }
         }
         return $result;
@@ -220,6 +237,12 @@ class TranslationService implements SingletonInterface
             $translationFile = $formRuntime->getRenderingOptions()['translation']['translationFile'];
         }
 
+        if (is_string($translationFile)) {
+            $translationFiles = [$translationFile];
+        } else {
+            $translationFiles = $this->sortArrayWithIntegerKeysDescending($translationFile);
+        }
+
         if (isset($renderingOptions['translatePropertyValueIfEmpty'])) {
             $translatePropertyValueIfEmpty = (bool)$renderingOptions['translatePropertyValueIfEmpty'];
         } else {
@@ -235,10 +258,12 @@ class TranslationService implements SingletonInterface
             $language = $renderingOptions['language'];
         }
 
-        $translationKeyChain = [
-            sprintf('%s:%s.finisher.%s.%s', $translationFile, $formRuntime->getIdentifier(), $finisherIdentifier, $optionKey),
-            sprintf('%s:finisher.%s.%s', $translationFile, $finisherIdentifier, $optionKey)
-        ];
+        $translationKeyChain = [];
+        foreach ($translationFiles as $translationFile) {
+            $translationKeyChain[] = sprintf('%s:%s.finisher.%s.%s', $translationFile, $formRuntime->getIdentifier(), $finisherIdentifier, $optionKey);
+            $translationKeyChain[] = sprintf('%s:finisher.%s.%s', $translationFile, $finisherIdentifier, $optionKey);
+        }
+
         $translatedValue = $this->processTranslationChain($translationKeyChain, $language);
         $translatedValue = (empty($translatedValue)) ? $optionValue : $translatedValue;
 
@@ -300,38 +325,50 @@ class TranslationService implements SingletonInterface
             $translationFile = $formRuntime->getRenderingOptions()['translation']['translationFile'];
         }
 
+        if (is_string($translationFile)) {
+            $translationFiles = [$translationFile];
+        } else {
+            $translationFiles = $this->sortArrayWithIntegerKeysDescending($translationFile);
+        }
+
         $language = null;
         if (isset($renderingOptions['translation']['language'])) {
             $language = $renderingOptions['translation']['language'];
         }
-        $translationKeyChain = [];
+
         if ($property === 'options' && is_array($defaultValue)) {
             foreach ($defaultValue as $optionValue => &$optionLabel) {
-                $translationKeyChain = [
-                    sprintf('%s:%s.element.%s.%s.%s.%s', $translationFile, $formRuntime->getIdentifier(), $element->getIdentifier(), $propertyType, $property, $optionValue),
-                    sprintf('%s:element.%s.%s.%s.%s', $translationFile, $element->getIdentifier(), $propertyType, $property, $optionValue)
-                ];
+                $translationKeyChain = [];
+                foreach ($translationFiles as $translationFile) {
+                    $translationKeyChain[] = sprintf('%s:%s.element.%s.%s.%s.%s', $translationFile, $formRuntime->getIdentifier(), $element->getIdentifier(), $propertyType, $property, $optionValue);
+                    $translationKeyChain[] = sprintf('%s:element.%s.%s.%s.%s', $translationFile, $element->getIdentifier(), $propertyType, $property, $optionValue);
+                }
+
                 $translatedValue = $this->processTranslationChain($translationKeyChain, $language);
                 $optionLabel = (empty($translatedValue)) ? $optionLabel : $translatedValue;
             }
             $translatedValue = $defaultValue;
         } elseif ($property === 'fluidAdditionalAttributes' && is_array($defaultValue)) {
             foreach ($defaultValue as $propertyName => &$propertyValue) {
-                $translationKeyChain = [
-                    sprintf('%s:%s.element.%s.%s.%s', $translationFile, $formRuntime->getIdentifier(), $element->getIdentifier(), $propertyType, $propertyName),
-                    sprintf('%s:element.%s.%s.%s', $translationFile, $element->getIdentifier(), $propertyType, $propertyName),
-                    sprintf('%s:element.%s.%s.%s', $translationFile, $element->getType(), $propertyType, $propertyName),
-                ];
+                $translationKeyChain = [];
+                foreach ($translationFiles as $translationFile) {
+                    $translationKeyChain[] = sprintf('%s:%s.element.%s.%s.%s', $translationFile, $formRuntime->getIdentifier(), $element->getIdentifier(), $propertyType, $propertyName);
+                    $translationKeyChain[] = sprintf('%s:element.%s.%s.%s', $translationFile, $element->getIdentifier(), $propertyType, $propertyName);
+                    $translationKeyChain[] = sprintf('%s:element.%s.%s.%s', $translationFile, $element->getType(), $propertyType, $propertyName);
+                }
+
                 $translatedValue = $this->processTranslationChain($translationKeyChain, $language);
                 $propertyValue = (empty($translatedValue)) ? $propertyValue : $translatedValue;
             }
             $translatedValue = $defaultValue;
         } else {
-            $translationKeyChain = [
-                sprintf('%s:%s.element.%s.%s.%s', $translationFile, $formRuntime->getIdentifier(), $element->getIdentifier(), $propertyType, $property),
-                sprintf('%s:element.%s.%s.%s', $translationFile, $element->getIdentifier(), $propertyType, $property),
-                sprintf('%s:element.%s.%s.%s', $translationFile, $element->getType(), $propertyType, $property),
-            ];
+            $translationKeyChain = [];
+            foreach ($translationFiles as $translationFile) {
+                $translationKeyChain[] = sprintf('%s:%s.element.%s.%s.%s', $translationFile, $formRuntime->getIdentifier(), $element->getIdentifier(), $propertyType, $property);
+                $translationKeyChain[] = sprintf('%s:element.%s.%s.%s', $translationFile, $element->getIdentifier(), $propertyType, $property);
+                $translationKeyChain[] = sprintf('%s:element.%s.%s.%s', $translationFile, $element->getType(), $propertyType, $property);
+            }
+
             $translatedValue = $this->processTranslationChain($translationKeyChain, $language);
             $translatedValue = (empty($translatedValue)) ? $defaultValue : $translatedValue;
         }
@@ -340,6 +377,63 @@ class TranslationService implements SingletonInterface
     }
 
     /**
+     * @param RootRenderableInterface $element
+     * @param int $code
+     * @param string $defaultValue
+     * @param array $arguments
+     * @param FormRuntime $formRuntime
+     * @return string
+     * @throws \InvalidArgumentException
+     * @internal
+     */
+    public function translateFormElementError(
+        RootRenderableInterface $element,
+        int $code,
+        array $arguments,
+        string $defaultValue = '',
+        FormRuntime $formRuntime
+    ): string {
+        if (empty($code)) {
+            throw new \InvalidArgumentException('The argument "code" is empty', 1489272978);
+        }
+
+        $validationErrors = $element->getProperties()['validationErrorMessages'];
+        foreach ($validationErrors as $validationError) {
+            if ((int)$validationError['code'] === $code) {
+                return sprintf($validationError['message'], $arguments);
+            }
+        }
+
+        $renderingOptions = $element->getRenderingOptions();
+        $translationFile = $renderingOptions['translation']['translationFile'];
+        if (empty($translationFile)) {
+            $translationFile = $formRuntime->getRenderingOptions()['translation']['translationFile'];
+        }
+
+        if (is_string($translationFile)) {
+            $translationFiles = [$translationFile];
+        } else {
+            $translationFiles = $this->sortArrayWithIntegerKeysDescending($translationFile);
+        }
+
+        $language = null;
+        if (isset($renderingOptions['language'])) {
+            $language = $renderingOptions['language'];
+        }
+
+        $translationKeyChain = [];
+        foreach ($translationFiles as $translationFile) {
+            $translationKeyChain[] = sprintf('%s:%s.validation.error.%s.%s', $translationFile, $formRuntime->getIdentifier(), $element->getIdentifier(), $code);
+            $translationKeyChain[] = sprintf('%s:%s.validation.error.%s', $translationFile, $formRuntime->getIdentifier(), $code);
+            $translationKeyChain[] = sprintf('%s:validation.error.%s', $translationFile, $code);
+        }
+
+        $translatedValue = $this->processTranslationChain($translationKeyChain, $language, $arguments);
+        $translatedValue = (empty($translatedValue)) ? $defaultValue : $translatedValue;
+        return $translatedValue;
+    }
+
+    /**
      * @param string $languageKey
      * @internal
      */
@@ -360,13 +454,17 @@ class TranslationService implements SingletonInterface
     /**
      * @param array $translationKeyChain
      * @param string $language
+     * @param array $arguments
      * @return string|null
      */
-    protected function processTranslationChain(array $translationKeyChain, string $language = null)
-    {
+    protected function processTranslationChain(
+        array $translationKeyChain,
+        string $language = null,
+        array $arguments = null
+    ) {
         $translatedValue = null;
         foreach ($translationKeyChain as $translationKey) {
-            $translatedValue = $this->translate($translationKey, null, null, $language);
+            $translatedValue = $this->translate($translationKey, $arguments, null, $language);
             if (!empty($translatedValue)) {
                 break;
             }
@@ -500,6 +598,20 @@ class TranslationService implements SingletonInterface
     }
 
     /**
+     * If the array contains numerical keys only, sort it in descending order
+     *
+     * @param array $array
+     * @return array
+     */
+    protected function sortArrayWithIntegerKeysDescending(array $array)
+    {
+        if (count(array_filter(array_keys($array), 'is_string')) === 0) {
+            krsort($array);
+        }
+        return $array;
+    }
+
+    /**
      * Returns instance of the configuration manager
      *
      * @return ConfigurationManagerInterface
diff --git a/typo3/sysext/form/Classes/ViewHelpers/TranslateElementErrorViewHelper.php b/typo3/sysext/form/Classes/ViewHelpers/TranslateElementErrorViewHelper.php
new file mode 100644 (file)
index 0000000..2c8a885
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Form\ViewHelpers;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper;
+use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
+use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
+use TYPO3\CMS\Form\Service\TranslationService;
+use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
+use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
+
+/**
+ * Translate form element properites.
+ *
+ * Scope: frontend / backend
+ * @api
+ */
+class TranslateElementErrorViewHelper extends AbstractViewHelper
+{
+    use CompileWithRenderStatic;
+
+    /**
+     * Initialize arguments.
+     *
+     * @return void
+     * @internal
+     */
+    public function initializeArguments()
+    {
+        parent::initializeArguments();
+        $this->registerArgument('element', RootRenderableInterface::class, 'Form Element to translate', true);
+        $this->registerArgument('code', 'integer', 'Error code', true);
+        $this->registerArgument('arguments', 'array', 'Error arguments', false, null);
+        $this->registerArgument('defaultValue', 'string', 'The default value', false, '');
+    }
+
+    /**
+     * Return array element by key.
+     *
+     * @param array $arguments
+     * @param \Closure $renderChildrenClosure
+     * @param RenderingContextInterface $renderingContext
+     * @return string
+     * @api
+     */
+    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
+    {
+        $element = $arguments['element'];
+
+        /** @var FormRuntime $formRuntime */
+        $formRuntime =  $renderingContext
+            ->getViewHelperVariableContainer()
+            ->get(RenderRenderableViewHelper::class, 'formRuntime');
+
+        return TranslationService::getInstance()->translateFormElementError(
+            $element,
+            $arguments['code'],
+            $arguments['arguments'],
+            $arguments['defaultValue'],
+            $formRuntime
+        );
+    }
+}
index c22d003..ecf401e 100644 (file)
@@ -21,6 +21,8 @@ TYPO3:
                 10: 'TYPO3.CMS.Form.mixins.formElementMixins.BaseFormElementMixin'
               rendererClassName: 'TYPO3\CMS\Form\Domain\Renderer\FluidFormRenderer'
               renderingOptions:
+                __inheritances:
+                  10: 'TYPO3.CMS.Form.mixins.translationSettingsMixin'
                 templateRootPaths:
                   10: 'EXT:form/Resources/Private/Frontend/Templates/'
                 partialRootPaths:
@@ -171,15 +173,11 @@ TYPO3:
 
           finishersDefinition:
             Closure:
-              __inheritances:
-                10: 'TYPO3.CMS.Form.mixins.finishersTranslationSettingsMixin'
               implementationClassName: 'TYPO3\CMS\Form\Domain\Finishers\ClosureFinisher'
               options:
                 #closure:
 
             Confirmation:
-              __inheritances:
-                10: 'TYPO3.CMS.Form.mixins.finishersTranslationSettingsMixin'
               implementationClassName: 'TYPO3\CMS\Form\Domain\Finishers\ConfirmationFinisher'
               #options:
                 #message: ''
@@ -196,8 +194,6 @@ TYPO3:
               implementationClassName: 'TYPO3\CMS\Form\Domain\Finishers\DeleteUploadsFinisher'
 
             FlashMessage:
-              __inheritances:
-                10: 'TYPO3.CMS.Form.mixins.finishersTranslationSettingsMixin'
               implementationClassName: 'TYPO3\CMS\Form\Domain\Finishers\FlashMessageFinisher'
               #options:
                 #messageBody: ''
@@ -271,20 +267,11 @@ TYPO3:
         translationSettingsMixin:
           translation:
             translationFile: 'EXT:form/Resources/Private/Language/locallang.xlf'
-            translatePropertyValueIfEmpty: true
-
-        finishersTranslationSettingsMixin:
-          options:
-            __inheritances:
-              10: 'TYPO3.CMS.Form.mixins.translationSettingsMixin'
+            #translatePropertyValueIfEmpty: true
 
         ########### FORM ELEMENT MIXINS ###########
         formElementMixins:
-          BaseFormElementMixin:
-            renderingOptions:
-              __inheritances:
-                10: 'TYPO3.CMS.Form.mixins.translationSettingsMixin'
-
+          BaseFormElementMixin: []
               # The form element type is chosen as the template name by default.
               # If you want another name you can set it with 'templateName'
               #templateName: 'CustomTemplateName'
@@ -327,8 +314,6 @@ TYPO3:
               saveToFileMount: '1:/user_upload/'
 
         finishersEmailMixin:
-          __inheritances:
-            10: 'TYPO3.CMS.Form.mixins.finishersTranslationSettingsMixin'
           implementationClassName: 'TYPO3\CMS\Form\Domain\Finishers\EmailFinisher'
           options:
             #subject: ''
index 817341f..6115642 100644 (file)
@@ -3,6 +3,9 @@ TYPO3:
     Form:
       prototypes:
         standard:
+          formEngine:
+            translationFile: 'EXT:form/Resources/Private/Language/Database.xlf'
+
           ########### TCE Forms CONFIGURATION ###########
 
           ### FINISHERS ###
@@ -52,8 +55,6 @@ TYPO3:
 
             Redirect:
               FormEngine:
-                __inheritances:
-                  10: 'TYPO3.CMS.Form.mixins.FormEngineTranslationSettingsMixin'
                 label: 'tt_content.finishersDefinition.Redirect.label'
                 elements:
                   pageUid:
@@ -75,12 +76,7 @@ TYPO3:
 
       ########### MIXINS ###########
       mixins:
-        FormEngineTranslationSettingsMixin:
-          translationFile: 'EXT:form/Resources/Private/Language/Database.xlf'
-
         FormEngineEmailMixin:
-          __inheritances:
-            10: 'TYPO3.CMS.Form.mixins.FormEngineTranslationSettingsMixin'
           label: 'tt_content.finishersDefinition.EmailToSender.label'
           elements:
             subject:
index 51646d1..1f6cbfd 100644 (file)
@@ -8,7 +8,7 @@
                                <f:if condition="{validationResults.flattenedErrors}">
                                        <span class="error help-block" role="alert">
                                                <f:for each="{validationResults.errors}" as="error">
-                                                       {error -> f:translate(key: 'LLL:{element.renderingOptions.translation.translationFile}:validation.error.{error.code}', arguments: error.arguments)}
+                                                       {formvh:translateElementError(element: element, code: error.code, arguments: error.arguments, defaultValue: error.message)}
                                                        <br />
                                                </f:for>
                                        </span>
index 6cc64d9..aa33f23 100644 (file)
@@ -15,15 +15,39 @@ namespace TYPO3\CMS\Form\Tests\Unit\Domain\Finishers;
  */
 
 use Prophecy\Argument;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Extbase\Object\ObjectManager;
 use TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher;
 use TYPO3\CMS\Form\Domain\Finishers\FinisherContext;
 use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
+use TYPO3\CMS\Form\Service\TranslationService;
 
 /**
  * Test case
  */
 class AbstractFinisherTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
 {
+    /**
+     * @var array A backup of registered singleton instances
+     */
+    protected $singletonInstances = [];
+
+    /**
+     * Set up
+     */
+    public function setUp()
+    {
+        $this->singletonInstances = GeneralUtility::getSingletonInstances();
+    }
+
+    /**
+     * Tear down
+     */
+    public function tearDown()
+    {
+        GeneralUtility::resetSingletonInstances($this->singletonInstances);
+        parent::tearDown();
+    }
 
     /**
      * @test
@@ -100,6 +124,22 @@ class AbstractFinisherTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCas
      */
     public function parseOptionReturnsValueFromFormRuntimeIfOptionNameReferenceAFormElementIdentifierWhoseValueIsAString()
     {
+        $objectMangerProphecy = $this->prophesize(ObjectManager::class);
+        GeneralUtility::setSingletonInstance(ObjectManager::class, $objectMangerProphecy->reveal());
+
+        $mockTranslationService = $this->getAccessibleMock(TranslationService::class, [
+            'translateFinisherOption'
+        ], [], '', false);
+
+        $mockTranslationService
+            ->expects($this->any())
+            ->method('translateFinisherOption')
+            ->willReturnArgument(3);
+
+        $objectMangerProphecy
+            ->get(TranslationService::class)
+            ->willReturn($mockTranslationService);
+
         $expected = 'element-value';
         $elementIdentifier = 'element-identifier-1';
 
@@ -133,6 +173,22 @@ class AbstractFinisherTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCas
      */
     public function parseOptionReturnsNoReplacedValueFromFormRuntimeIfOptionNameReferenceAFormElementIdentifierWhoseValueIsNotAString()
     {
+        $objectMangerProphecy = $this->prophesize(ObjectManager::class);
+        GeneralUtility::setSingletonInstance(ObjectManager::class, $objectMangerProphecy->reveal());
+
+        $mockTranslationService = $this->getAccessibleMock(TranslationService::class, [
+            'translateFinisherOption'
+        ], [], '', false);
+
+        $mockTranslationService
+            ->expects($this->any())
+            ->method('translateFinisherOption')
+            ->willReturnArgument(3);
+
+        $objectMangerProphecy
+            ->get(TranslationService::class)
+            ->willReturn($mockTranslationService);
+
         $elementIdentifier = 'element-identifier-1';
         $expected = '{' . $elementIdentifier . '}';
 
@@ -167,6 +223,22 @@ class AbstractFinisherTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCas
      */
     public function parseOptionReturnsNoReplacedValueFromFormRuntimeIfOptionNameReferenceANonExistingFormElement()
     {
+        $objectMangerProphecy = $this->prophesize(ObjectManager::class);
+        GeneralUtility::setSingletonInstance(ObjectManager::class, $objectMangerProphecy->reveal());
+
+        $mockTranslationService = $this->getAccessibleMock(TranslationService::class, [
+            'translateFinisherOption'
+        ], [], '', false);
+
+        $mockTranslationService
+            ->expects($this->any())
+            ->method('translateFinisherOption')
+            ->willReturnArgument(3);
+
+        $objectMangerProphecy
+            ->get(TranslationService::class)
+            ->willReturn($mockTranslationService);
+
         $elementIdentifier = 'element-identifier-1';
 
         $mockAbstractFinisher = $this->getAccessibleMockForAbstractClass(
@@ -200,6 +272,22 @@ class AbstractFinisherTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCas
      */
     public function parseOptionReturnsDefaultOptionValueIfOptionNameNotExistsWithinOptionsButWithinDefaultOptions()
     {
+        $objectMangerProphecy = $this->prophesize(ObjectManager::class);
+        GeneralUtility::setSingletonInstance(ObjectManager::class, $objectMangerProphecy->reveal());
+
+        $mockTranslationService = $this->getAccessibleMock(TranslationService::class, [
+            'translateFinisherOption'
+        ], [], '', false);
+
+        $mockTranslationService
+            ->expects($this->any())
+            ->method('translateFinisherOption')
+            ->willReturnArgument(3);
+
+        $objectMangerProphecy
+            ->get(TranslationService::class)
+            ->willReturn($mockTranslationService);
+
         $expected = 'defaultValue';
 
         $mockAbstractFinisher = $this->getAccessibleMockForAbstractClass(
@@ -233,6 +321,22 @@ class AbstractFinisherTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCas
      */
     public function parseOptionReturnsDefaultOptionValueIfOptionValueIsAFormElementReferenceAndTheFormElementValueIsEmpty()
     {
+        $objectMangerProphecy = $this->prophesize(ObjectManager::class);
+        GeneralUtility::setSingletonInstance(ObjectManager::class, $objectMangerProphecy->reveal());
+
+        $mockTranslationService = $this->getAccessibleMock(TranslationService::class, [
+            'translateFinisherOption'
+        ], [], '', false);
+
+        $mockTranslationService
+            ->expects($this->any())
+            ->method('translateFinisherOption')
+            ->willReturnArgument(3);
+
+        $objectMangerProphecy
+            ->get(TranslationService::class)
+            ->willReturn($mockTranslationService);
+
         $elementIdentifier = 'element-identifier-1';
         $expected = 'defaultValue';
 
@@ -269,6 +373,22 @@ class AbstractFinisherTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCas
      */
     public function parseOptionReturnsTimestampIfOptionValueIsATimestampRequestTrigger()
     {
+        $objectMangerProphecy = $this->prophesize(ObjectManager::class);
+        GeneralUtility::setSingletonInstance(ObjectManager::class, $objectMangerProphecy->reveal());
+
+        $mockTranslationService = $this->getAccessibleMock(TranslationService::class, [
+            'translateFinisherOption'
+        ], [], '', false);
+
+        $mockTranslationService
+            ->expects($this->any())
+            ->method('translateFinisherOption')
+            ->willReturnArgument(3);
+
+        $objectMangerProphecy
+            ->get(TranslationService::class)
+            ->willReturn($mockTranslationService);
+
         $mockAbstractFinisher = $this->getAccessibleMockForAbstractClass(
             AbstractFinisher::class,
             [],
diff --git a/typo3/sysext/form/Tests/Unit/Service/Fixtures/locallang_additional_text.xlf b/typo3/sysext/form/Tests/Unit/Service/Fixtures/locallang_additional_text.xlf
new file mode 100644 (file)
index 0000000..2c1856e
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xliff version="1.0" xmlns:t3="http://typo3.org/schemas/xliff">
+    <file t3:id="1489271547" source-language="en" datatype="plaintext" original="messages" date="2017-01-30T03:38:32Z" product-name="form">
+        <header/>
+        <body>
+            <trans-unit id="element.Page.renderingOptions.nextButtonLabel" xml:space="preserve">
+                <source>TEXT EN</source>
+            </trans-unit>
+
+            <trans-unit id="form-runtime-identifier.element.form-element-identifier.properties.label" xml:space="preserve">
+                <source>form-element-identifier ADDITIONAL LABEL EN</source>
+            </trans-unit>
+        </body>
+    </file>
+</xliff>
index ce4e55f..c54ad20 100644 (file)
@@ -907,4 +907,54 @@ class TranslationServiceTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestC
 
         $this->assertEquals($expected, $this->mockTranslationService->_call('translateFinisherOption', $mockFormRuntime, $finisherIdentifier, 'subject', 'subject value', $finisherRenderingOptions));
     }
+
+    /**
+     * @test
+     */
+    public function translateFormElementValueTranslateLabelFromAdditionalTranslationForConcreteFormAndConcreteElementIfElementRenderingOptionsContainsATranslationFileAndElementLabelIsNotEmptyAndPropertyShouldBeTranslatedAndTranslationExists()
+    {
+        $formRuntimeXlfPath = 'EXT:form/Tests/Unit/Service/Fixtures/locallang_form.xlf';
+        $textElementXlfPaths = [
+            10 => 'EXT:form/Tests/Unit/Service/Fixtures/locallang_text.xlf',
+            20 => 'EXT:form/Tests/Unit/Service/Fixtures/locallang_additional_text.xlf'
+         ];
+
+        $formRuntimeIdentifier = 'form-runtime-identifier';
+        $formElementIdentifier = 'form-element-identifier';
+
+        $formRuntimeRenderingOptions = [
+            'translation' => [
+                'translationFile' => $formRuntimeXlfPath,
+                'translatePropertyValueIfEmpty' => true
+            ],
+        ];
+
+        $formElementRenderingOptions = [
+            'translation' => [
+                'translationFile' => $textElementXlfPaths,
+                'translatePropertyValueIfEmpty' => true
+            ],
+        ];
+
+        $expected = 'form-element-identifier ADDITIONAL LABEL EN';
+
+        $this->store->flushData($formRuntimeXlfPath);
+
+        foreach ($textElementXlfPaths as $textElementXlfPath) {
+            $this->store->flushData($textElementXlfPath);
+        }
+
+        $mockFormElement = $this->getAccessibleMock(GenericFormElement::class, ['dummy'], [], '', false);
+
+        $mockFormElement->_set('type', 'Text');
+        $mockFormElement->_set('renderingOptions', $formElementRenderingOptions);
+        $mockFormElement->_set('identifier', $formElementIdentifier);
+        $mockFormElement->_set('label', 'some label');
+
+        $mockFormRuntime = $this->getAccessibleMock(FormRuntime::class, ['getIdentifier', 'getRenderingOptions'], [], '', false);
+        $mockFormRuntime->expects($this->any())->method('getIdentifier')->willReturn($formRuntimeIdentifier);
+        $mockFormRuntime->expects($this->any())->method('getRenderingOptions')->willReturn($formRuntimeRenderingOptions);
+
+        $this->assertEquals($expected, $this->mockTranslationService->_call('translateFormElementValue', $mockFormElement, 'label', $mockFormRuntime));
+    }
 }