diff --git a/_build/lexicon/README.md b/_build/lexicon/README.md new file mode 100644 index 00000000000..c34f701c10d --- /dev/null +++ b/_build/lexicon/README.md @@ -0,0 +1,34 @@ +# Core lexicon checker + +Tooling for [issue #14512](https://github.com/modxcms/revolution/issues/14512). + +## Run + +Requires `_build/build.config.php` and `_build/build.properties.php` (see the sample files). + +```bash +php _build/lexicon/checklexicon.php [language] [excludedFolders] +``` + +Default language is `en`. + +## Reports + +Generated files are gitignored (`_*.php` in this folder): + +| File | Meaning | +|------|---------| +| `_missing.php` | Keys referenced in core but not defined | +| `_superfluous.php` | Keys defined but not found by static scan | +| `_variable.php` | Dynamic / concatenated key usage | +| `_duplicates_identical.php` | Same key + same value in multiple **core** topics | +| `_duplicates_conflict.php` | Same key with **different** values across **core** topics | + +Cross-topic duplicate reports cover `core/lexicon/{lang}/` only. Setup language files are still scanned for missing/superfluous usage checks. + +## Safety notes + +- `_superfluous.php` is a **candidate list**, not an automatic delete list. Extras may call core lexicon keys that the scanner never sees. +- Prefer removing **identical** cross-topic duplicates first (keep the copy in `default` when the manager always loads that topic). +- Resolve `_duplicates_conflict.php` carefully: conflicting values usually mean a key is overloaded and should be renamed, not deleted. +- The CLI exits `0` after writing reports; use the report files (especially `_duplicates_conflict.php`) for review gates. diff --git a/_build/lexicon/checklexicon.class.php b/_build/lexicon/checklexicon.class.php new file mode 100644 index 00000000000..ff53ea6f681 --- /dev/null +++ b/_build/lexicon/checklexicon.class.php @@ -0,0 +1,568 @@ +modx = $modx; + $this->language = isset($options['language']) ? $options['language'] : 'en'; + $this->excludedFolders = array_merge( + $this->excludedFolders, + isset($options['excludedFolders']) ? array_map('trim', explode(',', $options['excludedFolders'])) : array() + ); + $this->scanPath = isset($options['scanPath']) ? $options['scanPath'] : MODX_BASE_PATH; + $this->lexiconPath = MODX_CORE_PATH . 'lexicon/'; + $this->setupLexiconPath = MODX_BASE_PATH . 'setup/lang/'; + } + + public function process() + { + $this->addKeys(); + + $coreTopics = self::loadLexiconTopics($this->lexiconPath . $this->language . '/'); + if ($coreTopics === false) { + $path = $this->lexiconPath . $this->language . '/'; + return array( + 'success' => false, + 'message' => 'Could not load the lexicons in the language folder "' . $path . '"!' + ); + } + + $setupTopics = self::loadLexiconTopics($this->setupLexiconPath . $this->language . '/'); + if ($setupTopics === false) { + $path = $this->setupLexiconPath . $this->language . '/'; + return array( + 'success' => false, + 'message' => 'Could not load the lexicons in the setup language folder "' + . $path . '"!' + ); + } + + $lexiconEntries = array_merge( + self::flattenTopicEntries($coreTopics), + self::flattenTopicEntries($setupTopics) + ); + + $this->missingKeys = array_diff($this->languageKeys, array_keys($lexiconEntries)); + $usedKeys = array_intersect($this->languageKeys, array_keys($lexiconEntries)); + $this->superfluousKeys = array_diff(array_keys($lexiconEntries), $usedKeys); + + $duplicates = self::findCrossTopicDuplicates($coreTopics); + $this->duplicateIdentical = $duplicates['identical']; + $this->duplicateConflict = $duplicates['conflict']; + + $msg = array(); + if ($result = $this->writeKeys('missing')) { + $msg[] = $result; + } + if ($result = $this->writeKeys('superfluous')) { + $msg[] = $result; + } + if ($result = $this->writeKeys('variable')) { + $msg[] = $result; + } + if ($result = $this->writeDuplicateKeys('identical')) { + $msg[] = $result; + } + if ($result = $this->writeDuplicateKeys('conflict')) { + $msg[] = $result; + } + if (empty($msg)) { + $msg = 'Every lexicon entry is available and no variable keys or cross-topic duplicates are used!'; + } else { + $msg = implode("\n", $msg); + } + + return [ + 'success' => true, + 'message' => $msg + ]; + } + + /** + * Load lexicon topic files into topic => [key => value]. + * + * @param string $path + * @return array|false + */ + public static function loadLexiconTopics($path) + { + if (!is_dir($path)) { + return false; + } + + $topics = []; + $iterator = new \DirectoryIterator($path); + foreach ($iterator as $current) { + if ($current->isDot() || strpos($current->getFilename(), 'inc.php') === false) { + continue; + } + $_lang = []; + try { + include $current->getRealPath(); + } catch (\Exception $e) { + continue; + } + $topic = basename($current->getFilename(), '.inc.php'); + $topics[$topic] = $_lang; + } + ksort($topics); + + return $topics; + } + + /** + * Flatten topic maps into a single key => value map (later topics overwrite). + * + * @param array $entriesByTopic + * @return array + */ + public static function flattenTopicEntries(array $entriesByTopic) + { + $flat = []; + foreach ($entriesByTopic as $entries) { + $flat = array_merge($flat, $entries); + } + + return $flat; + } + + /** + * Find keys defined in more than one topic. + * + * @param array $entriesByTopic topic => [key => value] + * @return array{identical: array, conflict: array} + */ + public static function findCrossTopicDuplicates(array $entriesByTopic) + { + $byKey = []; + foreach ($entriesByTopic as $topic => $entries) { + foreach ($entries as $key => $value) { + $byKey[$key][$topic] = $value; + } + } + + $identical = []; + $conflict = []; + foreach ($byKey as $key => $topics) { + if (count($topics) < 2) { + continue; + } + $values = array_unique(array_values($topics)); + $row = [ + 'key' => $key, + 'topics' => array_keys($topics), + 'values' => $topics, + ]; + if (count($values) === 1) { + $identical[$key] = $row; + } else { + $conflict[$key] = $row; + } + } + ksort($identical); + ksort($conflict); + + return [ + 'identical' => $identical, + 'conflict' => $conflict, + ]; + } + + /** + * Add used lexicon keys + */ + private function addKeys() + { + $directory = new \RecursiveDirectoryIterator($this->scanPath, \RecursiveDirectoryIterator::SKIP_DOTS); + $filter = new \RecursiveCallbackFilterIterator($directory, function ($current, $key, $iterator) { + /** @var \RecursiveDirectoryIterator $current */ + if ($current->getFilename()[0] === '.') { + return false; + } + if ($current->isDir()) { + return !in_array($current->getFilename(), $this->excludedFolders); + } else { + return $this->allowedFiletype($current); + } + }); + $iterator = new \RecursiveIteratorIterator($filter); + + foreach ($iterator as $path => $current) { + $this->addPhpKeys($path); + $this->addJsKeys($path); + $this->addChunkKeys($path); + $this->addSmartyKeys($path); + } + $this->addSettingKeys(); + $this->addMenuKeys(); + $this->addWidgetKeys(); + $this->addPermissionKeys(); + + $this->languageKeys = array_unique($this->languageKeys); + sort($this->languageKeys); + } + + /** + * @param \RecursiveDirectoryIterator $file + * @return bool + */ + private function allowedFiletype($file) + { + $pathinfo = pathinfo($file->getFilename()); + return ($file->isFile() && isset($pathinfo['extension']) && ( + $pathinfo['extension'] == 'php' || + $pathinfo['extension'] == 'js' || + $pathinfo['extension'] == 'html' || + $pathinfo['extension'] == 'tpl' || + $pathinfo['basename'] == 'config.json' + ) && + strpos($pathinfo['basename'], 'min.js') === false && + strpos($pathinfo['basename'], 'ext-') !== 0 + ) ? true : false; + } + + /** + * @param string $filename + */ + private function addPhpKeys($filename) + { + $fileContent = file_get_contents($filename); + $results = []; + preg_match_all('/(modx|xpdo)->lexicon\((?["\'])(.*?)\k\s*[,)]/m', $fileContent, $results); + if (is_array($results[3])) { + foreach ($results[3] as $result) { + if ( + substr($result, -1) !== '.' && + substr($result, -1) !== '_' + ) { + if ( + strpos($result, '$') === false + ) { + $this->languageKeys[] = $result; + } else { + $this->variableKeys[] = $result; + } + } + } + } + } + + /** + * @param string $filename + */ + private function addJsKeys($filename) + { + $fileContent = file_get_contents($filename); + $results = []; + preg_match_all('/_\((?[\'"])(.*?)\k\s*[,)]/m', $fileContent, $results); + if (is_array($results[2])) { + foreach ($results[2] as $result) { + if ( + substr($result, -1) !== '.' && + substr($result, -1) !== '_' + ) { + if ( + strpos($result, '+') === false + ) { + $this->languageKeys[] = $result; + } else { + $this->variableKeys[] = $result; + } + } + } + } + preg_match_all('/(createDelegate)\(.*?,\s+\[(?[\'"])(.*?)\k/m', $fileContent, $results); + if (is_array($results[3])) { + foreach ($results[3] as $result) { + if ( + substr($result, -1) !== '.' && + substr($result, -1) !== '_' + ) { + if ( + strpos($result, '+') === false + ) { + $this->languageKeys[] = $result; + } else { + $this->variableKeys[] = $result; + } + } + } + } + } + + /** + * @param string $filename + */ + private function addChunkKeys($filename) + { + $fileContent = file_get_contents($filename); + $results = []; + preg_match_all('/\[\[%(.*?)[?\]]/m', $fileContent, $results); + if (is_array($results[1])) { + foreach ($results[1] as $result) { + if ( + substr($result, -1) !== '.' && + substr($result, -1) !== '_' + ) { + if ( + strpos($result, '[[+') === false + ) { + $this->languageKeys[] = $result; + } else { + $this->variableKeys[] = $result; + } + } + } + } + } + + /** + * @param string $filename + */ + private function addSmartyKeys($filename) + { + $fileContent = file_get_contents($filename); + $results = []; + preg_match_all('/\$_lang\.(.*?)[ |}]/m', $fileContent, $results); + if (is_array($results[1])) { + foreach ($results[1] as $result) { + if ( + substr($result, -1) !== '.' && + substr($result, -1) !== '_' + ) { + $this->languageKeys[] = $result; + } + } + } + } + + private function addSettingKeys() + { + $settings = []; + $xpdo = &$this->modx; + if (file_exists(MODX_BASE_PATH . '_build/data/transport.core.system_settings.php')) { + $settings = include MODX_BASE_PATH . '_build/data/transport.core.system_settings.php'; + } + + foreach ($settings as $setting) { + $this->languageKeys[] = 'setting_' . $setting->get('key'); + $this->languageKeys[] = 'setting_' . $setting->get('key') . '_desc'; + if ( + !in_array($setting->get('area'), [ + 'authentication', 'caching', 'file', 'furls', 'gateway', + 'language', 'manager', 'session', 'site', 'system' + ]) + ) { + $this->languageKeys[] = 'area_' . $setting->get('area'); + } + } + } + + private function addMenuKeys() + { + $menus = []; + $xpdo = &$this->modx; + if (file_exists(MODX_BASE_PATH . '_build/data/transport.core.menus.php')) { + $menus = include MODX_BASE_PATH . '_build/data/transport.core.menus.php'; + } + + $xpdo->setLogLevel(xPDO::LOG_LEVEL_FATAL); + foreach ($menus as $menu) { + $this->addMenuKey($menu); + } + $xpdo->setLogLevel(xPDO::LOG_LEVEL_INFO); + } + + /** + * @param modMenu $menu + */ + private function addMenuKey(modMenu $menu) + { + $this->languageKeys[] = $menu->get('text'); + $this->languageKeys[] = $menu->get('description'); + $children = $menu->getMany('Children'); + foreach ($children as $child) { + $this->addMenuKey($child); + } + } + + private function addWidgetKeys() + { + $widgets = []; + $xpdo = &$this->modx; + if (file_exists(MODX_BASE_PATH . '_build/data/transport.core.dashboard_widgets.php')) { + $widgets = include MODX_BASE_PATH . '_build/data/transport.core.dashboard_widgets.php'; + } + + foreach ($widgets as $widget) { + $this->languageKeys[] = $widget->get('name'); + $this->languageKeys[] = $widget->get('description'); + } + } + + private function addPermissionKeys() + { + $permissionsPath = MODX_BASE_PATH . '_build/data/permissions/'; + $directory = new \RecursiveDirectoryIterator( + $permissionsPath, + \RecursiveDirectoryIterator::SKIP_DOTS + ); + $filter = new \RecursiveCallbackFilterIterator($directory, function ($current, $key, $iterator) { + /** @var \RecursiveDirectoryIterator $current */ + if ($current->getFilename()[0] === '.') { + return false; + } + if ($current->isDir()) { + return !in_array($current->getFilename(), $this->excludedFolders); + } else { + $pathinfo = pathinfo($current->getFilename()); + return ($current->isFile() && isset($pathinfo['extension']) && + $pathinfo['extension'] == 'php' + ) ? true : false; + } + }); + $iterator = new \RecursiveIteratorIterator($filter); + + $xpdo = &$this->modx; + foreach ($iterator as $path => $current) { + try { + $permissions = include $current->getRealPath(); + } catch (\Exception $e) { + $permissions = []; + } + foreach ($permissions as $permission) { + $this->languageKeys[] = $permission->get('description'); + } + } + } + + /** + * @param string $type + * @return bool|string + */ + private function writeKeys($type) + { + $folder = dirname(__FILE__); + switch ($type) { + case 'superfluous': + $keys = &$this->superfluousKeys; + $keysFile = '_superfluous.php'; + break; + case 'variable': + $keys = &$this->variableKeys; + $keysFile = '_variable.php'; + break; + default: + $type = 'missing'; + $keys = &$this->missingKeys; + $keysFile = '_missing.php'; + break; + } + sort($keys); + if (!empty($keys)) { + $handle = fopen($folder . '/' . $keysFile, 'w'); + if ($handle) { + fwrite($handle, "duplicateConflict; + $keysFile = '_duplicates_conflict.php'; + } else { + $type = 'identical'; + $rows = $this->duplicateIdentical; + $keysFile = '_duplicates_identical.php'; + } + + $reportPath = $folder . '/' . $keysFile; + if (empty($rows)) { + if (file_exists($reportPath)) { + unlink($reportPath); + } + return false; + } + + $handle = fopen($reportPath, 'w'); + if (!$handle) { + return 'Cannot write to file: ' . $keysFile; + } + + fwrite($handle, "language}.\n"); + fwrite($handle, " * Generated by checklexicon.php — do not commit.\n */\n"); + foreach ($rows as $row) { + $topics = implode(', ', $row['topics']); + if ($type === 'conflict') { + $parts = []; + foreach ($row['values'] as $topic => $value) { + $parts[] = $topic . '=' . var_export($value, true); + } + fwrite($handle, "// {$row['key']} @ {$topics}\n"); + fwrite($handle, '// ' . implode(' | ', $parts) . "\n"); + } else { + $value = var_export(reset($row['values']), true); + fwrite($handle, "// {$row['key']} @ {$topics} = {$value}\n"); + } + } + fclose($handle); + + return 'The ' . $type . ' cross-topic duplicates could be found in the file ' + . $keysFile . ' in the folder "' . $folder . '".'; + } +} diff --git a/_build/lexicon/checklexicon.php b/_build/lexicon/checklexicon.php index 57869e9e140..c3753972abf 100755 --- a/_build/lexicon/checklexicon.php +++ b/_build/lexicon/checklexicon.php @@ -1,9 +1,18 @@ XPDO_TABLE_PREFIX, xPDO::OPT_CACHE_PATH => MODX_CORE_PATH . 'cache/', ), array( - PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, ) ); $cacheManager = $xpdo->getCacheManager(); @@ -97,14 +101,11 @@ $xpdo->log(xPDO::LOG_LEVEL_INFO, 'Start lexicon check...'); flush(); -/* language can be defined for checking language specific lexicons - en default means checks the english lexicons */ $language = 'en'; if (!empty($argv) && $argc > 1) { $language = $argv[1]; } -/* excluded folders can be defined for excluding specific folders with a comma separated list */ $excluded = ''; if (!empty($argv) && $argc > 2) { $excluded = $argv[2]; @@ -126,467 +127,4 @@ echo "\nExecution time: {$totalTime}\n"; flush(); -exit (); - -class CheckLexicon -{ - public $scanPath = null; - public $lexiconPath = null; - public $setupLexiconPath = null; - - private $language = null; - private $excludedFolders = array('_build', 'cache', 'packages', 'node_modules', 'components', 'vendor'); - - private $languageKeys = array(); - private $missingKeys = array(); - private $superfluousKeys = array(); - private $variableKeys = array(); - - private $invalidLexicons = array(); - - private $modx = null; - - public function __construct(xPDO $modx, $options) - { - $this->modx = $modx; - $this->language = isset($options['language']) ? $options['language'] : 'en'; - $this->excludedFolders = array_merge($this->excludedFolders, isset($options['excludedFolders']) ? array_map('trim', explode(',', $options['excludedFolders'])) : array()); - $this->scanPath = isset($options['scanPath']) ? $options['scanPath'] : MODX_BASE_PATH; - $this->lexiconPath = MODX_CORE_PATH . 'lexicon/'; - $this->setupLexiconPath = MODX_BASE_PATH . 'setup/lang/'; - } - - public function process() - { - $this->addKeys(); - - $lexiconEntries = $this->loadLexicons(); - if (!is_array($lexiconEntries)) { - return array( - 'success' => false, - 'message' => $lexiconEntries - ); - } - - $this->missingKeys = array_diff($this->languageKeys, array_keys($lexiconEntries)); - $usedKeys = array_intersect($this->languageKeys, array_keys($lexiconEntries)); - $this->superfluousKeys = array_diff(array_keys($lexiconEntries), $usedKeys); - - $msg = array(); - if ($result = $this->writeKeys('missing')) { - $msg[] = $result; - } - if ($result = $this->writeKeys('superfluous')) { - $msg[] = $result; - } - if ($result = $this->writeKeys('variable')) { - $msg[] = $result; - } - if (empty($msg)) { - $msg = 'Every lexicon entry is available and no variable keys are used!'; - } else { - $msg = implode("\n", $msg); - } - - return [ - 'success' => true, - 'message' => $msg - ]; - - } - - - /** - * Load package lexicons - * - * @return bool|array - */ - private function loadLexicons() - { - $lexicons = []; - - if (!$lexicon = $this->loadLexiconFiles($this->lexiconPath . $this->language . '/')) { - return 'Could not load the lexicons in the language folder "' . $this->lexiconPath . $this->language . '/' . '"!'; - } else { - $lexicons = array_merge($lexicons, $lexicon); - } - - if (!$lexicon = $this->loadLexiconFiles($this->setupLexiconPath . $this->language . '/')) { - return 'Could not load the lexicons in the setup language folder "' . $this->lexiconPath . $this->language . '/' . '"!'; - } else { - $lexicons = array_merge($lexicons, $lexicon); - } - - return $lexicons; - } - - /** - * Load lexicon files - * - * @param $path string - * @return array|bool - */ - private function loadLexiconFiles($path) - { - if (file_exists($path)) { - $iterator = new \DirectoryIterator($path); - $_lang = []; - foreach ($iterator as $path => $current) { - if (strpos($current->getFilename(), 'inc.php') !== false) { - try { - include $current->getRealPath(); - } catch (Exception $e) { - $this->invalidLexicons[] = $current->getFilename(); - } - } - } - return $_lang; - } else { - return false; - } - } - - /** - * Add used lexicon keys - */ - private function addKeys() - { - $directory = new \RecursiveDirectoryIterator($this->scanPath, \RecursiveDirectoryIterator::SKIP_DOTS); - // Filter files ... - $filter = new \RecursiveCallbackFilterIterator($directory, function ($current, $key, $iterator) { - /** @var \RecursiveDirectoryIterator $current */ - // ... for files starting with a dot - if ($current->getFilename()[0] === '.') { - return false; - } - if ($current->isDir()) { - // ... for excluded folders - return !in_array($current->getFilename(), $this->excludedFolders); - } else { - // ... for allowed file types - return $this->allowedFiletype($current); - } - }); - $iterator = new \RecursiveIteratorIterator($filter); - - foreach ($iterator as $path => $current) { - $this->addPhpKeys($path); - $this->addJsKeys($path); - $this->addChunkKeys($path); - $this->addSmartyKeys($path); - } - $this->addSettingKeys(); - $this->addMenuKeys(); - $this->addWidgetKeys(); - $this->addPermissionKeys(); - - $this->languageKeys = array_unique($this->languageKeys); - sort($this->languageKeys); - } - - /** - * Check for allowed file types - * - * @param \RecursiveDirectoryIterator $file - * @return bool - */ - private function allowedFiletype($file) - { - $pathinfo = pathinfo($file->getFilename()); - return ($file->isFile() && isset($pathinfo['extension']) && ( - $pathinfo['extension'] == 'php' || - $pathinfo['extension'] == 'js' || - $pathinfo['extension'] == 'html' || - $pathinfo['extension'] == 'tpl' || - $pathinfo['basename'] == 'config.json' - ) && - strpos($pathinfo['basename'], 'min.js') === false && - strpos($pathinfo['basename'], 'ext-') !== 0 - ) ? true : false; - } - - /** - * Add lexicon calls in php files: - * modx->lexicon('whatever', - * - * @param string $filename - */ - private function addPhpKeys($filename) - { - $fileContent = file_get_contents($filename); - $results = []; - preg_match_all('/(modx|xpdo)->lexicon\((?["\'])(.*?)\k\s*[,)]/m', $fileContent, $results); - if (is_array($results[3])) { - foreach ($results[3] as $result) { - // Don't add lexicon keys that end with a dot or an underscore - if (substr($result, -1) !== '.' && - substr($result, -1) !== '_' - ) { - // Check, if the key contains a variable - if (strpos($result, '$') === false - ) { - $this->languageKeys[] = $result; - } else { - $this->variableKeys[] = $result; - } - } - } - } - } - - /** - * Add lexicon calls in javascript files: - * _('whatever', - * createDelegate(this, ['whatever' - * - * @param string $filename - */ - private function addJsKeys($filename) - { - $fileContent = file_get_contents($filename); - $results = []; - preg_match_all('/_\((?[\'"])(.*?)\k\s*[,)]/m', $fileContent, $results); - if (is_array($results[2])) { - foreach ($results[2] as $result) { - // Don't add lexicon keys that ends with a dot or an underscore - if (substr($result, -1) !== '.' && - substr($result, -1) !== '_' - ) { - // Check, if the key is concatenated - if (strpos($result, '+') === false - ) { - $this->languageKeys[] = $result; - } else { - $this->variableKeys[] = $result; - } - } - } - } - preg_match_all('/(createDelegate)\(.*?,\s+\[(?[\'"])(.*?)\k/m', $fileContent, $results); - if (is_array($results[3])) { - foreach ($results[3] as $result) { - // Don't add lexicon keys that ends with a dot or an underscore - if (substr($result, -1) !== '.' && - substr($result, -1) !== '_' - ) { - // Check, if the key is concatenated - if (strpos($result, '+') === false - ) { - $this->languageKeys[] = $result; - } else { - $this->variableKeys[] = $result; - } - } - } - } - } - - /** - * Add lexicon calls in chunk files: - * [[%whatever - * - * @param string $filename - */ - private function addChunkKeys($filename) - { - $fileContent = file_get_contents($filename); - $results = []; - preg_match_all('/\[\[%(.*?)[?\]]/m', $fileContent, $results); - if (is_array($results[1])) { - foreach ($results[1] as $result) { - // Don't add lexicon keys that ends with a dot or an underscore - if (substr($result, -1) !== '.' && - substr($result, -1) !== '_' - ) { - // Check, if the key contains a setting tag - if (strpos($result, '[[+') === false - ) { - $this->languageKeys[] = $result; - } else { - $this->variableKeys[] = $result; - } - } - } - } - } - - /** - * Add _lang calls in smarty template files: - * {$_lang.whatever - * - * @param string $filename - */ - private function addSmartyKeys($filename) - { - $fileContent = file_get_contents($filename); - $results = []; - preg_match_all('/\$_lang\.(.*?)[ |}]/m', $fileContent, $results); - if (is_array($results[1])) { - foreach ($results[1] as $result) { - // Don't add lexicon keys that ends with a dot or an underscore - if (substr($result, -1) !== '.' && - substr($result, -1) !== '_' - ) { - $this->languageKeys[] = $result; - } - } - } - } - - /** - * Add setting language keys - */ - private function addSettingKeys() - { - $settings = []; - $xpdo = &$this->modx; - if (file_exists(MODX_BASE_PATH . '_build/data/transport.core.system_settings.php')) { - $settings = include MODX_BASE_PATH . '_build/data/transport.core.system_settings.php'; - } - - foreach ($settings as $setting) { - $this->languageKeys[] = 'setting_' . $setting->get('key'); - $this->languageKeys[] = 'setting_' . $setting->get('key') . '_desc'; - if (!in_array($setting->get('area'), [ - 'authentication', 'caching', 'file', 'furls', 'gateway', - 'language', 'manager', 'session', 'site', 'system' - ])) { - $this->languageKeys[] = 'area_' . $setting->get('area'); - } - } - } - - /** - * Add menu language keys - */ - private function addMenuKeys() - { - $menus = []; - $xpdo = &$this->modx; - if (file_exists(MODX_BASE_PATH . '_build/data/transport.core.menus.php')) { - $menus = include MODX_BASE_PATH . '_build/data/transport.core.menus.php'; - } - - $xpdo->setLogLevel(xPDO::LOG_LEVEL_FATAL); - foreach ($menus as $menu) { - $this->addMenuKey($menu); - } - $xpdo->setLogLevel(xPDO::LOG_LEVEL_INFO); - } - - /** - * Recursive add menu language key - * @param modMenu $menu - */ - private function addMenuKey(modMenu $menu) - { - $this->languageKeys[] = $menu->get('text'); - $this->languageKeys[] = $menu->get('description'); - $children = $menu->getMany('Children'); - foreach ($children as $child) { - $this->addMenuKey($child); - } - } - - /** - * Add widget language keys - */ - private function addWidgetKeys() - { - $widgets = []; - $xpdo = &$this->modx; - if (file_exists(MODX_BASE_PATH . '_build/data/transport.core.dashboard_widgets.php')) { - $widgets = include MODX_BASE_PATH . '_build/data/transport.core.dashboard_widgets.php'; - } - - - foreach ($widgets as $widget) { - $this->languageKeys[] = $widget->get('name'); - $this->languageKeys[] = $widget->get('description'); - } - } - - /** - * Add permission language keys - */ - private function addPermissionKeys() - { - $directory = new \RecursiveDirectoryIterator(MODX_BASE_PATH . '_build/data/permissions/', \RecursiveDirectoryIterator::SKIP_DOTS); - $filter = new \RecursiveCallbackFilterIterator($directory, function ($current, $key, $iterator) { - /** @var \RecursiveDirectoryIterator $current */ - if ($current->getFilename()[0] === '.') { - return false; - } - if ($current->isDir()) { - return !in_array($current->getFilename(), $this->excludedFolders); - } else { - $pathinfo = pathinfo($current->getFilename()); - return ($current->isFile() && isset($pathinfo['extension']) && - $pathinfo['extension'] == 'php' - ) ? true : false; - } - }); - $iterator = new \RecursiveIteratorIterator($filter); - - $xpdo = &$this->modx; - foreach ($iterator as $path => $current) { - try { - $permissions = include $current->getRealPath(); - } catch (Exception $e) { - $permissions = []; - } - foreach ($permissions as $permission) { - $this->languageKeys[] = $permission->get('description'); - } - - } - } - - /** - * Write missing/superfluous/variable keys to the file _missing.php/_superfluous.php/_variable.php in the _build/lexicon folder - * - * @param string $type - * @return bool|string - */ - private function writeKeys($type) - { - $folder = dirname(__FILE__); - switch ($type) { - case 'superfluous': - $keys = &$this->superfluousKeys; - $keysFile = '_superfluous.php'; - break; - case 'variable': - $keys = &$this->variableKeys; - $keysFile = '_variable.php'; - break; - default: - $type = 'missing'; - $keys = &$this->missingKeys; - $keysFile = '_missing.php'; - break; - } - sort($keys); - if (!empty($keys)) { - $handle = fopen($folder . '/' . $keysFile, 'w'); - if ($handle) { - fwrite($handle, " [ + 'chunk' => 'Chunk', + 'chunk_err_nf' => 'Chunk not found!', + ], + 'default' => [ + 'chunk' => 'Chunk', + 'access' => 'Access', + ], + 'resource' => [ + 'access' => 'Access Permissions', + ], + ]; + + $result = CheckLexicon::findCrossTopicDuplicates($topics); + + $this->assertArrayHasKey('chunk', $result['identical']); + $this->assertSame(['chunk', 'default'], $result['identical']['chunk']['topics']); + $this->assertArrayHasKey('access', $result['conflict']); + $this->assertSame(['default', 'resource'], $result['conflict']['access']['topics']); + $this->assertArrayNotHasKey('chunk_err_nf', $result['identical']); + $this->assertArrayNotHasKey('chunk_err_nf', $result['conflict']); + } + + public function testLoadLexiconTopicsReadsEnglishCoreTopics() + { + $path = dirname(__DIR__, 4) . '/core/lexicon/en/'; + $topics = CheckLexicon::loadLexiconTopics($path); + + $this->assertIsArray($topics); + $this->assertArrayHasKey('default', $topics); + $this->assertArrayHasKey('chunk', $topics); + $this->assertArrayHasKey('chunk', $topics['default']); + $this->assertSame('Chunk', $topics['default']['chunk']); + } + + public function testElementTypeLabelsAreNotDuplicatedInTopicFiles() + { + $path = dirname(__DIR__, 4) . '/core/lexicon/en/'; + $topics = CheckLexicon::loadLexiconTopics($path); + $duplicates = CheckLexicon::findCrossTopicDuplicates($topics); + + foreach (['chunk', 'chunks', 'snippet', 'snippets', 'plugin', 'plugins', 'template', 'templates'] as $key) { + $this->assertArrayHasKey($key, $topics['default'], "default must keep {$key}"); + $this->assertArrayNotHasKey( + $key, + $duplicates['identical'], + "{$key} should no longer be an identical cross-topic duplicate" + ); + } + + $this->assertArrayNotHasKey('chunk', $topics['chunk']); + $this->assertArrayNotHasKey('chunks', $topics['chunk']); + $this->assertArrayNotHasKey('snippet', $topics['snippet']); + $this->assertArrayNotHasKey('snippets', $topics['snippet']); + $this->assertArrayNotHasKey('plugin', $topics['plugin']); + $this->assertArrayNotHasKey('plugins', $topics['plugin']); + $this->assertArrayNotHasKey('template', $topics['template']); + $this->assertArrayNotHasKey('templates', $topics['template']); + } +} diff --git a/_build/test/Tests/Model/Lexicon/modLexiconTest.php b/_build/test/Tests/Model/Lexicon/modLexiconTest.php index 519b1f5f143..f222e851b9f 100644 --- a/_build/test/Tests/Model/Lexicon/modLexiconTest.php +++ b/_build/test/Tests/Model/Lexicon/modLexiconTest.php @@ -232,8 +232,8 @@ public function testProcess($topic,$key,$properties,$expected) { */ public function providerProcess() { return [ - ['chunk','chunk', [],'Chunk'], - ['chunk','chunks', [],'Chunks'], + ['default','chunk', [],'Chunk'], + ['default','chunks', [],'Chunks'], ['chunk','chunk_err_nfs', ['id' => 1],'Chunk not found with id: 1'], ['chunk','chunk_err_nfs', ['id' => 123],'Chunk not found with id: 123'], ['chunk','chunk_err_nfs', ['id' => 'potatoes'],'Chunk not found with id: potatoes'], @@ -258,7 +258,7 @@ public function testExists($topic,$key,$expected = true) { public function providerExists() { return [ ['chunk','chunk_err_nf',true], - ['chunk','chunks',true], + ['default','chunks',true], ['chunk','potatoes',false], ['respect','for_programmers',false], ]; @@ -285,7 +285,7 @@ public function testFetch($topic,$key,$filterPrefix = '',$removePrefix = false) public function providerFetch() { return [ ['about','help_about'], - ['chunk','chunks'], + ['default','chunks'], ['element','tv_elements','tv_'], ['element','elements','tv_',true], ]; diff --git a/_build/test/phpunit.xml b/_build/test/phpunit.xml index 6eba756c6c6..d4369e17d83 100644 --- a/_build/test/phpunit.xml +++ b/_build/test/phpunit.xml @@ -50,6 +50,9 @@ Tests/Transport + + Tests/Build + Tests/Cases/Modx/ Tests/Cases/Request/ diff --git a/core/lexicon/en/chunk.inc.php b/core/lexicon/en/chunk.inc.php index 9ffddaf3127..d8f9a7b770f 100644 --- a/core/lexicon/en/chunk.inc.php +++ b/core/lexicon/en/chunk.inc.php @@ -1,4 +1,5 @@ Chunk as well as its content. The content must be HTML, either placed in the Chunk Code field below or in a static external file, and may include MODX tags. Note, however, that PHP code will not run in this element.'; $_lang['chunk_tag_copied'] = 'Chunk tag copied!'; -$_lang['chunks'] = 'Chunks'; // Temporarily match old keys to new ones to ensure compatibility // --fields diff --git a/core/lexicon/en/plugin.inc.php b/core/lexicon/en/plugin.inc.php index a153e8841c9..65852409adb 100644 --- a/core/lexicon/en/plugin.inc.php +++ b/core/lexicon/en/plugin.inc.php @@ -1,4 +1,5 @@ Plugin as well as its content. The content must be PHP, either placed in the Plugin Code field below or in a static external file. The PHP code entered runs in response to one or more MODX System Events that you specify.'; -$_lang['plugins'] = 'Plugins'; // Temporarily match old keys to new ones to ensure compatibility // --fields diff --git a/core/lexicon/en/snippet.inc.php b/core/lexicon/en/snippet.inc.php index c06eb0cf1a1..f1fcb6010f6 100644 --- a/core/lexicon/en/snippet.inc.php +++ b/core/lexicon/en/snippet.inc.php @@ -1,4 +1,5 @@ Snippet as well as its content. The content must be PHP, either placed in the Snippet Code field below or in a static external file. To receive output from your Snippet at the point where it is called (within a Template or Chunk), a value must be returned from within the code.'; $_lang['snippet_tag_copied'] = 'Snippet tag copied!'; -$_lang['snippets'] = 'Snippets'; // Temporarily match old keys to new ones to ensure compatibility // --fields diff --git a/core/lexicon/en/template.inc.php b/core/lexicon/en/template.inc.php index eae311fade3..f0f65fc4a2b 100644 --- a/core/lexicon/en/template.inc.php +++ b/core/lexicon/en/template.inc.php @@ -1,4 +1,5 @@ Template as well as its content. The content must be HTML, either placed in the Template Code field below or in a static external file, and may include MODX tags. Note that changed or new templates won’t be visible in your site’s cached pages until the cache is emptied; however, you can use the preview function on a page to see the template in action.'; $_lang['template_tv_edit'] = 'Edit the sort order of the TVs'; $_lang['template_tv_msg'] = 'The TVs assigned to this template are listed below.'; -$_lang['templates'] = 'Templates'; $_lang['tvt_err_nf'] = 'TV does not have access to the specified Template.'; $_lang['tvt_err_remove'] = 'An error occurred while trying to delete the TV from the template.';