From 04266f77ccae635fc8c3367afee2e64f041f3e42 Mon Sep 17 00:00:00 2001 From: Florian Thoma Date: Tue, 14 Feb 2023 10:29:40 +1100 Subject: [PATCH] Update i18nTextCollector to collect strings also from themes --- src/i18n/Messages/YamlWriter.php | 7 +- src/i18n/TextCollection/i18nTextCollector.php | 81 ++++++++++++++----- tests/php/i18n/i18nTextCollectorTest.php | 33 +++++++- 3 files changed, 95 insertions(+), 26 deletions(-) diff --git a/src/i18n/Messages/YamlWriter.php b/src/i18n/Messages/YamlWriter.php index e3c99dc8f..dbaf2d07c 100644 --- a/src/i18n/Messages/YamlWriter.php +++ b/src/i18n/Messages/YamlWriter.php @@ -3,6 +3,7 @@ namespace SilverStripe\i18n\Messages; use SilverStripe\Assets\Filesystem; +use SilverStripe\Core\Path; use SilverStripe\i18n\i18n; use Symfony\Component\Yaml\Dumper; use SilverStripe\i18n\Messages\Symfony\ModuleYamlLoader; @@ -43,17 +44,17 @@ class YamlWriter implements Writer } // Create folder for lang files - $langFolder = $path . '/lang'; + $langFolder = Path::join($path, 'lang'); if (!file_exists($langFolder ?? '')) { Filesystem::makeFolder($langFolder); - touch($langFolder . '/_manifest_exclude'); + touch(Path::join($langFolder, '_manifest_exclude')); } // De-normalise messages and convert to yml $content = $this->getYaml($messages, $locale); // Open the English file and write the Master String Table - $langFile = $langFolder . '/' . $locale . '.yml'; + $langFile = Path::join($langFolder, $locale . '.yml'); if ($fh = fopen($langFile ?? '', "w")) { fwrite($fh, $content ?? ''); fclose($fh); diff --git a/src/i18n/TextCollection/i18nTextCollector.php b/src/i18n/TextCollection/i18nTextCollector.php index 6becafcf4..d6a1adea6 100644 --- a/src/i18n/TextCollection/i18nTextCollector.php +++ b/src/i18n/TextCollection/i18nTextCollector.php @@ -12,6 +12,7 @@ use SilverStripe\Core\Injector\Injectable; use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\Module; use SilverStripe\Core\Manifest\ModuleLoader; +use SilverStripe\Core\Path; use SilverStripe\Dev\Debug; use SilverStripe\Control\Director; use ReflectionClass; @@ -50,6 +51,8 @@ class i18nTextCollector { use Injectable; + private const THEME_PREFIX = 'themes:'; + /** * Default (master) locale * @@ -100,6 +103,13 @@ class i18nTextCollector */ protected $fileExtensions = ['php', 'ss']; + /** + * List all modules and themes + * + * @var array + */ + private $modulesAndThemes; + /** * @param $locale */ @@ -179,6 +189,8 @@ class i18nTextCollector return; } + $modules = $this->getModulesAndThemes(); + // Write each module language file foreach ($entitiesByModule as $moduleName => $entities) { // Skip empty translations @@ -188,7 +200,7 @@ class i18nTextCollector // Clean sorting prior to writing ksort($entities); - $module = ModuleLoader::inst()->getManifest()->getModule($moduleName); + $module = $modules[$moduleName]; $this->write($module, $entities); } } @@ -215,9 +227,9 @@ class i18nTextCollector // Restrict modules we update to just the specified ones (if any passed) if (!empty($restrictToModules)) { // Normalise module names - $modules = array_filter(array_map(function ($name) { - $module = ModuleLoader::inst()->getManifest()->getModule($name); - return $module ? $module->getName() : null; + $allModules = $this->getModulesAndThemes(); + $modules = array_filter(array_map(function ($name) use ($allModules) { + return array_key_exists($name, $allModules) ? $this->getModuleName($name, $allModules[$name]) : null; }, $restrictToModules ?? [])); // Remove modules foreach (array_diff(array_keys($entitiesByModule ?? []), $modules) as $module) { @@ -375,10 +387,10 @@ class i18nTextCollector protected function mergeWithExisting($entitiesByModule) { // For each module do a simple merge of the default yml with these strings + $modules = $this->getModulesAndThemes(); foreach ($entitiesByModule as $module => $messages) { // Load existing localisations - $masterFile = ModuleLoader::inst()->getManifest()->getModule($module)->getPath() . - DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR . $this->defaultLocale . '.yml'; + $masterFile = Path::join($modules[$module]->getPath(), 'lang', $this->defaultLocale . '.yml'); $existingMessages = $this->getReader()->read($this->defaultLocale, $masterFile); // Merge @@ -401,11 +413,11 @@ class i18nTextCollector { // A master string tables array (one mst per module) $entitiesByModule = []; - $modules = ModuleLoader::inst()->getManifest()->getModules(); - foreach ($modules as $module) { + $modules = $this->getModulesAndThemes(); + foreach ($modules as $moduleName => $module) { // we store the master string tables $processedEntities = $this->processModule($module); - $moduleName = $module->getName(); + $moduleName = $this->getModuleName($moduleName, $module); if (isset($entitiesByModule[$moduleName])) { $entitiesByModule[$moduleName] = array_merge_recursive( $entitiesByModule[$moduleName], @@ -450,6 +462,37 @@ class i18nTextCollector return $entitiesByModule; } + /** + * Loads all modules and themes installed, including app. Uses the format of + * the @link ModuleLoader manifest for themes as well. + * Themes can be references with "themes:{theme name}". + */ + private function getModulesAndThemes(): array + { + if (!$this->modulesAndThemes) { + $modules = ModuleLoader::inst()->getManifest()->getModules(); + // load themes as modules + $themes = array_diff(scandir(THEMES_PATH), ['..', '.']); + if ($themes) { + foreach ($themes as $theme) { + if (is_dir(Path::join(THEMES_PATH, $theme))) { + $modules[self::THEME_PREFIX . $theme] = new Module(Path::join(THEMES_PATH, $theme), BASE_PATH); + } + } + } + $this->modulesAndThemes = $modules; + } + return $this->modulesAndThemes; + } + + /** + * Returns the name of the module or theme + */ + private function getModuleName(string $origName, Module $module): string + { + return strpos($origName, self::THEME_PREFIX) === 0 ? $origName : $module->getName(); + } + /** * Write entities to a module * @@ -462,7 +505,7 @@ class i18nTextCollector $this->getWriter()->write( $entities, $this->defaultLocale, - $this->baseSavePath . DIRECTORY_SEPARATOR . $module->getRelativePath() + Path::join($this->baseSavePath, $module->getRelativePath()) ); return $this; } @@ -517,25 +560,25 @@ class i18nTextCollector $modulePath = $module->getPath(); // Search all .ss files in themes - if (stripos($module->getRelativePath() ?? '', 'themes' . DIRECTORY_SEPARATOR) === 0) { + if (stripos($module->getRelativePath() ?? '', self::THEME_PREFIX) === 0) { return $this->getFilesRecursive($modulePath, null, 'ss'); } // If non-standard module structure, search all root files - if (!is_dir($modulePath . DIRECTORY_SEPARATOR . 'code') && !is_dir($modulePath . DIRECTORY_SEPARATOR . 'src')) { + if (!is_dir(Path::join($modulePath, 'code')) && !is_dir(Path::join($modulePath, 'src'))) { return $this->getFilesRecursive($modulePath); } // Get code files - if (is_dir($modulePath . DIRECTORY_SEPARATOR . 'src')) { - $files = $this->getFilesRecursive($modulePath . DIRECTORY_SEPARATOR . 'src', null, 'php'); + if (is_dir(Path::join($modulePath, 'src'))) { + $files = $this->getFilesRecursive(Path::join($modulePath, 'src'), null, 'php'); } else { - $files = $this->getFilesRecursive($modulePath . DIRECTORY_SEPARATOR . 'code', null, 'php'); + $files = $this->getFilesRecursive(Path::join($modulePath, 'code'), null, 'php'); } // Search for templates in this module - if (is_dir($modulePath . DIRECTORY_SEPARATOR . 'templates')) { - $templateFiles = $this->getFilesRecursive($modulePath . DIRECTORY_SEPARATOR . 'templates', null, 'ss'); + if (is_dir(Path::join($modulePath, 'templates'))) { + $templateFiles = $this->getFilesRecursive(Path::join($modulePath, 'templates'), null, 'ss'); } else { $templateFiles = $this->getFilesRecursive($modulePath, null, 'ss'); } @@ -987,8 +1030,6 @@ class i18nTextCollector return "{$namespace}.{$entity}"; } - - /** * Helper function that searches for potential files (templates and code) to be parsed * @@ -1004,7 +1045,7 @@ class i18nTextCollector $fileList = []; } // Skip ignored folders - if (is_file($folder . DIRECTORY_SEPARATOR . '_manifest_exclude') || preg_match($folderExclude ?? '', $folder ?? '')) { + if (is_file(Path::join($folder, '_manifest_exclude')) || preg_match($folderExclude ?? '', $folder ?? '')) { return $fileList; } diff --git a/tests/php/i18n/i18nTextCollectorTest.php b/tests/php/i18n/i18nTextCollectorTest.php index d74afdb33..3ab641bee 100644 --- a/tests/php/i18n/i18nTextCollectorTest.php +++ b/tests/php/i18n/i18nTextCollectorTest.php @@ -3,15 +3,14 @@ namespace SilverStripe\i18n\Tests; use SilverStripe\Assets\Filesystem; -use SilverStripe\Core\Config\Config; use SilverStripe\Core\Manifest\ModuleLoader; +use SilverStripe\Core\Manifest\ModuleManifest; +use SilverStripe\Core\Path; use SilverStripe\Dev\SapphireTest; use SilverStripe\i18n\i18n; use SilverStripe\i18n\Messages\YamlWriter; -use SilverStripe\i18n\Tests\i18nTest\TestDataObject; use SilverStripe\i18n\Tests\i18nTextCollectorTest\Collector; use SilverStripe\i18n\TextCollection\i18nTextCollector; -use SilverStripe\Security\Member; class i18nTextCollectorTest extends SapphireTest { @@ -734,6 +733,34 @@ PHP; ); } + public function testCollectFromTheme() + { + // create new module manifest only containing the test theme + $moduleManifest = new ModuleManifest($this->alternateBasePath); + $moduleManifest->addModule(Path::join($this->alternateBasePath, 'themes', 'testtheme1')); + $this->pushModuleManifest($moduleManifest); + + $c = i18nTextCollector::create(); + $entities = $c->collect(); + + $this->assertArrayHasKey('testtheme1', $entities); + + $this->assertEquals( + [ + 'i18nTestTheme1.LAYOUTTEMPLATE' => 'Theme1 Layout Template', + 'i18nTestTheme1.MAINTEMPLATE' => 'Theme1 Main Template', + 'i18nTestTheme1.ss.LAYOUTTEMPLATENONAMESPACE' => 'Theme1 Layout Template no namespace', + 'i18nTestTheme1.ss.REPLACEMENTNONAMESPACE' => 'Theme1 My replacement no namespace: {replacement}', + 'i18nTestTheme1Include.REPLACEMENTINCLUDENAMESPACE' => 'Theme1 My include replacement: {replacement}', + 'i18nTestTheme1Include.WITHNAMESPACE' => 'Theme1 Include Entity with Namespace', + 'i18nTestTheme1Include.ss.NONAMESPACE' => 'Theme1 Include Entity without Namespace', + 'i18nTestTheme1Include.ss.REPLACEMENTINCLUDENONAMESPACE' => 'Theme1 My include replacement no namespace: {replacement}', + 'i18nTestTheme1.REPLACEMENTNAMESPACE' => 'Theme1 My replacement: {replacement}', + ], + $entities['testtheme1'] + ); + } + /** * Test that duplicate keys are resolved to the appropriate modules */