Apply PSR-12 code style (#3886)
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / language / I18nHandler.class.php
index f6ba5f7e30c480636b690bc1104f21ab681fa44c..d93a24a7236c9710a85f8aa28df63094440b4b51 100644 (file)
@@ -1,5 +1,7 @@
 <?php
+
 namespace wcf\system\language;
+
 use wcf\data\language\Language;
 use wcf\system\database\util\PreparedStatementConditionBuilder;
 use wcf\system\exception\SystemException;
@@ -10,511 +12,525 @@ use wcf\util\StringUtil;
 
 /**
  * Provides internationalization support for input fields.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package    WoltLabSuite\Core\System\Language
+ *
+ * @author  Alexander Ebert
+ * @copyright   2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Language
  */
-class I18nHandler extends SingletonFactory {
-       /**
-        * list of available languages
-        * @var Language[]
-        */
-       protected $availableLanguages = [];
-       
-       /**
-        * list of element ids
-        * @var string[]
-        */
-       protected $elementIDs = [];
-       
-       /**
-        * list of plain values for elements
-        * @var string[]
-        */
-       protected $plainValues = [];
-       
-       /**
-        * i18n values for elements
-        * @var string[][]
-        */
-       protected $i18nValues = [];
-       
-       /**
-        * element options
-        * @var mixed[][]
-        */
-       protected $elementOptions = [];
-       
-       /**
-        * language variable regex object
-        * @var Regex
-        */
-       protected $regex = null;
-       
-       /**
-        * @inheritDoc
-        */
-       protected function init() {
-               $this->availableLanguages = LanguageFactory::getInstance()->getLanguages();
-       }
-       
-       /**
-        * Registers a new element id, returns false if element id is already set.
-        * 
-        * @param       string          $elementID
-        * @return      bool
-        */
-       public function register($elementID) {
-               if (in_array($elementID, $this->elementIDs)) {
-                       return false;
-               }
-               
-               $this->elementIDs[] = $elementID;
-               return true;
-       }
-       
-       /**
-        * Unregisters the element with the given id.
-        * 
-        * Does nothing if no such element exists.
-        * 
-        * @param       string          $elementID
-        * @since       5.2
-        */
-       public function unregister($elementID) {
-               $index = array_search($elementID, $this->elementIDs);
-               if ($index !== false) {
-                       unset($this->elementIDs[$index]);
-               }
-               
-               unset($this->plainValues[$elementID], $this->i18nValues[$elementID]);
-       }
-       
-       /**
-        * Reads plain and i18n values from request data.
-        * 
-        * @param       array|null      $requestData    used request data (if `null`, `$_POST` is used)
-        * @throws      SystemException
-        */
-       public function readValues(array $requestData = null) {
-               if ($requestData === null) {
-                       $requestData = $_POST;
-               }
-               
-               foreach ($this->elementIDs as $elementID) {
-                       if (isset($requestData[$elementID])) {
-                               // you should trim the string before using it; prevents unwanted newlines
-                               $this->plainValues[$elementID] = StringUtil::unifyNewlines(StringUtil::trim($requestData[$elementID]));
-                               continue;
-                       }
-                       
-                       $i18nElementID = $elementID . '_i18n';
-                       if (isset($requestData[$i18nElementID]) && is_array($requestData[$i18nElementID])) {
-                               $this->i18nValues[$elementID] = [];
-                               
-                               foreach ($requestData[$i18nElementID] as $languageID => $value) {
-                                       $this->i18nValues[$elementID][$languageID] = StringUtil::unifyNewlines(StringUtil::trim($value));
-                               }
-                               
-                               continue;
-                       }
-                       
-                       throw new SystemException("Missing expected value for element id '".$elementID."'");
-               }
-       }
-       
-       /**
-        * Returns true if given element has disabled i18n functionality.
-        * 
-        * @param       string          $elementID
-        * @return      bool
-        */
-       public function isPlainValue($elementID) {
-               if (isset($this->plainValues[$elementID])) {
-                       return true;
-               }
-               
-               return false;
-       }
-       
-       /**
-        * Returns true if given element has enabled i18n functionality.
-        * 
-        * @param       string          $elementID
-        * @return      bool
-        */
-       public function hasI18nValues($elementID) {
-               if (isset($this->i18nValues[$elementID])) {
-                       return true;
-               }
-               
-               return false;
-       }
-       
-       /**
-        * Returns the plain value for the given element.
-        * 
-        * @param       string          $elementID
-        * @return      string
-        * @see         \wcf\system\language\I18nHandler::isPlainValue()
-        */
-       public function getValue($elementID) {
-               return $this->plainValues[$elementID];
-       }
-       
-       /**
-        * Returns the values for the given element. If the element is multilingual,
-        * the multilingual values are returned, otherwise the plain value is
-        * returned for each language id.
-        * 
-        * @param       string          $elementID
-        * @return      string[]
-        */
-       public function getValues($elementID) {
-               if ($this->hasI18nValues($elementID)) {
-                       return $this->i18nValues[$elementID];
-               }
-               
-               $plainValue = $this->getValue($elementID);
-               
-               $values = [];
-               foreach ($this->availableLanguages as $language) {
-                       $values[$language->languageID] = $plainValue;
-               }
-               
-               return $values;
-       }
-       
-       /**
-        * Sets the value for the given element. If the element is multilingual,
-        * the given value is set for every available language.
-        * 
-        * @param       string          $elementID
-        * @param       string          $plainValue
-        * @param       bool            $forceAsPlainValue      if `true`, the value is added as a plain value in any case
-        * @throws      SystemException
-        */
-       public function setValue($elementID, $plainValue, $forceAsPlainValue = false) {
-               if (!is_string($plainValue)) {
-                       throw new SystemException('Invalid argument for parameter $plainValue', 0, 'Expected string. '.ucfirst(gettype($plainValue)).' given.');
-               }
-               
-               if (!$this->isPlainValue($elementID) && !$forceAsPlainValue) {
-                       $i18nValues = [];
-                       foreach ($this->availableLanguages as $language) {
-                               $i18nValues[$language->languageID] = StringUtil::trim($plainValue);
-                       }
-                       $this->setValues($elementID, $i18nValues);
-               }
-               else {
-                       $this->plainValues[$elementID] = StringUtil::trim($plainValue);
-               }
-       }
-       
-       /**
-        * Sets the values for the given element. If the element is not multilingual,
-        * use I18nHandler::setValue() instead.
-        * 
-        * @param       string          $elementID
-        * @param       string[]        $i18nValues
-        * @throws      SystemException
-        */
-       public function setValues($elementID, array $i18nValues) {
-               if (empty($i18nValues)) {
-                       throw new SystemException('Invalid argument for parameter $i18nValues', 0, 'Expected filled array as second argument. Empty array given.');
-               }
-               if (!$this->isPlainValue($elementID)) {
-                       $this->i18nValues[$elementID] = $i18nValues;
-               }
-               else {
-                       $plainValue = array_shift($i18nValues);
-                       $this->setValue($elementID, $plainValue);
-               }
-       }
-       
-       /**
-        * Returns true if the value with the given id is valid.
-        * 
-        * @param       string          $elementID
-        * @param       bool            $requireI18n
-        * @param       bool            $permitEmptyValue
-        * @return      bool
-        */
-       public function validateValue($elementID, $requireI18n = false, $permitEmptyValue = false) {
-               // do not force i18n if only one language is available
-               if ($requireI18n && count($this->availableLanguages) == 1) {
-                       $requireI18n = false;
-               }
-               
-               if ($this->isPlainValue($elementID)) {
-                       // plain values may be left empty
-                       if ($permitEmptyValue) {
-                               return true;
-                       }
-                       
-                       if ($requireI18n || $this->getValue($elementID) == '') {
-                               return false;
-                       }
-               }
-               else if ($requireI18n && (!isset($this->i18nValues[$elementID]) || empty($this->i18nValues[$elementID]))) {
-                       return false;
-               }
-               else {
-                       foreach ($this->availableLanguages as $language) {
-                               if (!isset($this->i18nValues[$elementID][$language->languageID])) {
-                                       return false;
-                               }
-                               
-                               if (!$permitEmptyValue && empty($this->i18nValues[$elementID][$language->languageID])) {
-                                       return false;
-                               }
-                       }
-               }
-               
-               return true;
-       }
-       
-       /**
-        * Saves language variable for i18n.
-        * 
-        * @param       string|string[]         $elementID              either the id of the element or externally passed array `languageID => value`
-        * @param       string                  $languageVariable
-        * @param       string                  $languageCategory
-        * @param       int                     $packageID
-        */
-       public function save($elementID, $languageVariable, $languageCategory, $packageID = PACKAGE_ID) {
-               // get language category id
-               $sql = "SELECT  languageCategoryID
-                       FROM    wcf".WCF_N."_language_category
-                       WHERE   languageCategory = ?";
-               $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute([$languageCategory]);
-               $row = $statement->fetchArray();
-               $languageCategoryID = $row['languageCategoryID'];
-               
-               if (count($this->availableLanguages) == 1) {
-                       $languageIDs = array_keys($this->availableLanguages);
-               }
-               else {
-                       if (is_array($elementID)) {
-                               $languageIDs = array_keys($elementID);
-                       }
-                       else {
-                               $languageIDs = array_keys($this->i18nValues[$elementID]);
-                       }
-               }
-               
-               $conditions = new PreparedStatementConditionBuilder();
-               $conditions->add("languageID IN (?)", [$languageIDs]);
-               $conditions->add("languageItem = ?", [$languageVariable]);
-               
-               $sql = "SELECT  languageItemID, languageID
-                       FROM    wcf".WCF_N."_language_item
-                       ".$conditions;
-               $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute($conditions->getParameters());
-               $languageItemIDs = $statement->fetchMap('languageID', 'languageItemID');
-               
-               $insertLanguageIDs = $updateLanguageIDs = [];
-               foreach ($languageIDs as $languageID) {
-                       if (isset($languageItemIDs[$languageID])) {
-                               $updateLanguageIDs[] = $languageID;
-                       }
-                       else {
-                               $insertLanguageIDs[] = $languageID;
-                       }
-               }
-               
-               // insert language items
-               if (!empty($insertLanguageIDs)) {
-                       $sql = "INSERT INTO     wcf".WCF_N."_language_item
-                                               (languageID, languageItem, languageItemValue, languageItemOriginIsSystem, languageCategoryID, packageID)
-                               VALUES          (?, ?, ?, ?, ?, ?)";
-                       $statement = WCF::getDB()->prepareStatement($sql);
-                       
-                       foreach ($insertLanguageIDs as $languageID) {
-                               if (is_array($elementID)) {
-                                       $value = $elementID[$languageID];
-                               }
-                               else if (isset($this->i18nValues[$elementID])) {
-                                       $value = $this->i18nValues[$elementID][$languageID];
-                               }
-                               else {
-                                       $value = $this->plainValues[$elementID];
-                               }
-                               
-                               $statement->execute([
-                                       $languageID,
-                                       $languageVariable,
-                                       $value,
-                                       0,
-                                       $languageCategoryID,
-                                       $packageID
-                               ]);
-                       }
-               }
-               
-               // update language items
-               if (!empty($updateLanguageIDs)) {
-                       $sql = "UPDATE  wcf".WCF_N."_language_item
-                               SET     languageItemValue = ?,
-                                       languageItemOriginIsSystem = ?
-                               WHERE   languageItemID = ?";
-                       $statement = WCF::getDB()->prepareStatement($sql);
-                       
-                       foreach ($updateLanguageIDs as $languageID) {
-                               if (is_array($elementID)) {
-                                       $value = $elementID[$languageID];
-                               }
-                               else if (isset($this->i18nValues[$elementID])) {
-                                       $value = $this->i18nValues[$elementID][$languageID];
-                               }
-                               else {
-                                       $value = $this->plainValues[$elementID];
-                               }
-                               
-                               $statement->execute([
-                                       $value,
-                                       0,
-                                       $languageItemIDs[$languageID]
-                               ]);
-                       }
-               }
-               
-               // reset language cache
-               LanguageFactory::getInstance()->deleteLanguageCache();
-       }
-       
-       /**
-        * Removes previously created i18n language variables.
-        * 
-        * @param       string          $languageVariable
-        */
-       public function remove($languageVariable) {
-               $sql = "DELETE FROM     wcf".WCF_N."_language_item
-                       WHERE           languageItem = ?";
-               $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute([$languageVariable]);
-               
-               // reset language cache
-               LanguageFactory::getInstance()->deleteLanguageCache();
-       }
-       
-       /**
-        * Sets additional options for elements, required if updating values.
-        * 
-        * @param       int             $elementID
-        * @param       int             $packageID
-        * @param       string          $value
-        * @param       string          $pattern
-        */
-       public function setOptions($elementID, $packageID, $value, $pattern) {
-               $this->elementOptions[$elementID] = [
-                       'packageID' => $packageID,
-                       'pattern' => $pattern,
-                       'value' => $value
-               ];
-       }
-       
-       /**
-        * Assigns element values to template. Using request data once reading
-        * initial database data is explicitly disallowed.
-        * 
-        * @param       bool            $useRequestData
-        */
-       public function assignVariables($useRequestData = true) {
-               $elementValues = [];
-               $elementValuesI18n = [];
-               
-               foreach ($this->elementIDs as $elementID) {
-                       $value = '';
-                       $i18nValues = [];
-                       
-                       // use POST values instead of querying database
-                       if ($useRequestData) {
-                               if ($this->isPlainValue($elementID)) {
-                                       $value = $this->getValue($elementID);
-                               }
-                               else {
-                                       if ($this->hasI18nValues($elementID)) {
-                                               $i18nValues = $this->i18nValues[$elementID];
-                                               // encoding the entries for javascript
-                                               foreach ($i18nValues as $languageID => $value) {
-                                                       $i18nValues[$languageID] = StringUtil::encodeJS(StringUtil::unifyNewlines($value));
-                                               }
-                                       }
-                                       else {
-                                               $i18nValues = [];
-                                       }
-                               }
-                       }
-                       else {
-                               $isI18n = Regex::compile('^'.$this->elementOptions[$elementID]['pattern'].'$')->match($this->elementOptions[$elementID]['value']);
-                               if (!$isI18n) {
-                                       // check if it's a regular language variable
-                                       $isI18n = Regex::compile('^([a-zA-Z0-9-_]+\.)+[a-zA-Z0-9-_]+$')->match($this->elementOptions[$elementID]['value']);
-                               }
-                               
-                               if ($isI18n) {
-                                       // use i18n values from language items
-                                       $sql = "SELECT  languageID, languageItemValue
-                                               FROM    wcf".WCF_N."_language_item
-                                               WHERE   languageItem = ?";
-                                       $statement = WCF::getDB()->prepareStatement($sql);
-                                       $statement->execute([
-                                               $this->elementOptions[$elementID]['value']
-                                       ]);
-                                       while ($row = $statement->fetchArray()) {
-                                               $languageItemValue = StringUtil::unifyNewlines($row['languageItemValue']);
-                                               $i18nValues[$row['languageID']] = StringUtil::encodeJS($languageItemValue);
-                                               
-                                               if ($row['languageID'] == LanguageFactory::getInstance()->getDefaultLanguageID()) {
-                                                       $value = $languageItemValue;
-                                               }
-                                       }
-                                       
-                                       // item appeared to be a language item but either is not or does not exist
-                                       if (empty($i18nValues) && empty($value)) {
-                                               $value = $this->elementOptions[$elementID]['value'];
-                                       }
-                               }
-                               else {
-                                       // use data provided by setOptions()
-                                       $value = $this->elementOptions[$elementID]['value'];
-                               }
-                       }
-                       
-                       $elementValues[$elementID] = $value;
-                       $elementValuesI18n[$elementID] = $i18nValues;
-               }
-               
-               WCF::getTPL()->assign([
-                       'availableLanguages' => $this->availableLanguages,
-                       'i18nPlainValues' => $elementValues,
-                       'i18nValues' => $elementValuesI18n
-               ]);
-       }
-       
-       /**
-        * Resets internally stored data after creating a new object through a form.
-        */
-       public function reset() {
-               $this->i18nValues = $this->plainValues = [];
-       }
-       
-       /**
-        * Returns true if given string equals a language variable.
-        * 
-        * @param       string          $string
-        * @return      bool
-        */
-       protected function isLanguageVariable($string) {
-               if ($this->regex === null) {
-                       $this->regex = new Regex('^([a-zA-Z0-9-_]+\.)+[a-zA-Z0-9-_]+$');
-               }
-               
-               return $this->regex->match($string);
-       }
+class I18nHandler extends SingletonFactory
+{
+    /**
+     * list of available languages
+     * @var Language[]
+     */
+    protected $availableLanguages = [];
+
+    /**
+     * list of element ids
+     * @var string[]
+     */
+    protected $elementIDs = [];
+
+    /**
+     * list of plain values for elements
+     * @var string[]
+     */
+    protected $plainValues = [];
+
+    /**
+     * i18n values for elements
+     * @var string[][]
+     */
+    protected $i18nValues = [];
+
+    /**
+     * element options
+     * @var mixed[][]
+     */
+    protected $elementOptions = [];
+
+    /**
+     * language variable regex object
+     * @var Regex
+     */
+    protected $regex;
+
+    /**
+     * @inheritDoc
+     */
+    protected function init()
+    {
+        $this->availableLanguages = LanguageFactory::getInstance()->getLanguages();
+    }
+
+    /**
+     * Registers a new element id, returns false if element id is already set.
+     *
+     * @param string $elementID
+     * @return  bool
+     */
+    public function register($elementID)
+    {
+        if (\in_array($elementID, $this->elementIDs)) {
+            return false;
+        }
+
+        $this->elementIDs[] = $elementID;
+
+        return true;
+    }
+
+    /**
+     * Unregisters the element with the given id.
+     *
+     * Does nothing if no such element exists.
+     *
+     * @param string $elementID
+     * @since   5.2
+     */
+    public function unregister($elementID)
+    {
+        $index = \array_search($elementID, $this->elementIDs);
+        if ($index !== false) {
+            unset($this->elementIDs[$index]);
+        }
+
+        unset($this->plainValues[$elementID], $this->i18nValues[$elementID]);
+    }
+
+    /**
+     * Reads plain and i18n values from request data.
+     *
+     * @param array|null $requestData used request data (if `null`, `$_POST` is used)
+     * @throws  SystemException
+     */
+    public function readValues(?array $requestData = null)
+    {
+        if ($requestData === null) {
+            $requestData = $_POST;
+        }
+
+        foreach ($this->elementIDs as $elementID) {
+            if (isset($requestData[$elementID])) {
+                // you should trim the string before using it; prevents unwanted newlines
+                $this->plainValues[$elementID] = StringUtil::unifyNewlines(StringUtil::trim($requestData[$elementID]));
+                continue;
+            }
+
+            $i18nElementID = $elementID . '_i18n';
+            if (isset($requestData[$i18nElementID]) && \is_array($requestData[$i18nElementID])) {
+                $this->i18nValues[$elementID] = [];
+
+                foreach ($requestData[$i18nElementID] as $languageID => $value) {
+                    $this->i18nValues[$elementID][$languageID] = StringUtil::unifyNewlines(StringUtil::trim($value));
+                }
+
+                continue;
+            }
+
+            throw new SystemException("Missing expected value for element id '" . $elementID . "'");
+        }
+    }
+
+    /**
+     * Returns true if given element has disabled i18n functionality.
+     *
+     * @param string $elementID
+     * @return  bool
+     */
+    public function isPlainValue($elementID)
+    {
+        if (isset($this->plainValues[$elementID])) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns true if given element has enabled i18n functionality.
+     *
+     * @param string $elementID
+     * @return  bool
+     */
+    public function hasI18nValues($elementID)
+    {
+        if (isset($this->i18nValues[$elementID])) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the plain value for the given element.
+     *
+     * @param string $elementID
+     * @return  string
+     * @see     \wcf\system\language\I18nHandler::isPlainValue()
+     */
+    public function getValue($elementID)
+    {
+        return $this->plainValues[$elementID];
+    }
+
+    /**
+     * Returns the values for the given element. If the element is multilingual,
+     * the multilingual values are returned, otherwise the plain value is
+     * returned for each language id.
+     *
+     * @param string $elementID
+     * @return  string[]
+     */
+    public function getValues($elementID)
+    {
+        if ($this->hasI18nValues($elementID)) {
+            return $this->i18nValues[$elementID];
+        }
+
+        $plainValue = $this->getValue($elementID);
+
+        $values = [];
+        foreach ($this->availableLanguages as $language) {
+            $values[$language->languageID] = $plainValue;
+        }
+
+        return $values;
+    }
+
+    /**
+     * Sets the value for the given element. If the element is multilingual,
+     * the given value is set for every available language.
+     *
+     * @param string $elementID
+     * @param string $plainValue
+     * @param bool $forceAsPlainValue if `true`, the value is added as a plain value in any case
+     * @throws  SystemException
+     */
+    public function setValue($elementID, $plainValue, $forceAsPlainValue = false)
+    {
+        if (!\is_string($plainValue)) {
+            throw new SystemException(
+                'Invalid argument for parameter $plainValue',
+                0,
+                'Expected string. ' . \ucfirst(\gettype($plainValue)) . ' given.'
+            );
+        }
+
+        if (!$this->isPlainValue($elementID) && !$forceAsPlainValue) {
+            $i18nValues = [];
+            foreach ($this->availableLanguages as $language) {
+                $i18nValues[$language->languageID] = StringUtil::trim($plainValue);
+            }
+            $this->setValues($elementID, $i18nValues);
+        } else {
+            $this->plainValues[$elementID] = StringUtil::trim($plainValue);
+        }
+    }
+
+    /**
+     * Sets the values for the given element. If the element is not multilingual,
+     * use I18nHandler::setValue() instead.
+     *
+     * @param string $elementID
+     * @param string[] $i18nValues
+     * @throws  SystemException
+     */
+    public function setValues($elementID, array $i18nValues)
+    {
+        if (empty($i18nValues)) {
+            throw new SystemException(
+                'Invalid argument for parameter $i18nValues',
+                0,
+                'Expected filled array as second argument. Empty array given.'
+            );
+        }
+        if (!$this->isPlainValue($elementID)) {
+            $this->i18nValues[$elementID] = $i18nValues;
+        } else {
+            $plainValue = \array_shift($i18nValues);
+            $this->setValue($elementID, $plainValue);
+        }
+    }
+
+    /**
+     * Returns true if the value with the given id is valid.
+     *
+     * @param string $elementID
+     * @param bool $requireI18n
+     * @param bool $permitEmptyValue
+     * @return  bool
+     */
+    public function validateValue($elementID, $requireI18n = false, $permitEmptyValue = false)
+    {
+        // do not force i18n if only one language is available
+        if ($requireI18n && \count($this->availableLanguages) == 1) {
+            $requireI18n = false;
+        }
+
+        if ($this->isPlainValue($elementID)) {
+            // plain values may be left empty
+            if ($permitEmptyValue) {
+                return true;
+            }
+
+            if ($requireI18n || $this->getValue($elementID) == '') {
+                return false;
+            }
+        } elseif ($requireI18n && (!isset($this->i18nValues[$elementID]) || empty($this->i18nValues[$elementID]))) {
+            return false;
+        } else {
+            foreach ($this->availableLanguages as $language) {
+                if (!isset($this->i18nValues[$elementID][$language->languageID])) {
+                    return false;
+                }
+
+                if (!$permitEmptyValue && empty($this->i18nValues[$elementID][$language->languageID])) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Saves language variable for i18n.
+     *
+     * @param string|string[] $elementID either the id of the element or externally passed array `languageID => value`
+     * @param string $languageVariable
+     * @param string $languageCategory
+     * @param int $packageID
+     */
+    public function save($elementID, $languageVariable, $languageCategory, $packageID = PACKAGE_ID)
+    {
+        // get language category id
+        $sql = "SELECT  languageCategoryID
+                FROM    wcf" . WCF_N . "_language_category
+                WHERE   languageCategory = ?";
+        $statement = WCF::getDB()->prepareStatement($sql);
+        $statement->execute([$languageCategory]);
+        $row = $statement->fetchArray();
+        $languageCategoryID = $row['languageCategoryID'];
+
+        if (\count($this->availableLanguages) == 1) {
+            $languageIDs = \array_keys($this->availableLanguages);
+        } else {
+            if (\is_array($elementID)) {
+                $languageIDs = \array_keys($elementID);
+            } else {
+                $languageIDs = \array_keys($this->i18nValues[$elementID]);
+            }
+        }
+
+        $conditions = new PreparedStatementConditionBuilder();
+        $conditions->add("languageID IN (?)", [$languageIDs]);
+        $conditions->add("languageItem = ?", [$languageVariable]);
+
+        $sql = "SELECT  languageItemID, languageID
+                FROM    wcf" . WCF_N . "_language_item
+                " . $conditions;
+        $statement = WCF::getDB()->prepareStatement($sql);
+        $statement->execute($conditions->getParameters());
+        $languageItemIDs = $statement->fetchMap('languageID', 'languageItemID');
+
+        $insertLanguageIDs = $updateLanguageIDs = [];
+        foreach ($languageIDs as $languageID) {
+            if (isset($languageItemIDs[$languageID])) {
+                $updateLanguageIDs[] = $languageID;
+            } else {
+                $insertLanguageIDs[] = $languageID;
+            }
+        }
+
+        // insert language items
+        if (!empty($insertLanguageIDs)) {
+            $sql = "INSERT INTO wcf" . WCF_N . "_language_item
+                                (languageID, languageItem, languageItemValue, languageItemOriginIsSystem, languageCategoryID, packageID)
+                    VALUES      (?, ?, ?, ?, ?, ?)";
+            $statement = WCF::getDB()->prepareStatement($sql);
+
+            foreach ($insertLanguageIDs as $languageID) {
+                if (\is_array($elementID)) {
+                    $value = $elementID[$languageID];
+                } elseif (isset($this->i18nValues[$elementID])) {
+                    $value = $this->i18nValues[$elementID][$languageID];
+                } else {
+                    $value = $this->plainValues[$elementID];
+                }
+
+                $statement->execute([
+                    $languageID,
+                    $languageVariable,
+                    $value,
+                    0,
+                    $languageCategoryID,
+                    $packageID,
+                ]);
+            }
+        }
+
+        // update language items
+        if (!empty($updateLanguageIDs)) {
+            $sql = "UPDATE  wcf" . WCF_N . "_language_item
+                    SET     languageItemValue = ?,
+                            languageItemOriginIsSystem = ?
+                    WHERE   languageItemID = ?";
+            $statement = WCF::getDB()->prepareStatement($sql);
+
+            foreach ($updateLanguageIDs as $languageID) {
+                if (\is_array($elementID)) {
+                    $value = $elementID[$languageID];
+                } elseif (isset($this->i18nValues[$elementID])) {
+                    $value = $this->i18nValues[$elementID][$languageID];
+                } else {
+                    $value = $this->plainValues[$elementID];
+                }
+
+                $statement->execute([
+                    $value,
+                    0,
+                    $languageItemIDs[$languageID],
+                ]);
+            }
+        }
+
+        // reset language cache
+        LanguageFactory::getInstance()->deleteLanguageCache();
+    }
+
+    /**
+     * Removes previously created i18n language variables.
+     *
+     * @param string $languageVariable
+     */
+    public function remove($languageVariable)
+    {
+        $sql = "DELETE FROM wcf" . WCF_N . "_language_item
+                WHERE       languageItem = ?";
+        $statement = WCF::getDB()->prepareStatement($sql);
+        $statement->execute([$languageVariable]);
+
+        // reset language cache
+        LanguageFactory::getInstance()->deleteLanguageCache();
+    }
+
+    /**
+     * Sets additional options for elements, required if updating values.
+     *
+     * @param int $elementID
+     * @param int $packageID
+     * @param string $value
+     * @param string $pattern
+     */
+    public function setOptions($elementID, $packageID, $value, $pattern)
+    {
+        $this->elementOptions[$elementID] = [
+            'packageID' => $packageID,
+            'pattern' => $pattern,
+            'value' => $value,
+        ];
+    }
+
+    /**
+     * Assigns element values to template. Using request data once reading
+     * initial database data is explicitly disallowed.
+     *
+     * @param bool $useRequestData
+     */
+    public function assignVariables($useRequestData = true)
+    {
+        $elementValues = [];
+        $elementValuesI18n = [];
+
+        foreach ($this->elementIDs as $elementID) {
+            $value = '';
+            $i18nValues = [];
+
+            // use POST values instead of querying database
+            if ($useRequestData) {
+                if ($this->isPlainValue($elementID)) {
+                    $value = $this->getValue($elementID);
+                } else {
+                    if ($this->hasI18nValues($elementID)) {
+                        $i18nValues = $this->i18nValues[$elementID];
+                        // encoding the entries for javascript
+                        foreach ($i18nValues as $languageID => $value) {
+                            $i18nValues[$languageID] = StringUtil::encodeJS(StringUtil::unifyNewlines($value));
+                        }
+                    } else {
+                        $i18nValues = [];
+                    }
+                }
+            } else {
+                $isI18n = Regex::compile('^' . $this->elementOptions[$elementID]['pattern'] . '$')
+                    ->match($this->elementOptions[$elementID]['value']);
+                if (!$isI18n) {
+                    // check if it's a regular language variable
+                    $isI18n = Regex::compile('^([a-zA-Z0-9-_]+\.)+[a-zA-Z0-9-_]+$')
+                        ->match($this->elementOptions[$elementID]['value']);
+                }
+
+                if ($isI18n) {
+                    // use i18n values from language items
+                    $sql = "SELECT  languageID, languageItemValue
+                            FROM    wcf" . WCF_N . "_language_item
+                            WHERE   languageItem = ?";
+                    $statement = WCF::getDB()->prepareStatement($sql);
+                    $statement->execute([
+                        $this->elementOptions[$elementID]['value'],
+                    ]);
+                    while ($row = $statement->fetchArray()) {
+                        $languageItemValue = StringUtil::unifyNewlines($row['languageItemValue']);
+                        $i18nValues[$row['languageID']] = StringUtil::encodeJS($languageItemValue);
+
+                        if ($row['languageID'] == LanguageFactory::getInstance()->getDefaultLanguageID()) {
+                            $value = $languageItemValue;
+                        }
+                    }
+
+                    // item appeared to be a language item but either is not or does not exist
+                    if (empty($i18nValues) && empty($value)) {
+                        $value = $this->elementOptions[$elementID]['value'];
+                    }
+                } else {
+                    // use data provided by setOptions()
+                    $value = $this->elementOptions[$elementID]['value'];
+                }
+            }
+
+            $elementValues[$elementID] = $value;
+            $elementValuesI18n[$elementID] = $i18nValues;
+        }
+
+        WCF::getTPL()->assign([
+            'availableLanguages' => $this->availableLanguages,
+            'i18nPlainValues' => $elementValues,
+            'i18nValues' => $elementValuesI18n,
+        ]);
+    }
+
+    /**
+     * Resets internally stored data after creating a new object through a form.
+     */
+    public function reset()
+    {
+        $this->i18nValues = $this->plainValues = [];
+    }
+
+    /**
+     * Returns true if given string equals a language variable.
+     *
+     * @param string $string
+     * @return  bool
+     */
+    protected function isLanguageVariable($string)
+    {
+        if ($this->regex === null) {
+            $this->regex = new Regex('^([a-zA-Z0-9-_]+\.)+[a-zA-Z0-9-_]+$');
+        }
+
+        return $this->regex->match($string);
+    }
 }