Merge pull request #10685 from xini/fix-i18n-collect-themes

Update i18nTextCollector to collect strings also from themes
This commit is contained in:
Guy Sartorelli 2023-05-10 10:40:28 +12:00 committed by GitHub
commit 7210ac8cd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 95 additions and 26 deletions

View File

@ -3,6 +3,7 @@
namespace SilverStripe\i18n\Messages; namespace SilverStripe\i18n\Messages;
use SilverStripe\Assets\Filesystem; use SilverStripe\Assets\Filesystem;
use SilverStripe\Core\Path;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use Symfony\Component\Yaml\Dumper; use Symfony\Component\Yaml\Dumper;
use SilverStripe\i18n\Messages\Symfony\ModuleYamlLoader; use SilverStripe\i18n\Messages\Symfony\ModuleYamlLoader;
@ -43,17 +44,17 @@ class YamlWriter implements Writer
} }
// Create folder for lang files // Create folder for lang files
$langFolder = $path . '/lang'; $langFolder = Path::join($path, 'lang');
if (!file_exists($langFolder ?? '')) { if (!file_exists($langFolder ?? '')) {
Filesystem::makeFolder($langFolder); Filesystem::makeFolder($langFolder);
touch($langFolder . '/_manifest_exclude'); touch(Path::join($langFolder, '_manifest_exclude'));
} }
// De-normalise messages and convert to yml // De-normalise messages and convert to yml
$content = $this->getYaml($messages, $locale); $content = $this->getYaml($messages, $locale);
// Open the English file and write the Master String Table // 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")) { if ($fh = fopen($langFile ?? '', "w")) {
fwrite($fh, $content ?? ''); fwrite($fh, $content ?? '');
fclose($fh); fclose($fh);

View File

@ -12,6 +12,7 @@ use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\Module; use SilverStripe\Core\Manifest\Module;
use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Core\Path;
use SilverStripe\Dev\Debug; use SilverStripe\Dev\Debug;
use SilverStripe\Control\Director; use SilverStripe\Control\Director;
use ReflectionClass; use ReflectionClass;
@ -50,6 +51,8 @@ class i18nTextCollector
{ {
use Injectable; use Injectable;
private const THEME_PREFIX = 'themes:';
/** /**
* Default (master) locale * Default (master) locale
* *
@ -100,6 +103,13 @@ class i18nTextCollector
*/ */
protected $fileExtensions = ['php', 'ss']; protected $fileExtensions = ['php', 'ss'];
/**
* List all modules and themes
*
* @var array
*/
private $modulesAndThemes;
/** /**
* @param $locale * @param $locale
*/ */
@ -179,6 +189,8 @@ class i18nTextCollector
return; return;
} }
$modules = $this->getModulesAndThemes();
// Write each module language file // Write each module language file
foreach ($entitiesByModule as $moduleName => $entities) { foreach ($entitiesByModule as $moduleName => $entities) {
// Skip empty translations // Skip empty translations
@ -188,7 +200,7 @@ class i18nTextCollector
// Clean sorting prior to writing // Clean sorting prior to writing
ksort($entities); ksort($entities);
$module = ModuleLoader::inst()->getManifest()->getModule($moduleName); $module = $modules[$moduleName];
$this->write($module, $entities); $this->write($module, $entities);
} }
} }
@ -215,9 +227,9 @@ class i18nTextCollector
// Restrict modules we update to just the specified ones (if any passed) // Restrict modules we update to just the specified ones (if any passed)
if (!empty($restrictToModules)) { if (!empty($restrictToModules)) {
// Normalise module names // Normalise module names
$modules = array_filter(array_map(function ($name) { $allModules = $this->getModulesAndThemes();
$module = ModuleLoader::inst()->getManifest()->getModule($name); $modules = array_filter(array_map(function ($name) use ($allModules) {
return $module ? $module->getName() : null; return array_key_exists($name, $allModules) ? $this->getModuleName($name, $allModules[$name]) : null;
}, $restrictToModules ?? [])); }, $restrictToModules ?? []));
// Remove modules // Remove modules
foreach (array_diff(array_keys($entitiesByModule ?? []), $modules) as $module) { foreach (array_diff(array_keys($entitiesByModule ?? []), $modules) as $module) {
@ -375,10 +387,10 @@ class i18nTextCollector
protected function mergeWithExisting($entitiesByModule) protected function mergeWithExisting($entitiesByModule)
{ {
// For each module do a simple merge of the default yml with these strings // For each module do a simple merge of the default yml with these strings
$modules = $this->getModulesAndThemes();
foreach ($entitiesByModule as $module => $messages) { foreach ($entitiesByModule as $module => $messages) {
// Load existing localisations // Load existing localisations
$masterFile = ModuleLoader::inst()->getManifest()->getModule($module)->getPath() . $masterFile = Path::join($modules[$module]->getPath(), 'lang', $this->defaultLocale . '.yml');
DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR . $this->defaultLocale . '.yml';
$existingMessages = $this->getReader()->read($this->defaultLocale, $masterFile); $existingMessages = $this->getReader()->read($this->defaultLocale, $masterFile);
// Merge // Merge
@ -401,11 +413,11 @@ class i18nTextCollector
{ {
// A master string tables array (one mst per module) // A master string tables array (one mst per module)
$entitiesByModule = []; $entitiesByModule = [];
$modules = ModuleLoader::inst()->getManifest()->getModules(); $modules = $this->getModulesAndThemes();
foreach ($modules as $module) { foreach ($modules as $moduleName => $module) {
// we store the master string tables // we store the master string tables
$processedEntities = $this->processModule($module); $processedEntities = $this->processModule($module);
$moduleName = $module->getName(); $moduleName = $this->getModuleName($moduleName, $module);
if (isset($entitiesByModule[$moduleName])) { if (isset($entitiesByModule[$moduleName])) {
$entitiesByModule[$moduleName] = array_merge_recursive( $entitiesByModule[$moduleName] = array_merge_recursive(
$entitiesByModule[$moduleName], $entitiesByModule[$moduleName],
@ -450,6 +462,37 @@ class i18nTextCollector
return $entitiesByModule; 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 * Write entities to a module
* *
@ -462,7 +505,7 @@ class i18nTextCollector
$this->getWriter()->write( $this->getWriter()->write(
$entities, $entities,
$this->defaultLocale, $this->defaultLocale,
$this->baseSavePath . DIRECTORY_SEPARATOR . $module->getRelativePath() Path::join($this->baseSavePath, $module->getRelativePath())
); );
return $this; return $this;
} }
@ -517,25 +560,25 @@ class i18nTextCollector
$modulePath = $module->getPath(); $modulePath = $module->getPath();
// Search all .ss files in themes // 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'); return $this->getFilesRecursive($modulePath, null, 'ss');
} }
// If non-standard module structure, search all root files // 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); return $this->getFilesRecursive($modulePath);
} }
// Get code files // Get code files
if (is_dir($modulePath . DIRECTORY_SEPARATOR . 'src')) { if (is_dir(Path::join($modulePath, 'src'))) {
$files = $this->getFilesRecursive($modulePath . DIRECTORY_SEPARATOR . 'src', null, 'php'); $files = $this->getFilesRecursive(Path::join($modulePath, 'src'), null, 'php');
} else { } 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 // Search for templates in this module
if (is_dir($modulePath . DIRECTORY_SEPARATOR . 'templates')) { if (is_dir(Path::join($modulePath, 'templates'))) {
$templateFiles = $this->getFilesRecursive($modulePath . DIRECTORY_SEPARATOR . 'templates', null, 'ss'); $templateFiles = $this->getFilesRecursive(Path::join($modulePath, 'templates'), null, 'ss');
} else { } else {
$templateFiles = $this->getFilesRecursive($modulePath, null, 'ss'); $templateFiles = $this->getFilesRecursive($modulePath, null, 'ss');
} }
@ -987,8 +1030,6 @@ class i18nTextCollector
return "{$namespace}.{$entity}"; return "{$namespace}.{$entity}";
} }
/** /**
* Helper function that searches for potential files (templates and code) to be parsed * Helper function that searches for potential files (templates and code) to be parsed
* *
@ -1004,7 +1045,7 @@ class i18nTextCollector
$fileList = []; $fileList = [];
} }
// Skip ignored folders // 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; return $fileList;
} }

View File

@ -3,15 +3,14 @@
namespace SilverStripe\i18n\Tests; namespace SilverStripe\i18n\Tests;
use SilverStripe\Assets\Filesystem; use SilverStripe\Assets\Filesystem;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\Core\Manifest\ModuleManifest;
use SilverStripe\Core\Path;
use SilverStripe\Dev\SapphireTest; use SilverStripe\Dev\SapphireTest;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\i18n\Messages\YamlWriter; use SilverStripe\i18n\Messages\YamlWriter;
use SilverStripe\i18n\Tests\i18nTest\TestDataObject;
use SilverStripe\i18n\Tests\i18nTextCollectorTest\Collector; use SilverStripe\i18n\Tests\i18nTextCollectorTest\Collector;
use SilverStripe\i18n\TextCollection\i18nTextCollector; use SilverStripe\i18n\TextCollection\i18nTextCollector;
use SilverStripe\Security\Member;
class i18nTextCollectorTest extends SapphireTest 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 * Test that duplicate keys are resolved to the appropriate modules
*/ */