Add proper mangament of missing language items (#3314)
authorMatthias Schmidt <gravatronics@live.com>
Thu, 4 Jun 2020 12:52:00 +0000 (14:52 +0200)
committerGitHub <noreply@github.com>
Thu, 4 Jun 2020 12:52:00 +0000 (14:52 +0200)
Close #2980

13 files changed:
com.woltlab.wcf/acpMenu.xml
wcfsetup/install/files/acp/templates/__devtoolsMissingLanguageItemStackTrace.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/devtoolsMissingLanguageItemList.tpl [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/DevtoolsMissingLanguageItemListPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/IndexPage.class.php
wcfsetup/install/files/lib/data/devtools/missing/language/item/DevtoolsMissingLanguageItem.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/devtools/missing/language/item/DevtoolsMissingLanguageItemAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/devtools/missing/language/item/DevtoolsMissingLanguageItemEditor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/devtools/missing/language/item/DevtoolsMissingLanguageItemList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/language/Language.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml
wcfsetup/setup/db/install.sql

index 865da65e58af31ac53a22107f013ff561cf2de27..1781702140c1b8b7081283f2288d90627712862d 100644 (file)
                        <permissions>admin.configuration.package.canInstallPackage</permissions>
                        <icon>fa-plus</icon>
                </acpmenuitem>
+               <acpmenuitem name="wcf.acp.menu.link.devtools.missingLanguageItem.list">
+                       <controller>wcf\acp\page\DevtoolsMissingLanguageItemListPage</controller>
+                       <parent>wcf.acp.menu.link.devtools</parent>
+                       <permissions>admin.configuration.package.canInstallPackage</permissions>
+               </acpmenuitem>
                <acpmenuitem name="wcf.acp.menu.link.devtools.notificationTest">
                        <controller>wcf\acp\page\DevtoolsNotificationTestPage</controller>
                        <parent>wcf.acp.menu.link.devtools</parent>
diff --git a/wcfsetup/install/files/acp/templates/__devtoolsMissingLanguageItemStackTrace.tpl b/wcfsetup/install/files/acp/templates/__devtoolsMissingLanguageItemStackTrace.tpl
new file mode 100644 (file)
index 0000000..39ef0cd
--- /dev/null
@@ -0,0 +1,29 @@
+<span class="icon icon16 fa-align-justify jsTooltip pointer jsOutputFormatToggle" title="{lang}wcf.acp.devtools.missingLanguageItem.stackTrace.toggleOutputFormat{/lang}" style="margin-bottom: 10px"></span>
+
+<pre>{*
+       *}{foreach from=$stackTrace item=stackEntry name=stackEntries}{*
+               *}#{$tpl[foreach][stackEntries][iteration]} {$stackEntry[file]} ({$stackEntry[line]}):
+{*             *}    {$stackEntry['class']}{$stackEntry['type']}{$stackEntry['function']}({*
+                               *}{foreach from=$stackEntry['args'] item=stackEntryArg name=stackEntryArgs}{*
+                                       *}{assign var='argType' value=$stackEntryArg|gettype}{*
+                                       *}{if $argType === 'integer' || $argType === 'double'}{*
+                                               *}{$stackEntryArg}{*
+                                       *}{elseif $argType === 'NULL'}{*
+                                               *}null{*
+                                       *}{elseif $argType === 'string'}{*
+                                               *}'{$stackEntryArg}'{*
+                                       *}{elseif $argType === 'boolean'}{*
+                                               *}{if $stackEntryArg}true{else}false{/if}{*
+                                       *}{elseif $argType === 'array'}{*
+                                               *}[{if $stackEntryArg|count > 5}{$stackEntryArg|count} items{else}{implode from=$stackEntryArg|array_keys item=stackEntryKey}{$stackEntryKey} => {/implode}{/if}]{*
+                                       *}{elseif $argType === 'object'}{*
+                                               *}{$item|get_class}{*
+                                       *}{elseif $argType === 'resource'}{*
+                                               *}resource({$item|get_resource_type}){*
+                                       *}{elseif $argType === 'resource (closed)'}{*
+                                               *}resource (closed){*
+                                       *}{/if}{if !$tpl[foreach][stackEntryArgs][last]},{/if}{*
+                               *}{/foreach}{*
+                       *})
+{*     *}{/foreach}{*
+*}</pre>
diff --git a/wcfsetup/install/files/acp/templates/devtoolsMissingLanguageItemList.tpl b/wcfsetup/install/files/acp/templates/devtoolsMissingLanguageItemList.tpl
new file mode 100644 (file)
index 0000000..9ad006a
--- /dev/null
@@ -0,0 +1,128 @@
+{include file='header' pageTitle='wcf.acp.menu.link.devtools.missingLanguageItem.list'}
+
+<header class="contentHeader">
+       <div class="contentHeaderTitle">
+               <h1 class="contentTitle">{lang}wcf.acp.menu.link.devtools.missingLanguageItem.list{/lang}{if $items} <span class="badge badgeInverse">{#$items}</span>{/if}</h1>
+       </div>
+       
+       {hascontent}
+               <nav class="contentHeaderNavigation">
+                       <ul>
+                               {content}
+                                       {if $items}
+                                               <li><a href="#" id="clearMissingLanguageItemLog" class="button"><span class="icon icon16 fa-times"></span> <span>{lang}wcf.acp.devtools.missingLanguageItem.clearLog{/lang}</span></a></li>
+                                       {/if}
+                                       
+                                       {event name='contentHeaderNavigation'}
+                               {/content}
+                       </ul>
+               </nav>
+       {/hascontent}
+</header>
+
+{hascontent}
+       <div class="paginationTop">
+               {content}
+                       {pages print=true assign=pagesLinks controller='DevtoolsMissingLanguageItemList' link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}
+               {/content}
+       </div>
+{/hascontent}
+
+{if $items}
+       <div class="section tabularBox">
+               <table class="table">
+                       <thead>
+                               <tr>
+                                       <th class="columnID{if $sortField === 'itemID'} active {@$sortOrder}{/if}" colspan="2"><a href="{link controller='DevtoolsMissingLanguageItemList'}sortField=itemID&sortOrder={if $sortField === 'itemID' && $sortOrder === 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.global.objectID{/lang}</a></th>
+                                       <th class="columnText{if $sortField === 'languageID'} active {@$sortOrder}{/if}"><a href="{link controller='DevtoolsMissingLanguageItemList'}sortField=languageID&sortOrder={if $sortField === 'languageID' && $sortOrder === 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.acp.devtools.missingLanguageItem.languageID{/lang}</a></th>
+                                       <th class="columnText{if $sortField === 'languageItem'} active {@$sortOrder}{/if}"><a href="{link controller='DevtoolsMissingLanguageItemList'}sortField=languageItem&sortOrder={if $sortField === 'languageItem' && $sortOrder === 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.acp.devtools.missingLanguageItem.languageItem{/lang}</a></th>
+                                       <th class="columnText{if $sortField === 'lastTime'} active {@$sortOrder}{/if}"><a href="{link controller='DevtoolsMissingLanguageItemList'}sortField=lastTime&sortOrder={if $sortField === 'lastTime' && $sortOrder === 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.acp.devtools.missingLanguageItem.lastTime{/lang}</a></th>
+                                       
+                                       {event name='columnHeads'}
+                               </tr>
+                       </thead>
+                       
+                       <tbody>
+                               {foreach from=$objects item=logEntry}
+                                       <tr class="jsObjectRow">
+                                               <td class="columnIcon">
+                                                       <span class="icon icon16 fa-times jsDeleteButton jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-id="{@$logEntry->getObjectID()}" data-confirm-message-html="{lang __encode=true}wcf.acp.devtools.missingLanguageItem.delete.confirmMessage{/lang}"></span>
+                                                       <span class="icon icon16 fa-align-justify jsStackTraceButton jsTooltip pointer" title="{lang}wcf.acp.devtools.missingLanguageItem.showStackTrace{/lang}" data-stack-trace="{$logEntry->getStackTrace()}"></span>
+                                               </td>
+                                               <td class="columnID">{@$logEntry->getObjectID()}</td>
+                                               <td class="columnText">{if $logEntry->getLanguage()}{$logEntry->getLanguage()}{else}{$logEntry->languageID}{/if}</td>
+                                               <td class="columnText">{$logEntry->languageItem}</td>
+                                               <td class="columnDate">{@$logEntry->lastTime|time}</td>
+                                       </tr>
+                               {/foreach}
+                       </tbody>
+               </table>
+       </div>
+
+       <footer class="contentFooter">
+               {hascontent}
+                       <div class="paginationBottom">
+                               {content}{@$pagesLinks}{/content}
+                       </div>
+               {/hascontent}
+               
+               {hascontent}
+                       <nav class="contentFooterNavigation">
+                               <ul>
+                                       {content}
+                                               {event name='contentFooterNavigation'}
+                                       {/content}
+                               </ul>
+                       </nav>
+               {/hascontent}
+       </footer>
+       
+       <script data-relocate="true">
+               require(['Ajax', 'Ui/Confirmation', 'Ui/Dialog'], function(Ajax, UiConfirmation, UiDialog) {
+                       new WCF.Action.Delete('wcf\\data\\devtools\\missing\\language\\item\\DevtoolsMissingLanguageItemAction', '.jsObjectRow');
+                       
+                       elBySelAll('.jsStackTraceButton', undefined, function(button) {
+                               button.addEventListener('click', function(event) {
+                                       var dialog = UiDialog.openStatic(
+                                               'logEntryStackTrace',
+                                               elData(event.currentTarget, 'stack-trace'),
+                                               {
+                                                       title: '{lang}wcf.acp.devtools.missingLanguageItem.stackTrace{/lang}',
+                                               }
+                                       );
+                                       
+                                       elBySel('.jsOutputFormatToggle', dialog.dialog).addEventListener('click', function(event) {
+                                               var pre = event.currentTarget.nextElementSibling;
+                                               if (pre.style.whiteSpace) {
+                                                       pre.style.whiteSpace = '';
+                                               }
+                                               else {
+                                                       pre.style.whiteSpace = 'pre-wrap';
+                                               }
+                                       });
+                               });
+                       });
+                       
+                       elById('clearMissingLanguageItemLog').addEventListener('click', function() {
+                               UiConfirmation.show({
+                                       'confirm': function() {
+                                               Ajax.apiOnce({
+                                                       data: {
+                                                               actionName: 'clearLog',
+                                                               className: 'wcf\\data\\devtools\\missing\\language\\item\\DevtoolsMissingLanguageItemAction',
+                                                       },
+                                                       success: function() {
+                                                               window.location.reload();
+                                                       }
+                                               });
+                                       },
+                                       'message': '{lang}wcf.acp.devtools.missingLanguageItem.clearLog.confirmMessage{/lang}',
+                               });
+                       });
+               });
+       </script>
+{else}
+       <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+{include file='footer'}
diff --git a/wcfsetup/install/files/lib/acp/page/DevtoolsMissingLanguageItemListPage.class.php b/wcfsetup/install/files/lib/acp/page/DevtoolsMissingLanguageItemListPage.class.php
new file mode 100644 (file)
index 0000000..0d5fb2f
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+namespace wcf\acp\page;
+use wcf\data\devtools\missing\language\item\DevtoolsMissingLanguageItemList;
+use wcf\page\SortablePage;
+
+/**
+ * Shows the list of missing language item log entries.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2020 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Acp\Page
+ * @since      5.3
+ */
+class DevtoolsMissingLanguageItemListPage extends SortablePage {
+       /**
+        * @inheritDoc
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.devtools.missingLanguageItem.list';
+       
+       /**
+        * @inheritDoc
+        */
+       public $defaultSortField = 'lastTime';
+       
+       /**
+        * @inheritDoc
+        */
+       public $defaultSortOrder = 'DESC';
+       
+       /**
+        * @inheritDoc
+        */
+       public $itemsPerPage = 50;
+       
+       /**
+        * @inheritDoc
+        */
+       public $objectListClassName = DevtoolsMissingLanguageItemList::class;
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededModules = ['ENABLE_DEVELOPER_TOOLS'];
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededPermissions = ['admin.configuration.package.canInstallPackage'];
+       
+       /**
+        * @inheritDoc
+        */
+       public $validSortFields = ['itemID', 'languageID', 'languageItem', 'lastTime'];
+}
index 9f43ca65a73ffeb93b8f34b1269a8ae7b1196b57..5d257203f20b1d5983a955a88031a65518faf86c 100755 (executable)
@@ -1,5 +1,6 @@
 <?php
 namespace wcf\acp\page;
+use wcf\data\devtools\missing\language\item\DevtoolsMissingLanguageItemList;
 use wcf\page\AbstractPage;
 use wcf\system\application\ApplicationHandler;
 use wcf\system\cache\builder\OptionCacheBuilder;
@@ -120,7 +121,15 @@ class IndexPage extends AbstractPage {
                        && file_exists(WCF_DIR . 'log/missingLanguageItems.txt')
                        && filesize(WCF_DIR . 'log/missingLanguageItems.txt') > 0
                ) {
-                       $missingLanguageItemsMTime = filemtime(WCF_DIR . 'log/missingLanguageItems.txt');
+                       $logList = new DevtoolsMissingLanguageItemList();
+                       $logList->sqlOrderBy = 'lastTime DESC';
+                       $logList->sqlLimit = 1;
+                       $logList->readObjects();
+                       $logEntry = $logList->getSingleObject();
+                       
+                       if ($logEntry !== null) {
+                               $missingLanguageItemsMTime = $logEntry->lastTime;
+                       }
                }
                
                WCF::getTPL()->assign([
diff --git a/wcfsetup/install/files/lib/data/devtools/missing/language/item/DevtoolsMissingLanguageItem.class.php b/wcfsetup/install/files/lib/data/devtools/missing/language/item/DevtoolsMissingLanguageItem.class.php
new file mode 100644 (file)
index 0000000..243582f
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+namespace wcf\data\devtools\missing\language\item;
+use wcf\data\DatabaseObject;
+use wcf\data\language\Language;
+use wcf\system\language\LanguageFactory;
+use wcf\system\WCF;
+use wcf\util\JSON;
+
+/**
+ * Represents a missing language item log entry.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2020 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Devtools\Missing\Language\Item
+ * @since      5.3
+ * 
+ * @property-read      integer         $itemID         unique id of the missing language item log entry
+ * @property-read      integer         $languageID     id of the language the missing language item was requested for
+ * @property-read      string          $languageItem   name of the missing language item
+ * @property-read      integer         $lastTime       timestamp of the last time the missing language item was requested
+ * @property-read      string          $stackTrace     stack trace of how the missing language item was requested for the last time
+ */
+class DevtoolsMissingLanguageItem extends DatabaseObject {
+       /**
+        * Returns the language the missing language item was requested for or `null` if the language
+        * does not exist anymore.
+        * 
+        * @return      null|Language
+        */
+       public function getLanguage() {
+               if ($this->languageID === null) {
+                       return null;
+               }
+               
+               return LanguageFactory::getInstance()->getLanguage($this->languageID);
+       }
+       
+       /**
+        * Returns the formatted stack trace of how the missing language item was requested for the
+        * last time.
+        * 
+        * @return      string
+        */
+       public function getStackTrace() {
+               $stackTrace = JSON::decode($this->stackTrace);
+               foreach ($stackTrace as &$stackEntry) {
+                       foreach ($stackEntry['args'] as &$stackEntryArg) {
+                               if (gettype($stackEntryArg) === 'string') {
+                                       $stackEntryArg = str_replace(["\n", "\t"], ['\n', '\t'], $stackEntryArg);
+                               }
+                       }
+                       unset($stackEntryArg);
+               }
+               unset($stackEntry);
+               
+               return WCF::getTPL()->fetch('__devtoolsMissingLanguageItemStackTrace', 'wcf', [
+                       'stackTrace' => $stackTrace,
+               ]);
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/devtools/missing/language/item/DevtoolsMissingLanguageItemAction.class.php b/wcfsetup/install/files/lib/data/devtools/missing/language/item/DevtoolsMissingLanguageItemAction.class.php
new file mode 100644 (file)
index 0000000..afc3865
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+namespace wcf\data\devtools\missing\language\item;
+use wcf\data\AbstractDatabaseObjectAction;
+use wcf\data\IDeleteAction;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+use wcf\util\JSON;
+use function wcf\functions\exception\sanitizeStacktrace;
+
+/**
+ * Executes missing language item log entry-related actions.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2020 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Devtools\Missing\Language\Item
+ * @since      5.3
+ * 
+ * @method     DevtoolsMissingLanguageItemEditor[]     getObjects()
+ * @method     DevtoolsMissingLanguageItemEditor       getSingleObject()
+ */
+class DevtoolsMissingLanguageItemAction extends AbstractDatabaseObjectAction implements IDeleteAction {
+       /**
+        * @inheritDoc
+        */
+       protected $permissionsDelete = ['admin.configuration.package.canInstallPackage'];
+       
+       /**
+        * Logs a missing language item.
+        */
+       public function logLanguageItem() {
+               $stackTraceData = sanitizeStacktrace(new \Exception(), true);
+               // Remove stack entries related to logging.
+               array_shift($stackTraceData);
+               array_shift($stackTraceData);
+               array_shift($stackTraceData);
+               $stackTrace = JSON::encode($stackTraceData);
+               
+               $sql = "INSERT INTO             wcf" . WCF_N . "_devtools_missing_language_item
+                                               (languageID, languageItem, lastTime, stackTrace)
+                       VALUES                  (?, ?, ?, ?)
+                       ON DUPLICATE KEY
+                       UPDATE                  lastTime = ?,
+                                               stackTrace = ?";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute([
+                       $this->parameters['language']->languageID,
+                       $this->parameters['languageItem'],
+                       TIME_NOW,
+                       $stackTrace,
+                       
+                       TIME_NOW,
+                       $stackTrace,
+               ]);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function validateDelete() {
+               if (!ENABLE_DEVELOPER_TOOLS || !LOG_MISSING_LANGUAGE_ITEMS) {
+                       throw new IllegalLinkException();
+               }
+               
+               parent::validateDelete();
+       }
+       
+       /**
+        * Validates the `clearLog` action.
+        */
+       public function validateClearLog() {
+               if (!ENABLE_DEVELOPER_TOOLS || !LOG_MISSING_LANGUAGE_ITEMS) {
+                       throw new IllegalLinkException();
+               }
+               
+               WCF::getSession()->checkPermissions(['admin.configuration.package.canInstallPackage']);
+       }
+       
+       /**
+        * Removes all entries from the missing language item log.
+        */
+       public function clearLog() {
+               $sql = "DELETE FROM     wcf" . WCF_N . "_devtools_missing_language_item";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute();
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/devtools/missing/language/item/DevtoolsMissingLanguageItemEditor.class.php b/wcfsetup/install/files/lib/data/devtools/missing/language/item/DevtoolsMissingLanguageItemEditor.class.php
new file mode 100644 (file)
index 0000000..4204723
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+namespace wcf\data\devtools\missing\language\item;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit missing language item log entry.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2020 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Devtools\Missing\Language\Item
+ * @since      5.3
+ *
+ * @method static      DevtoolsMissingLanguageItem     create(array $parameters = [])
+ * @method             DevtoolsMissingLanguageItem     getDecoratedObject()
+ * @mixin              DevtoolsMissingLanguageItem
+ */
+class DevtoolsMissingLanguageItemEditor extends DatabaseObjectEditor {
+       /**
+        * @inheritDoc
+        */
+       protected static $baseClass = DevtoolsMissingLanguageItem::class;
+}
diff --git a/wcfsetup/install/files/lib/data/devtools/missing/language/item/DevtoolsMissingLanguageItemList.class.php b/wcfsetup/install/files/lib/data/devtools/missing/language/item/DevtoolsMissingLanguageItemList.class.php
new file mode 100644 (file)
index 0000000..bd53415
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+namespace wcf\data\devtools\missing\language\item;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of missing language item log entries.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2020 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\Devtools\Missing\Language\Item
+ * @since      5.3
+ * 
+ * @method     DevtoolsMissingLanguageItem             current()
+ * @method     DevtoolsMissingLanguageItem[]           getObjects()
+ * @method     DevtoolsMissingLanguageItem             getSingleObject()
+ * @method     DevtoolsMissingLanguageItem|null        search($objectID)
+ * @property   DevtoolsMissingLanguageItem[]           $objects
+ */
+class DevtoolsMissingLanguageItemList extends DatabaseObjectList {}
index ec8d2ea38ae2b8199ee7a5e7bc87578c0c336280..2e8a8379454a69b0c259a3e8b87ba42de91d519e 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 namespace wcf\data\language;
 use wcf\data\DatabaseObject;
+use wcf\data\devtools\missing\language\item\DevtoolsMissingLanguageItemAction;
 use wcf\system\language\LanguageFactory;
 use wcf\system\WCF;
 use wcf\util\StringUtil;
@@ -129,11 +130,10 @@ class Language extends DatabaseObject {
                        LOG_MISSING_LANGUAGE_ITEMS &&
                        preg_match('~^([a-zA-Z0-9-_]+\.)+[a-zA-Z0-9-_]+$~', $item)
                ) {
-                       $logFile = WCF_DIR . 'log/missingLanguageItems.txt';
-                       \wcf\functions\exception\logThrowable(
-                               new \Exception("Missing language item '{$item}'."),
-                               $logFile
-                       );
+                       (new DevtoolsMissingLanguageItemAction([], 'logLanguageItem', [
+                               'language' => $this,
+                               'languageItem' => $item,
+                       ]))->executeAction();
                }
                
                // return plain input
@@ -167,11 +167,10 @@ class Language extends DatabaseObject {
                        $staticItem === $item &&
                        preg_match('~^([a-zA-Z0-9-_]+\.)+[a-zA-Z0-9-_]+$~', $item)
                ) {
-                       $logFile = WCF_DIR . 'log/missingLanguageItems.txt';
-                       \wcf\functions\exception\logThrowable(
-                               new \Exception("Missing language item '{$item}'."),
-                               $logFile
-                       );
+                       (new DevtoolsMissingLanguageItemAction([], 'logLanguageItem', [
+                               'language' => $this,
+                               'languageItem' => $item,
+                       ]))->executeAction();
                }
                
                return $staticItem;
index c694939a0c41f67aa5466ead2207897d5d1c828c..714e010d033f6486a59fc74dbf6374950260811d 100644 (file)
                <item name="wcf.acp.devtools.project.edit.error.brokenPath"><![CDATA[Es existiert keine <kbd>package.xml</kbd> im angegebenen Pfad. Die Paket-Dateien wurden entweder gelöscht oder verschoben.]]></item>
                <item name="wcf.acp.devtools.project.edit.warning.missingElements"><![CDATA[Die folgenden Informationen fehlen in der <kbd>package.xml</kbd>-Datei: {implode from=$missingElements item=missingElement}<kbd>{$missingElement}</kbd>{/implode}.]]></item>
                <item name="wcf.acp.devtools.project.license"><![CDATA[Lizenz]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.languageID"><![CDATA[Sprache]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.languageItem"><![CDATA[Name des Texts]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.lastTime"><![CDATA[Zeitpunkt des letzten Auftretens]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.delete.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} den Eintrag wirklich löschen?]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.stackTrace"><![CDATA[Stacktrace]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.showStackTrace"><![CDATA[Stacktrace anzeigen]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.clearLog"><![CDATA[Alle Einträge löschen]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.clearLog.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} wirklich alle Einträge löschen?]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.stackTrace.toggleOutputFormat"><![CDATA[Zeilenumbrüche einschalten/ausschalten]]></item>
        </category>
        <category name="wcf.acp.email">
                <item name="wcf.acp.email.smtp.test"><![CDATA[SMTP-Verbindungstest]]></item>
@@ -928,7 +937,7 @@ ACHTUNG: Die oben genannten Meldungen sind stark gekürzt. Sie können Details z
                <item name="wcf.acp.index.woltlab.pluginStore"><![CDATA[Plugin-Store]]></item>
                <item name="wcf.acp.index.tinyBuild"><![CDATA[Die Seitenbeschleunigung für Gäste verbessert die Ladezeiten für Besucher und Suchmaschinen, es wird empfohlen diese <a href="{link controller='Option' id=1 optionName="visitor_use_tiny_build"}#category_module.system{/link}">zu aktivieren</a>.]]></item>
                <item name="wcf.acp.index.recaptchaWithoutKey"><![CDATA[Die Nutzung von reCAPTCHA ohne einen individuellen Website-Schlüssel wird von Google nicht mehr unterstützt.<br><br>Für eine weitere Nutzung {if LANGUAGE_USE_INFORMAL_VARIANT}musst du{else}müssen Sie{/if} <a href="{$recaptchaKeyLink}">einen Schlüssel in den Optionen hinterlegen</a>, unterhalb des Eingabefeldes befindet sich eine Anleitung zum Anfordern des Schlüssels.]]></item>
-               <item name="wcf.acp.index.missingLanguageItems"><![CDATA[Es wurden fehlende Sprachvariablen protokolliert (zuletzt: {@$missingLanguageItemsMTime|time}). {if LANGUAGE_USE_INFORMAL_VARIANT}Überprüfe{else}Überprüfen Sie{/if} die Datei <kbd>{'WCF_DIR'|constant}log/missingLanguageItems.txt</kbd> für weitere Informationen.]]></item>
+               <item name="wcf.acp.index.missingLanguageItems"><![CDATA[Es wurden fehlende Sprachvariablen protokolliert (zuletzt: {@$missingLanguageItemsMTime|time}). {if LANGUAGE_USE_INFORMAL_VARIANT}Überprüfe{else}Überprüfen Sie{/if} die <a href="{link controller='DevtoolsMissingLanguageItemList'}{/link}">Liste der fehlenden Texten</a> für weitere Informationen.]]></item>
        </category>
        <category name="wcf.acp.label">
                <item name="wcf.acp.label.add"><![CDATA[Label hinzufügen]]></item>
@@ -1186,6 +1195,7 @@ ACHTUNG: Die oben genannten Meldungen sind stark gekürzt. Sie können Details z
                <item name="wcf.acp.menu.link.reactionType.edit"><![CDATA[Reaktions-Typ bearbeiten]]></item>
                <item name="wcf.acp.menu.link.language.item.add"><![CDATA[Text hinzufügen]]></item>
                <item name="wcf.acp.menu.link.systemCheck"><![CDATA[Systemüberprüfung]]></item>
+               <item name="wcf.acp.menu.link.devtools.missingLanguageItem.list"><![CDATA[Fehlende Texte]]></item>
        </category>
        <category name="wcf.acp.modificationLog">
                <item name="wcf.acp.modificationLog.list"><![CDATA[Globales Änderungsprotokoll]]></item>
index 0ad6aaa0d4b4c4246a283a1ae3a627e4e39e7cb1..9c7e207d20a7f8f161f5216be338d0aed6fdd1c4 100644 (file)
                <item name="wcf.acp.devtools.project.edit.error.brokenPath"><![CDATA[There is no <kbd>package.xml</kbd> in the given directory. The package files have either been deleted or moved.]]></item>
                <item name="wcf.acp.devtools.project.edit.warning.missingElements"><![CDATA[The following information is missing from the <kbd>package.xml</kbd> file: {implode from=$missingElements item=missingElement}<kbd>{$missingElement}</kbd>{/implode}.]]></item>
                <item name="wcf.acp.devtools.project.license"><![CDATA[License]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.languageID"><![CDATA[Language]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.languageItem"><![CDATA[Name of Phrase]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.lastTime"><![CDATA[Time of Last Occurence]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.delete.confirmMessage"><![CDATA[Do you really want to delete the entry?]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.stackTrace"><![CDATA[Stacktrace]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.showStackTrace"><![CDATA[Show Stacktrace]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.clearLog"><![CDATA[Delete all entries]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.clearLog.confirmMessage"><![CDATA[Do you really want to delete all entries?]]></item>
+               <item name="wcf.acp.devtools.missingLanguageItem.stackTrace.toggleOutputFormat"><![CDATA[Toggle line breaks]]></item>
        </category>
        <category name="wcf.acp.email">
                <item name="wcf.acp.email.smtp.test"><![CDATA[SMTP Connection Test]]></item>
@@ -905,7 +914,7 @@ ATTENTION: The messages listed above are greatly shortened. You can view details
                <item name="wcf.acp.index.woltlab.pluginStore"><![CDATA[Plugin Store]]></item>
                <item name="wcf.acp.index.tinyBuild"><![CDATA[The accelerated guest view improves the page responsiveness and loading times for both visitors and search engines alike, please consider <a href="{link controller='Option' id=1 optionName="visitor_use_tiny_build"}#category_module.system{/link}">enabling it</a>.]]></item>
                <item name="wcf.acp.index.recaptchaWithoutKey"><![CDATA[Using reCAPTCHA without an individual website key is no longer supported by Google.<br><br>For further use you need to <a href="{$recaptchaKeyLink}">provide a key in your options</a>, please follow the instructions below the input field to obtain a key.]]></item>
-               <item name="wcf.acp.index.missingLanguageItems"><![CDATA[Missing language items have been detected (last time: {@$missingLanguageItemsMTime|time}). Check the file <kbd>{'WCF_DIR'|constant}log/missingLanguageItems.txt</kbd> for more information.]]></item>
+               <item name="wcf.acp.index.missingLanguageItems"><![CDATA[Missing language items have been detected (last time: {@$missingLanguageItemsMTime|time}). Check the <a href="{link controller='DevtoolsMissingLanguageItemList'}{/link}">list of missing phrases</a> for more information.]]></item>
        </category>
        <category name="wcf.acp.label">
                <item name="wcf.acp.label.add"><![CDATA[Add Label]]></item>
@@ -1163,6 +1172,7 @@ ATTENTION: The messages listed above are greatly shortened. You can view details
                <item name="wcf.acp.menu.link.reactionType.edit"><![CDATA[Edit Reaction Type]]></item>
                <item name="wcf.acp.menu.link.language.item.add"><![CDATA[Add Phrase]]></item>
                <item name="wcf.acp.menu.link.systemCheck"><![CDATA[System Check]]></item>
+               <item name="wcf.acp.menu.link.devtools.missingLanguageItem.list"><![CDATA[Missing Phrases]]></item>
        </category>
        <category name="wcf.acp.modificationLog">
                <item name="wcf.acp.modificationLog.list"><![CDATA[Global Modification Log]]></item>
index 50def4fe5e05d2be76be792424bd2752799bff95..60e3f1c5573ebe4b0b8497e95be855a5e2932cd6 100644 (file)
@@ -542,6 +542,17 @@ CREATE TABLE wcf1_devtools_project (
        UNIQUE KEY name (name)
 );
 
+DROP TABLE IF EXISTS wcf1_devtools_missing_language_item;
+CREATE TABLE wcf1_devtools_missing_language_item (
+       itemID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+       languageID INT(10),
+       languageItem VARCHAR(191) NOT NULL,
+       lastTime INT(10) NOT NULL,
+       stackTrace MEDIUMTEXT NOT NULL,
+       
+       UNIQUE KEY (languageID, languageItem)
+);
+
 DROP TABLE IF EXISTS wcf1_edit_history_entry;
 CREATE TABLE wcf1_edit_history_entry (
        entryID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
@@ -1939,6 +1950,8 @@ ALTER TABLE wcf1_cronjob ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (pa
 
 ALTER TABLE wcf1_cronjob_log ADD FOREIGN KEY (cronjobID) REFERENCES wcf1_cronjob (cronjobID) ON DELETE CASCADE;
 
+ALTER TABLE wcf1_devtools_missing_language_item ADD FOREIGN KEY (languageID) REFERENCES wcf1_language (languageID) ON DELETE SET NULL;
+
 ALTER TABLE wcf1_edit_history_entry ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE;
 ALTER TABLE wcf1_edit_history_entry ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE SET NULL;
 ALTER TABLE wcf1_edit_history_entry ADD FOREIGN KEY (obsoletedByUserID) REFERENCES wcf1_user (userID) ON DELETE SET NULL;