Merge branch '5.3'
authorMatthias Schmidt <gravatronics@live.com>
Fri, 5 Feb 2021 10:34:20 +0000 (11:34 +0100)
committerMatthias Schmidt <gravatronics@live.com>
Fri, 5 Feb 2021 10:34:20 +0000 (11:34 +0100)
1  2 
wcfsetup/install/files/lib/system/form/builder/field/TI18nFormField.class.php

index d7b28e8046fc3906911a9d258011b01c811e5ab3,2bebce1be751f9c1d9326034d29c4a8c0485e963..e82831bfdeff39d3cdd86f909d77cbf18bac81ef
@@@ -17,424 -15,402 +17,426 @@@ use wcf\util\StringUtil
  
  /**
   * Provides default implementations of `II18nFormField` methods and other i18n-related methods.
 - * 
 + *
   * This trait can only to be used in combination with `TFormField`.
 - * 
 - * @author    Matthias Schmidt
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\System\Form\Builder\Field
 - * @since     5.2
 - * 
 - * @mixin     II18nFormField
 + *
 + * @author  Matthias Schmidt
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\System\Form\Builder\Field
 + * @since   5.2
 + *
 + * @mixin   II18nFormField
   */
 -trait TI18nFormField {
 -      /**
 -       * `true` if this field supports i18n input and `false` otherwise
 -       * @var bool
 -       */
 -      protected $i18n = false;
 -      
 -      /**
 -       * `true` if this field requires i18n input and `false` otherwise
 -       * @var bool
 -       */
 -      protected $i18nRequired = false;
 -      
 -      /**
 -       * pattern for the language item used to save the i18n values
 -       * @var null|string
 -       */
 -      protected $languageItemPattern;
 -      
 -      /**
 -       * name of the nin-i18n JavaScript data handler module used for Ajax dialogs
 -       * @var null|string
 -       */
 -      protected $nonI18nJavaScriptDataHandlerModule;
 -      
 -      /**
 -       * Returns additional template variables used to generate the html representation
 -       * of this node.
 -       * 
 -       * @return      array           additional template variables
 -       */
 -      public function getHtmlVariables() {
 -              if ($this->isI18n()) {
 -                      I18nHandler::getInstance()->assignVariables();
 -                      
 -                      return [
 -                              'elementIdentifier' => $this->getPrefixedId(),
 -                              'forceSelection' => $this->isI18nRequired()
 -                      ];
 -              }
 -              
 -              return [];
 -      }
 -      
 -      /**
 -       * Returns the pattern for the language item used to save the i18n values.
 -       * 
 -       * @return      string                          language item pattern
 -       * 
 -       * @throws      \BadMethodCallException         if i18n is disabled for this field or no language item has been set
 -       */
 -      public function getLanguageItemPattern() {
 -              if (!$this->isI18n()) {
 -                      throw new \BadMethodCallException("You can only get the language item pattern for fields with i18n enabled.");
 -              }
 -              
 -              if ($this->languageItemPattern === null) {
 -                      throw new \BadMethodCallException("Language item pattern has not been set.");
 -              }
 -              
 -              return $this->languageItemPattern;
 -      }
 -      
 -      /**
 -       * Returns the field value saved in the database.
 -       *
 -       * This method is useful if the actual returned by `getValue()`
 -       * cannot be stored in the database as-is. If the return value of
 -       * `getValue()` is, however, the actual value that should be stored
 -       * in the database, this method is expected to call `getValue()`
 -       * internally.
 -       *
 -       * @return      mixed
 -       */
 -      public function getSaveValue() {
 -              if (!$this->hasSaveValue()) {
 -                      return null;
 -              }
 -              else if ($this->getValue() === null && $this instanceof INullableFormField && !$this->isNullable()) {
 -                      return '';
 -              }
 -              
 -              return parent::getSaveValue();
 -      }
 -      
 -      /**
 -       * Returns the value of this field or `null` if no value has been set.
 -       * 
 -       * @return      mixed
 -       */
 -      public function getValue() {
 -              if ($this->isI18n()) {
 -                      if ($this->hasPlainValue()) {
 -                              return I18nHandler::getInstance()->getValue($this->getPrefixedId());
 -                      }
 -                      else if ($this->hasI18nValues()) {
 -                              $values = I18nHandler::getInstance()->getValues($this->getPrefixedId());
 -                              
 -                              // handle legacy values from the past when multilingual values
 -                              // were available
 -                              if (count(LanguageFactory::getInstance()->getLanguages()) === 1) {
 -                                      if (isset($values[WCF::getLanguage()->languageID])) {
 -                                              return $values[WCF::getLanguage()->languageID];
 -                                      }
 -                                      
 -                                      return current($values);
 -                              }
 -                              
 -                              return $values;
 -                      }
 -                      
 -                      return '';
 -              }
 -              
 -              return $this->value;
 -      }
 -      
 -      /**
 -       * Returns `true` if the current field value is a i18n value and returns `false`
 -       * otherwise or if no value has been set.
 -       * 
 -       * @return      bool
 -       */
 -      public function hasI18nValues() {
 -              return I18nHandler::getInstance()->hasI18nValues($this->getPrefixedId());
 -      }
 -      
 -      /**
 -       * Returns `true` if the current field value is a plain value and returns `false`
 -       * otherwise or if no value has been set.
 -       * 
 -       * @return      bool
 -       */
 -      public function hasPlainValue() {
 -              return I18nHandler::getInstance()->isPlainValue($this->getPrefixedId());
 -      }
 -      
 -      /**
 -       * Returns `true` if this field provides a value that can simply be stored
 -       * in a column of the database object's database table and returns `false`
 -       * otherwise.
 -       * 
 -       * Note: If `false` is returned, this field should probabily add its own
 -       * `IFormFieldDataProcessor` object to the form document's data processor.
 -       * A suitable place to add the processor is the `parent()`
 -       * 
 -       * @return      bool
 -       */
 -      public function hasSaveValue() {
 -              return !$this->isI18n() || $this->hasPlainValue();
 -      }
 -      
 -      /**
 -       * Sets whether this field is supports i18n input and returns this field.
 -       * 
 -       * @param       bool            $i18n           determines if field supports i18n input
 -       * @return      II18nFormField                  this field
 -       */
 -      public function i18n($i18n = true) {
 -              if ($this->javaScriptDataHandlerModule) {
 -                      if ($this->isI18n() && !$i18n) {
 -                              $this->javaScriptDataHandlerModule = $this->nonI18nJavaScriptDataHandlerModule;
 -                      }
 -                      else if (!$this->isI18n() && $i18n) {
 -                              $this->nonI18nJavaScriptDataHandlerModule = $this->javaScriptDataHandlerModule;
 -                              $this->javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/ValueI18n';
 -                      }
 -              }
 -              
 -              $this->i18n = $i18n;
 -              
 -              return $this;
 -      }
 -      
 -      /**
 -       * Sets whether this field's value must be i18n input and returns this field.
 -       *
 -       * If this method sets that the field's value must be i18n input, it also must
 -       * ensure that i18n support is enabled.
 -       *
 -       * @param       bool            $i18nRequired           determines if field value must be i18n input
 -       * @return      static                                  this field
 -       */
 -      public function i18nRequired($i18nRequired = true) {
 -              $this->i18nRequired = $i18nRequired;
 -              $this->i18n();
 -              
 -              return $this;
 -      }
 -      
 -      /**
 -       * Returns `true` if this field supports i18n input and returns `false` otherwise.
 -       * By default, fields do not support i18n input.
 -       * 
 -       * @return      bool
 -       */
 -      public function isI18n() {
 -              return $this->i18n;
 -      }
 -      
 -      /**
 -       * Returns `true` if this field's value must be i18n input and returns `false` otherwise.
 -       * By default, fields do not support i18n input.
 -       * 
 -       * @return      bool
 -       */
 -      public function isI18nRequired() {
 -              return $this->i18nRequired;
 -      }
 -      
 -      /**
 -       * Sets the pattern for the language item used to save the i18n values
 -       * and returns this field.
 -       * 
 -       * @param       string          $pattern        language item pattern
 -       * @return      II18nFormField                  this field
 -       * 
 -       * @throws      \BadMethodCallException         if i18n is disabled for this field
 -       * @throws      \InvalidArgumentException       if the given pattern is invalid
 -       */
 -      public function languageItemPattern($pattern) {
 -              if (!$this->isI18n()) {
 -                      throw new \BadMethodCallException("The language item pattern can only be set for fields with i18n enabled.");
 -              }
 -              
 -              if (!Regex::compile($pattern)->isValid()) {
 -                      throw new \InvalidArgumentException("Given pattern is invalid.");
 -              }
 -              
 -              $this->languageItemPattern = $pattern;
 -              
 -              return $this;
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function updatedObject(array $data, IStorableObject $object, $loadValues = true) {
 -              if ($loadValues && isset($data[$this->getObjectProperty()])) {
 -                      $value = $data[$this->getObjectProperty()];
 -                      
 -                      if ($this->isI18n()) {
 -                              // do not use `I18nHandler::setOptions()` because then `I18nHandler` only
 -                              // reads the values when assigning the template variables and the values
 -                              // are not available in this class via `getValue()`
 -                              $this->setStringValue($value);
 -                      }
 -                      else {
 -                              $this->value = $value;
 -                      }
 -              }
 -              
 -              return $this;
 -      }
 -      
 -      /**
 -       * Is called once after all nodes have been added to the document this node belongs to.
 -       * 
 -       * This method enables this node to perform actions that require the whole document having
 -       * finished constructing itself and every parent-child relationship being established.
 -       * 
 -       * @return      IFormNode                       this node
 -       * 
 -       * @throws      \BadMethodCallException         if this node has already been populated
 -       */
 -      public function populate() {
 -              parent::populate();
 -              
 -              if ($this->isI18n()) {
 -                      I18nHandler::getInstance()->unregister($this->getPrefixedId());
 -                      I18nHandler::getInstance()->register($this->getPrefixedId());
 -                      
 -                      /** @var IFormDocument $document */
 -                      $document = $this->getDocument();
 -                      $document->getDataHandler()->addProcessor(new CustomFormDataProcessor('i18n', function(IFormDocument $document, array $parameters) {
 -                              if ($this->checkDependencies() && $this->hasI18nValues()) {
 -                                      $parameters[$this->getObjectProperty() . '_i18n'] = $this->getValue();
 -                              }
 -                              
 -                              return $parameters;
 -                      }));
 -              }
 -              
 -              return $this;
 -      }
 -      
 -      /**
 -       * Reads the value of this field from request data and return this field.
 -       * 
 -       * @return      IFormField      this field
 -       */
 -      public function readValue() {
 -              if ($this->isI18n()) {
 -                      I18nHandler::getInstance()->readValues($this->getDocument()->getRequestData());
 -              }
 -              else if ($this->getDocument()->hasRequestData($this->getPrefixedId())) {
 -                      $value = $this->getDocument()->getRequestData($this->getPrefixedId());
 -                      
 -                      if (is_string($value)) {
 -                              $this->value = StringUtil::trim($value);
 -                      }
 -              }
 -              
 -              return $this;
 -      }
 -      
 -      /**
 -       * Sets the value of this form field based on the given value.
 -       * If the value is a language item matching the language item pattern,
 -       * the relevant language items are loaded and their values are used as
 -       * field values.
 -       * 
 -       * @param       string          $value          set value
 -       */
 -      protected function setStringValue($value) {
 -              if (Regex::compile('^' . $this->getLanguageItemPattern() . '$')->match($value)) {
 -                      $languageItemList = new LanguageItemList();
 -                      $languageItemList->getConditionBuilder()->add('languageItem = ?', [$value]);
 -                      $languageItemList->readObjects();
 -                      
 -                      $values = [];
 -                      foreach ($languageItemList as $languageItem) {
 -                              $values[$languageItem->languageID] = $languageItem->languageItemValue;
 -                      }
 -                      
 -                      I18nHandler::getInstance()->setValues($this->getPrefixedId(), $values);
 -              }
 -              else {
 -                      I18nHandler::getInstance()->setValue($this->getPrefixedId(), $value, !$this->isI18nRequired());
 -              }
 -      }
 -      
 -      /**
 -       * Sets the value of this field and returns this field.
 -       * 
 -       * @param       string|string[]         $value          new field value
 -       * @return      static                                  this field
 -       * 
 -       * @throws      \InvalidArgumentException               if the given value is of an invalid type or otherwise is invalid
 -       */
 -      public function value($value) {
 -              if ($this->isI18n()) {
 -                      if (is_string($value) || is_numeric($value)) {
 -                              $this->setStringValue($value);
 -                      }
 -                      else if (is_array($value)) {
 -                              if (!empty($value)) {
 -                                      I18nHandler::getInstance()->setValues($this->getPrefixedId(), $value);
 -                              }
 -                      }
 -                      else {
 -                              throw new \InvalidArgumentException("Given value is neither a nor an array, " . gettype($value) . " given.");
 -                      }
 -              }
 -              else {
 -                      if (!is_string($value) && !is_numeric($value)) {
 -                              throw new \InvalidArgumentException("Given value is no string, " . gettype($value) . " given.");
 -                      }
 -                      
 -                      return parent::value($value);
 -              }
 -              
 -              return $this;
 -      }
 -      
 -      /**
 -       * Validates the node.
 -       *
 -       * Note: A `IFormParentNode` object may only return `true` if all of its child
 -       * nodes are valid. A `IFormField` object is valid if its value is valid.
 -       */
 -      public function validate() {
 -              // If i18n is required for a non-required field and the field is
 -              // empty in all languages, `I18nHandler::validateValue()` will mark
 -              // as invalid even though it is a valid state for this form field,
 -              // thus the additional condition.
 -              if ($this->isI18n() && (!empty(ArrayUtil::trim($this->getValue())) || $this->isRequired())) {
 -                      if (!I18nHandler::getInstance()->validateValue($this->getPrefixedId(), $this->isI18nRequired(), !$this->isRequired())) {
 -                              if ($this->hasPlainValue()) {
 -                                      $this->addValidationError(new FormFieldValidationError('empty'));
 -                              }
 -                              else {
 -                                      $this->addValidationError(new FormFieldValidationError('multilingual'));
 -                              }
 -                      }
 -              }
 -      }
 +trait TI18nFormField
 +{
 +    /**
 +     * `true` if this field supports i18n input and `false` otherwise
 +     * @var bool
 +     */
 +    protected $i18n = false;
 +
 +    /**
 +     * `true` if this field requires i18n input and `false` otherwise
 +     * @var bool
 +     */
 +    protected $i18nRequired = false;
 +
 +    /**
 +     * pattern for the language item used to save the i18n values
 +     * @var null|string
 +     */
 +    protected $languageItemPattern;
 +
 +    /**
 +     * name of the nin-i18n JavaScript data handler module used for Ajax dialogs
 +     * @var null|string
 +     */
 +    protected $nonI18nJavaScriptDataHandlerModule;
 +
 +    /**
 +     * Returns additional template variables used to generate the html representation
 +     * of this node.
 +     *
 +     * @return  array       additional template variables
 +     */
 +    public function getHtmlVariables()
 +    {
 +        if ($this->isI18n()) {
 +            I18nHandler::getInstance()->assignVariables();
 +
 +            return [
 +                'elementIdentifier' => $this->getPrefixedId(),
 +                'forceSelection' => $this->isI18nRequired(),
 +            ];
 +        }
 +
 +        return [];
 +    }
 +
 +    /**
 +     * Returns the pattern for the language item used to save the i18n values.
 +     *
 +     * @return  string              language item pattern
 +     *
 +     * @throws  \BadMethodCallException     if i18n is disabled for this field or no language item has been set
 +     */
 +    public function getLanguageItemPattern()
 +    {
 +        if (!$this->isI18n()) {
 +            throw new \BadMethodCallException(
 +                "You can only get the language item pattern for fields with i18n enabled."
 +            );
 +        }
 +
 +        if ($this->languageItemPattern === null) {
 +            throw new \BadMethodCallException("Language item pattern has not been set.");
 +        }
 +
 +        return $this->languageItemPattern;
 +    }
 +
 +    /**
 +     * Returns the field value saved in the database.
 +     *
 +     * This method is useful if the actual returned by `getValue()`
 +     * cannot be stored in the database as-is. If the return value of
 +     * `getValue()` is, however, the actual value that should be stored
 +     * in the database, this method is expected to call `getValue()`
 +     * internally.
 +     *
 +     * @return  mixed
 +     */
 +    public function getSaveValue()
 +    {
 +        if (!$this->hasSaveValue()) {
 +            return;
 +        } elseif ($this->getValue() === null && $this instanceof INullableFormField && !$this->isNullable()) {
 +            return '';
 +        }
 +
 +        return parent::getSaveValue();
 +    }
 +
 +    /**
 +     * Returns the value of this field or `null` if no value has been set.
 +     *
 +     * @return  mixed
 +     */
 +    public function getValue()
 +    {
 +        if ($this->isI18n()) {
 +            if ($this->hasPlainValue()) {
 +                return I18nHandler::getInstance()->getValue($this->getPrefixedId());
 +            } elseif ($this->hasI18nValues()) {
 +                $values = I18nHandler::getInstance()->getValues($this->getPrefixedId());
 +
 +                // handle legacy values from the past when multilingual values
 +                // were available
 +                if (\count(LanguageFactory::getInstance()->getLanguages()) === 1) {
 +                    if (isset($values[WCF::getLanguage()->languageID])) {
 +                        return $values[WCF::getLanguage()->languageID];
 +                    }
 +
 +                    return \current($values);
 +                }
 +
 +                return $values;
 +            }
 +
 +            return '';
 +        }
 +
 +        return $this->value;
 +    }
 +
 +    /**
 +     * Returns `true` if the current field value is a i18n value and returns `false`
 +     * otherwise or if no value has been set.
 +     *
 +     * @return  bool
 +     */
 +    public function hasI18nValues()
 +    {
 +        return I18nHandler::getInstance()->hasI18nValues($this->getPrefixedId());
 +    }
 +
 +    /**
 +     * Returns `true` if the current field value is a plain value and returns `false`
 +     * otherwise or if no value has been set.
 +     *
 +     * @return  bool
 +     */
 +    public function hasPlainValue()
 +    {
 +        return I18nHandler::getInstance()->isPlainValue($this->getPrefixedId());
 +    }
 +
 +    /**
 +     * Returns `true` if this field provides a value that can simply be stored
 +     * in a column of the database object's database table and returns `false`
 +     * otherwise.
 +     *
 +     * Note: If `false` is returned, this field should probabily add its own
 +     * `IFormFieldDataProcessor` object to the form document's data processor.
 +     * A suitable place to add the processor is the `parent()`
 +     *
 +     * @return  bool
 +     */
 +    public function hasSaveValue()
 +    {
 +        return !$this->isI18n() || $this->hasPlainValue();
 +    }
 +
 +    /**
 +     * Sets whether this field is supports i18n input and returns this field.
 +     *
 +     * @param bool $i18n determines if field supports i18n input
 +     * @return  II18nFormField          this field
 +     */
 +    public function i18n($i18n = true)
 +    {
 +        if ($this->javaScriptDataHandlerModule) {
 +            if ($this->isI18n() && !$i18n) {
 +                $this->javaScriptDataHandlerModule = $this->nonI18nJavaScriptDataHandlerModule;
 +            } elseif (!$this->isI18n() && $i18n) {
 +                $this->nonI18nJavaScriptDataHandlerModule = $this->javaScriptDataHandlerModule;
 +                $this->javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/ValueI18n';
 +            }
 +        }
 +
 +        $this->i18n = $i18n;
 +
 +        return $this;
 +    }
 +
 +    /**
 +     * Sets whether this field's value must be i18n input and returns this field.
 +     *
 +     * If this method sets that the field's value must be i18n input, it also must
 +     * ensure that i18n support is enabled.
 +     *
 +     * @param bool $i18nRequired determines if field value must be i18n input
 +     * @return  static                  this field
 +     */
 +    public function i18nRequired($i18nRequired = true)
 +    {
 +        $this->i18nRequired = $i18nRequired;
 +        $this->i18n();
 +
 +        return $this;
 +    }
 +
 +    /**
 +     * Returns `true` if this field supports i18n input and returns `false` otherwise.
 +     * By default, fields do not support i18n input.
 +     *
 +     * @return  bool
 +     */
 +    public function isI18n()
 +    {
 +        return $this->i18n;
 +    }
 +
 +    /**
 +     * Returns `true` if this field's value must be i18n input and returns `false` otherwise.
 +     * By default, fields do not support i18n input.
 +     *
 +     * @return  bool
 +     */
 +    public function isI18nRequired()
 +    {
 +        return $this->i18nRequired;
 +    }
 +
 +    /**
 +     * Sets the pattern for the language item used to save the i18n values
 +     * and returns this field.
 +     *
 +     * @param string $pattern language item pattern
 +     * @return  II18nFormField          this field
 +     *
 +     * @throws  \BadMethodCallException     if i18n is disabled for this field
 +     * @throws  \InvalidArgumentException   if the given pattern is invalid
 +     */
 +    public function languageItemPattern($pattern)
 +    {
 +        if (!$this->isI18n()) {
 +            throw new \BadMethodCallException(
 +                "The language item pattern can only be set for fields with i18n enabled."
 +            );
 +        }
 +
 +        if (!Regex::compile($pattern)->isValid()) {
 +            throw new \InvalidArgumentException("Given pattern is invalid.");
 +        }
 +
 +        $this->languageItemPattern = $pattern;
 +
 +        return $this;
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function updatedObject(array $data, IStorableObject $object, $loadValues = true)
 +    {
 +        if ($loadValues && isset($data[$this->getObjectProperty()])) {
 +            $value = $data[$this->getObjectProperty()];
 +
 +            if ($this->isI18n()) {
 +                // do not use `I18nHandler::setOptions()` because then `I18nHandler` only
 +                // reads the values when assigning the template variables and the values
 +                // are not available in this class via `getValue()`
 +                $this->setStringValue($value);
 +            } else {
 +                $this->value = $value;
 +            }
 +        }
 +
 +        return $this;
 +    }
 +
 +    /**
 +     * Is called once after all nodes have been added to the document this node belongs to.
 +     *
 +     * This method enables this node to perform actions that require the whole document having
 +     * finished constructing itself and every parent-child relationship being established.
 +     *
 +     * @return  IFormNode           this node
 +     *
 +     * @throws  \BadMethodCallException     if this node has already been populated
 +     */
 +    public function populate()
 +    {
 +        parent::populate();
 +
 +        if ($this->isI18n()) {
 +            I18nHandler::getInstance()->unregister($this->getPrefixedId());
 +            I18nHandler::getInstance()->register($this->getPrefixedId());
 +
 +            /** @var IFormDocument $document */
 +            $document = $this->getDocument();
 +            $document->getDataHandler()->addProcessor(new CustomFormDataProcessor(
 +                'i18n',
 +                function (IFormDocument $document, array $parameters) {
 +                    if ($this->checkDependencies() && $this->hasI18nValues()) {
 +                        $parameters[$this->getObjectProperty() . '_i18n'] = $this->getValue();
 +                    }
 +
 +                    return $parameters;
 +                }
 +            ));
 +        }
 +
 +        return $this;
 +    }
 +
 +    /**
 +     * Reads the value of this field from request data and return this field.
 +     *
 +     * @return  IFormField  this field
 +     */
 +    public function readValue()
 +    {
 +        if ($this->isI18n()) {
 +            I18nHandler::getInstance()->readValues($this->getDocument()->getRequestData());
 +        } elseif ($this->getDocument()->hasRequestData($this->getPrefixedId())) {
 +            $value = $this->getDocument()->getRequestData($this->getPrefixedId());
 +
 +            if (\is_string($value)) {
 +                $this->value = StringUtil::trim($value);
 +            }
 +        }
 +
 +        return $this;
 +    }
 +
 +    /**
 +     * Sets the value of this form field based on the given value.
 +     * If the value is a language item matching the language item pattern,
 +     * the relevant language items are loaded and their values are used as
 +     * field values.
 +     *
 +     * @param string $value set value
 +     */
 +    protected function setStringValue($value)
 +    {
 +        if (Regex::compile('^' . $this->getLanguageItemPattern() . '$')->match($value)) {
 +            $languageItemList = new LanguageItemList();
 +            $languageItemList->getConditionBuilder()->add('languageItem = ?', [$value]);
 +            $languageItemList->readObjects();
 +
 +            $values = [];
 +            foreach ($languageItemList as $languageItem) {
 +                $values[$languageItem->languageID] = $languageItem->languageItemValue;
 +            }
 +
 +            I18nHandler::getInstance()->setValues($this->getPrefixedId(), $values);
 +        } else {
 +            I18nHandler::getInstance()->setValue($this->getPrefixedId(), $value, !$this->isI18nRequired());
 +        }
 +    }
 +
 +    /**
 +     * Sets the value of this field and returns this field.
 +     *
 +     * @param string|string[] $value new field value
 +     * @return  static                  this field
 +     *
 +     * @throws  \InvalidArgumentException       if the given value is of an invalid type or otherwise is invalid
 +     */
 +    public function value($value)
 +    {
 +        if ($this->isI18n()) {
 +            if (\is_string($value) || \is_numeric($value)) {
 +                $this->setStringValue($value);
 +            } elseif (\is_array($value)) {
 +                if (!empty($value)) {
 +                    I18nHandler::getInstance()->setValues($this->getPrefixedId(), $value);
 +                }
 +            } else {
 +                throw new \InvalidArgumentException(
 +                    "Given value is neither a nor an array, " . \gettype($value) . " given."
 +                );
 +            }
 +        } else {
 +            if (!\is_string($value) && !\is_numeric($value)) {
 +                throw new \InvalidArgumentException("Given value is no string, " . \gettype($value) . " given.");
 +            }
 +
 +            return parent::value($value);
 +        }
 +
 +        return $this;
 +    }
 +
 +    /**
 +     * Validates the node.
 +     *
 +     * Note: A `IFormParentNode` object may only return `true` if all of its child
 +     * nodes are valid. A `IFormField` object is valid if its value is valid.
 +     */
 +    public function validate()
 +    {
-         // if i18n is required for a non-required field and the field is
-         // empty, that is no error
-         if ($this->isI18n() && (!empty(ArrayUtil::trim($this->getValue()))) || $this->isRequired()) {
++        // If i18n is required for a non-required field and the field is
++        // empty in all languages, `I18nHandler::validateValue()` will mark
++        // as invalid even though it is a valid state for this form field,
++        // thus the additional condition.
++        if ($this->isI18n() && (!empty(ArrayUtil::trim($this->getValue())) || $this->isRequired())) {
 +            if (
 +                !I18nHandler::getInstance()->validateValue(
 +                    $this->getPrefixedId(),
 +                    $this->isI18nRequired(),
 +                    !$this->isRequired()
 +                )
 +            ) {
 +                if ($this->hasPlainValue()) {
 +                    $this->addValidationError(new FormFieldValidationError('empty'));
 +                } else {
 +                    $this->addValidationError(new FormFieldValidationError('multilingual'));
 +                }
 +            }
 +        }
 +    }
  }