2 namespace wcf\data\language
;
3 use wcf\data\language\category\LanguageCategory
;
4 use wcf\data\language\category\LanguageCategoryEditor
;
5 use wcf\data\language\item\LanguageItemEditor
;
6 use wcf\data\language\item\LanguageItemList
;
7 use wcf\data\DatabaseObjectEditor
;
8 use wcf\data\IEditableCachedObject
;
9 use wcf\system\cache\builder\LanguageCacheBuilder
;
10 use wcf\system\database\util\PreparedStatementConditionBuilder
;
11 use wcf\system\exception\SystemException
;
12 use wcf\system\io\AtomicWriter
;
13 use wcf\system\language\LanguageFactory
;
16 use wcf\util\DirectoryUtil
;
17 use wcf\util\FileUtil
;
18 use wcf\util\StringUtil
;
22 * Provides functions to edit languages.
24 * @author Alexander Ebert
25 * @copyright 2001-2016 WoltLab GmbH
26 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
27 * @package com.woltlab.wcf
28 * @subpackage data.language
29 * @category Community Framework
31 * @method Language getDecoratedObject()
34 class LanguageEditor
extends DatabaseObjectEditor
implements IEditableCachedObject
{
38 protected static $baseClass = Language
::class;
43 public function delete() {
46 self
::deleteLanguageFiles($this->languageID
);
50 * Updates the language files for the given category.
52 * @param LanguageCategory $languageCategory
54 public function updateCategory(LanguageCategory
$languageCategory) {
55 $this->writeLanguageFiles([$languageCategory->languageCategoryID
]);
59 * Write the languages files.
61 * @param integer[] $languageCategoryIDs
63 protected function writeLanguageFiles(array $languageCategoryIDs) {
64 $conditions = new PreparedStatementConditionBuilder();
65 $conditions->add("languageID = ?", [$this->languageID
]);
66 $conditions->add("languageCategoryID IN (?)", [$languageCategoryIDs]);
69 $sql = "SELECT languageItem, languageItemValue, languageCustomItemValue,
70 languageUseCustomValue, languageCategoryID
71 FROM wcf".WCF_N
."_language_item
73 $statement = WCF
::getDB()->prepareStatement($sql);
74 $statement->execute($conditions->getParameters());
76 while ($row = $statement->fetchArray()) {
77 $languageCategoryID = $row['languageCategoryID'];
78 if (!isset($items[$languageCategoryID])) {
79 $items[$languageCategoryID] = [];
82 $items[$languageCategoryID][$row['languageItem']] = ($row['languageUseCustomValue']) ?
$row['languageCustomItemValue'] : $row['languageItemValue'];
85 foreach ($items as $languageCategoryID => $languageItems) {
86 $category = LanguageFactory
::getInstance()->getCategoryByID($languageCategoryID);
87 if ($category === null) {
91 $filename = WCF_DIR
.'language/'.$this->languageID
.'_'.$category->languageCategory
.'.php';
92 $writer = new AtomicWriter($filename);
94 $writer->write("<?php\n/**\n* WoltLab Community Framework\n* language: ".$this->languageCode
."\n* encoding: UTF-8\n* category: ".$category->languageCategory
."\n* generated at ".gmdate("r")."\n* \n* DO NOT EDIT THIS FILE\n*/\n");
95 foreach ($languageItems as $languageItem => $languageItemValue) {
96 $writer->write("\$this->items['".$languageItem."'] = '".str_replace("'", "\'", $languageItemValue)."';\n");
98 // compile dynamic language variables
99 if ($category->languageCategory
!= 'wcf.global' && strpos($languageItemValue, '{') !== false) {
100 $writer->write("\$this->dynamicItems['".$languageItem."'] = '");
102 $output = LanguageFactory
::getInstance()->getScriptingCompiler()->compileString($languageItem, $languageItemValue);
103 $writer->write(str_replace("'", "\'", $output['template']));
105 $writer->write("';\n");
111 FileUtil
::makeWritable($filename);
116 * Exports this language.
118 * @param integer[] $packageIDArray
119 * @param boolean $exportCustomValues
121 public function export($packageIDArray = [], $exportCustomValues = false) {
122 $conditions = new PreparedStatementConditionBuilder();
123 $conditions->add("language_item.languageID = ?", [$this->languageID
]);
129 echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<language xmlns=\"http://www.woltlab.com\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.woltlab.com http://www.woltlab.com/XSD/maelstrom/language.xsd\" languagecode=\"".$this->languageCode
."\" languagename=\"".$this->languageName
."\" countrycode=\"".$this->countryCode
."\">\n";
133 if (!empty($packageIDArray)) {
134 $conditions->add("language_item.packageID IN (?)", [$packageIDArray]);
137 $sql = "SELECT languageItem, " . ($exportCustomValues ?
"CASE WHEN languageUseCustomValue > 0 THEN languageCustomItemValue ELSE languageItemValue END AS languageItemValue" : "languageItemValue") . ", languageCategory
138 FROM wcf".WCF_N
."_language_item language_item
139 LEFT JOIN wcf".WCF_N
."_language_category language_category
140 ON (language_category.languageCategoryID = language_item.languageCategoryID)
142 $statement = WCF
::getDB()->prepareStatement($sql);
143 $statement->execute($conditions->getParameters());
144 while ($row = $statement->fetchArray()) {
145 $items[$row['languageCategory']][$row['languageItem']] = $row['languageItemValue'];
151 foreach ($items as $category => $categoryItems) {
153 ksort($categoryItems);
156 echo "\t<category name=\"".$category."\">\n";
159 foreach ($categoryItems as $item => $value) {
160 echo "\t\t<item name=\"".$item."\"><![CDATA[".StringUtil
::escapeCDATA($value)."]]></item>\n";
164 echo "\t</category>\n";
172 * Imports language items from an XML file into this language.
173 * Updates the relevant language files automatically.
176 * @param integer $packageID
177 * @param boolean $updateFiles
178 * @param boolean $updateExistingItems
180 public function updateFromXML(XML
$xml, $packageID, $updateFiles = true, $updateExistingItems = true) {
181 $xpath = $xml->xpath();
182 $usedCategories = [];
185 $categories = $xpath->query('/ns:language/ns:category');
186 foreach ($categories as $category) {
187 $usedCategories[$category->getAttribute('name')] = 0;
190 if (empty($usedCategories)) return;
192 // select existing categories
193 $conditions = new PreparedStatementConditionBuilder();
194 $conditions->add("languageCategory IN (?)", [array_keys($usedCategories)]);
196 $sql = "SELECT languageCategoryID, languageCategory
197 FROM wcf".WCF_N
."_language_category
199 $statement = WCF
::getDB()->prepareStatement($sql);
200 $statement->execute($conditions->getParameters());
201 while ($row = $statement->fetchArray()) {
202 $usedCategories[$row['languageCategory']] = $row['languageCategoryID'];
205 // create new categories
206 foreach ($usedCategories as $categoryName => $categoryID) {
207 if ($categoryID) continue;
209 $category = LanguageCategoryEditor
::create([
210 'languageCategory' => $categoryName
212 $usedCategories[$categoryName] = $category->languageCategoryID
;
215 // loop through categories to import items
217 foreach ($categories as $category) {
218 $categoryName = $category->getAttribute('name');
219 $categoryID = $usedCategories[$categoryName];
221 // loop through items
222 $elements = $xpath->query('child::*', $category);
223 foreach ($elements as $element) {
224 $itemName = $element->getAttribute('name');
225 $itemValue = $element->nodeValue
;
227 $itemData[] = $this->languageID
;
228 $itemData[] = $itemName;
229 $itemData[] = $itemValue;
230 $itemData[] = $categoryID;
231 if ($packageID) $itemData[] = ($packageID == -1) ? PACKAGE_ID
: $packageID;
235 if (!empty($itemData)) {
236 // insert/update a maximum of 50 items per run (prevents issues with max_allowed_packet)
237 $step = ($packageID) ?
5 : 4;
238 WCF
::getDB()->beginTransaction();
239 for ($i = 0, $length = count($itemData); $i < $length; $i +
= 50 * $step) {
240 $parameters = array_slice($itemData, $i, 50 * $step);
241 $repeat = count($parameters) / $step;
243 $sql = "INSERT".(!$updateExistingItems ?
" IGNORE" : "")." INTO wcf".WCF_N
."_language_item
244 (languageID, languageItem, languageItemValue, languageCategoryID". ($packageID ?
", packageID" : "") . ")
245 VALUES ".substr(str_repeat('(?, ?, ?, ?'. ($packageID ?
', ?' : '') .'), ', $repeat), 0, -2);
247 if ($updateExistingItems) {
248 if ($packageID > 0) {
249 // do not update anything if language item is owned by a different package
250 $sql .= " ON DUPLICATE KEY
251 UPDATE languageUseCustomValue = IF(packageID = ".$packageID.", IF(languageItemValue = VALUES(languageItemValue), languageUseCustomValue, 0), languageUseCustomValue),
252 languageItemValue = IF(packageID = ".$packageID.", IF(languageItemOriginIsSystem = 0, languageItemValue, VALUES(languageItemValue)), languageItemValue),
253 languageCategoryID = IF(packageID = ".$packageID.", VALUES(languageCategoryID), languageCategoryID)";
256 // skip package id check during WCFSetup (packageID = 0) or if using the ACP form (packageID = -1)
257 $sql .= " ON DUPLICATE KEY
258 UPDATE languageUseCustomValue = IF(languageItemValue = VALUES(languageItemValue), languageUseCustomValue, 0),
259 languageItemValue = IF(languageItemOriginIsSystem = 0, languageItemValue, VALUES(languageItemValue)),
260 languageCategoryID = VALUES(languageCategoryID)";
264 $statement = WCF
::getDB()->prepareStatement($sql);
265 $statement->execute($parameters);
267 WCF
::getDB()->commitTransaction();
270 // update the relevant language files
272 self
::deleteLanguageFiles($this->languageID
);
275 // delete relevant template compilations
276 $this->deleteCompiledTemplates();
280 * Deletes the language cache.
282 * @param string $languageID
283 * @param string $category
285 public static function deleteLanguageFiles($languageID = '.*', $category = '.*') {
286 if ($category != '.*') $category = preg_quote($category, '~');
287 if ($languageID != '.*') $languageID = intval($languageID);
289 DirectoryUtil
::getInstance(WCF_DIR
.'language/')->removePattern(new Regex($languageID.'_'.$category.'\.php$'));
293 * Deletes relevant template compilations.
295 public function deleteCompiledTemplates() {
297 DirectoryUtil
::getInstance(WCF_DIR
.'templates/compiled/')->removePattern(new Regex('.*_'.$this->languageID
.'_.*\.php$'));
299 DirectoryUtil
::getInstance(WCF_DIR
.'acp/templates/compiled/')->removePattern(new Regex('.*_'.$this->languageID
.'_.*\.php$'));
303 * Updates all language files of the given package id.
305 public static function updateAll() {
306 self
::deleteLanguageFiles();
310 * Takes an XML object and returns the specific language code.
314 * @throws SystemException
316 public static function readLanguageCodeFromXML(XML
$xml) {
317 $rootNode = $xml->xpath()->query('/ns:language')->item(0);
318 $attributes = $xml->xpath()->query('attribute::*', $rootNode);
319 foreach ($attributes as $attribute) {
320 if ($attribute->name
== 'languagecode') {
321 return $attribute->value
;
325 throw new SystemException("missing attribute 'languagecode' in language file");
329 * Takes an XML object and returns the specific language name.
332 * @return string language name
333 * @throws SystemException
335 public static function readLanguageNameFromXML(XML
$xml) {
336 $rootNode = $xml->xpath()->query('/ns:language')->item(0);
337 $attributes = $xml->xpath()->query('attribute::*', $rootNode);
338 foreach ($attributes as $attribute) {
339 if ($attribute->name
== 'languagename') {
340 return $attribute->value
;
344 throw new SystemException("missing attribute 'languagename' in language file");
348 * Takes an XML object and returns the specific country code.
351 * @return string country code
352 * @throws SystemException
354 public static function readCountryCodeFromXML(XML
$xml) {
355 $rootNode = $xml->xpath()->query('/ns:language')->item(0);
356 $attributes = $xml->xpath()->query('attribute::*', $rootNode);
357 foreach ($attributes as $attribute) {
358 if ($attribute->name
== 'countrycode') {
359 return $attribute->value
;
363 throw new SystemException("missing attribute 'countrycode' in language file");
367 * Imports language items from an XML file into a new or a current language.
368 * Updates the relevant language files automatically.
371 * @param integer $packageID
372 * @param Language $source
373 * @return LanguageEditor
375 public static function importFromXML(XML
$xml, $packageID, Language
$source = null) {
376 $languageCode = self
::readLanguageCodeFromXML($xml);
378 // try to find an existing language with the given language code
379 $language = LanguageFactory
::getInstance()->getLanguageByCode($languageCode);
381 // create new language
382 if ($language === null) {
383 $countryCode = self
::readCountryCodeFromXML($xml);
384 $languageName = self
::readLanguageNameFromXML($xml);
385 $language = self
::create([
386 'countryCode' => $countryCode,
387 'languageCode' => $languageCode,
388 'languageName' => $languageName
392 $sourceEditor = new LanguageEditor($source);
393 $sourceEditor->copy($language);
398 $languageEditor = new LanguageEditor($language);
399 $languageEditor->updateFromXML($xml, $packageID);
401 // return language object
402 return $languageEditor;
406 * Copies all language variables from current language to language specified as $destination.
407 * Caution: This method expects that target language does not have any items!
409 * @param Language $destination
411 public function copy(Language
$destination) {
412 $sql = "INSERT INTO wcf".WCF_N
."_language_item
413 (languageID, languageItem, languageItemValue, languageItemOriginIsSystem, languageCategoryID, packageID)
414 SELECT ?, languageItem, languageItemValue, languageItemOriginIsSystem, languageCategoryID, packageID
415 FROM wcf".WCF_N
."_language_item
416 WHERE languageID = ?";
417 $statement = WCF
::getDB()->prepareStatement($sql);
418 $statement->execute([
419 $destination->languageID
,
425 * Updates the language items of a language category.
427 * @param array $items
428 * @param LanguageCategory $category
429 * @param integer $packageID
430 * @param array $useCustom
432 public function updateItems(array $items, LanguageCategory
$category, $packageID = PACKAGE_ID
, array $useCustom = []) {
433 if (empty($items)) return;
435 // find existing language items
436 $languageItemList = new LanguageItemList();
437 $languageItemList->getConditionBuilder()->add("language_item.languageItem IN (?)", [array_keys($items)]);
438 $languageItemList->getConditionBuilder()->add("languageID = ?", [$this->languageID
]);
439 $languageItemList->readObjects();
441 foreach ($languageItemList->getObjects() as $languageItem) {
442 $languageItemEditor = new LanguageItemEditor($languageItem);
443 $languageItemEditor->update([
444 'languageCustomItemValue' => $items[$languageItem->languageItem
],
445 'languageUseCustomValue' => (isset($useCustom[$languageItem->languageItem
])) ?
1 : 0
448 // remove updated items, leaving items to be created within
449 unset($items[$languageItem->languageItem
]);
452 // create remaining items
453 if (!empty($items)) {
454 // bypass LanguageItemEditor::create() for performance reasons
455 $sql = "INSERT INTO wcf".WCF_N
."_language_item
456 (languageID, languageItem, languageItemValue, languageItemOriginIsSystem, languageCategoryID, packageID)
457 VALUES (?, ?, ?, ?, ?, ?)";
458 $statement = WCF
::getDB()->prepareStatement($sql);
460 foreach ($items as $itemName => $itemValue) {
461 $statement->execute([
466 $category->languageCategoryID
,
472 // update the relevant language files
473 self
::deleteLanguageFiles($this->languageID
, $category->languageCategory
);
475 // delete relevant template compilations
476 $this->deleteCompiledTemplates();
480 * Sets current language as default language.
482 public function setAsDefault() {
483 // remove default flag from all languages
484 $sql = "UPDATE wcf".WCF_N
."_language
486 $statement = WCF
::getDB()->prepareStatement($sql);
487 $statement->execute([0]);
489 // set current language as default language
490 $this->update(['isDefault' => 1]);
496 * Clears language cache.
498 public function clearCache() {
499 LanguageCacheBuilder
::getInstance()->reset();
503 * Enables the multilingualism feature for given languages.
505 * @param array $languageIDs
507 public static function enableMultilingualism(array $languageIDs = []) {
508 $sql = "UPDATE wcf".WCF_N
."_language
510 $statement = WCF
::getDB()->prepareStatement($sql);
511 $statement->execute([0]);
513 if (!empty($languageIDs)) {
515 $statementParameters = [];
516 foreach ($languageIDs as $languageID) {
517 if (!empty($sql)) $sql .= ',';
519 $statementParameters[] = $languageID;
522 $sql = "UPDATE wcf".WCF_N
."_language
524 WHERE languageID IN (".$sql.")";
525 $statement = WCF
::getDB()->prepareStatement($sql);
526 array_unshift($statementParameters, 1);
527 $statement->execute($statementParameters);
534 public static function resetCache() {
535 LanguageFactory
::getInstance()->clearCache();