diff --git a/readme.md b/readme.md index ea362d3..6435a45 100644 --- a/readme.md +++ b/readme.md @@ -38,6 +38,14 @@ it will install to the path specified by `./release-` in the current di `release` actually has several sub-commands which can be run independently. These are as below: -* `release:changelog` Just generates the changelog and commits this to source control. * `release:create` creates the project folder +* `release:changelog` Just generates the changelog and commits this to source control. +* `release:translate` Updates translations and commits this to source control + +## Module-level commands + +Outside of doing core releases, you can use this for specific modules + +* `module:translate ` Updates translations for modules and commits this to source control. If you +don't specify a list of modules then all modules will be translated. Specify 'installer' for root module. diff --git a/src/Application.php b/src/Application.php index 5a11bd1..d5d0321 100644 --- a/src/Application.php +++ b/src/Application.php @@ -18,6 +18,8 @@ protected function getDefaultCommands() $commands[] = new Commands\Release\Create(); $commands[] = new Commands\Release\Changelog(); $commands[] = new Commands\Release\Release(); + + $commands[] = new Commands\Module\Translate(); return $commands; } diff --git a/src/Commands/Module/Module.php b/src/Commands/Module/Module.php new file mode 100644 index 0000000..de85574 --- /dev/null +++ b/src/Commands/Module/Module.php @@ -0,0 +1,47 @@ +addArgument( + 'modules', + InputArgument::IS_ARRAY | InputArgument::OPTIONAL, + 'Optional list of modules to filter (separate by space)' + ); + $this->addOption('directory', 'd', InputOption::VALUE_REQUIRED, 'Module directory'); + } + + /** + * Get the directory the project is, or will be in + * + * @return string + */ + protected function getInputDirectory() { + $directory = $this->input->getOption('directory'); + if(!$directory) { + $directory = getcwd(); + } + return $directory; + } + + /** + * Gets the list of module names to filter by (or empty if all modules) + * + * @return array + */ + protected function getInputModules() { + return $this->input->getArgument('modules') ?: array(); + } +} diff --git a/src/Commands/Module/Translate.php b/src/Commands/Module/Translate.php new file mode 100644 index 0000000..0cc0a8c --- /dev/null +++ b/src/Commands/Module/Translate.php @@ -0,0 +1,30 @@ +getInputDirectory(); + $modules = $this->getInputModules(); + + $translate = new UpdateTranslations($this, $directory, $modules); + $translate->run($this->input, $this->output); + //$step->run($this->input, $this->output); + } + +} diff --git a/src/Model/Module.php b/src/Model/Module.php index 074279c..1de31ec 100644 --- a/src/Model/Module.php +++ b/src/Model/Module.php @@ -51,6 +51,24 @@ public function __construct($directory, $name, Project $parent = null) { public function getDirectory() { return $this->directory; } + + /** + * Gets the module lang dir + * + * @return string + */ + public function getLangDirectory() { + return $this->directory . '/lang'; + } + + /** + * Base name only of location of code + * + * @return string + */ + public function getCodeDirectory() { + return $this->getName(); + } /** * A project is valid if it has a root composer.json @@ -58,6 +76,15 @@ public function getDirectory() { public function isValid() { return $this->directory && realpath($this->directory . '/composer.json'); } + + /** + * Determine if this project has a .tx configured + * + * @return bool + */ + public function isTranslatable() { + return $this->directory && realpath($this->directory . '/.tx/config'); + } public function getName() { return $this->name; diff --git a/src/Model/Project.php b/src/Model/Project.php index d17b915..bf38898 100644 --- a/src/Model/Project.php +++ b/src/Model/Project.php @@ -2,6 +2,8 @@ namespace SilverStripe\Cow\Model; +use InvalidArgumentException; + /** * Represents information about a project in a given directory * @@ -11,10 +13,16 @@ class Project extends Module { public function __construct($directory) { parent::__construct($directory, 'installer'); + + if(!realpath($this->directory . '/mysite')) { + throw new InvalidArgumentException("No installer found in \"{$this->directory}\""); + } } /** * Gets the list of modules in this installer + * + * @return Module[] */ public function getModules() { // Include self as head module @@ -44,7 +52,7 @@ public function getModule($name) { } /** - * Check if the given path contains a module + * Check if the given path contains a non-installer module * * @return bool */ @@ -59,4 +67,12 @@ protected function isModulePath($path) { $ignore = array('mysite', 'assets', 'vendor'); return !in_array($name, $ignore); } + + public function getLangDirectory() { + return $this->directory . '/mysite/lang'; + } + + public function getCodeDirectory() { + return 'mysite'; + } } diff --git a/src/Steps/Release/UpdateTranslations.php b/src/Steps/Release/UpdateTranslations.php index 03fe221..2959878 100644 --- a/src/Steps/Release/UpdateTranslations.php +++ b/src/Steps/Release/UpdateTranslations.php @@ -2,29 +2,232 @@ namespace SilverStripe\Cow\Steps\Release; +use InvalidArgumentException; +use SilverStripe\Cow\Commands\Command; +use SilverStripe\Cow\Model\Module; +use SilverStripe\Cow\Model\Project; use SilverStripe\Cow\Steps\Step; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Synchronise all translations with transifex, merging these with strings detected in code files + * + * Basic process follows: + * - Set mtime on all local files to long ago (1 year in the past?) because tx pull breaks on new files and won't update them + * - Pull all source files from transifex with the below: + * `tx pull -a -s -f --minimum-perc=10` + * - Detect all new translations, making sure to merge in changes + * `./framework/sake dev/tasks/i18nTextCollectorTask "flush=all" "merge=1" + * - Detect all new JS translations in a similar way (todo) + * - Generate javascript from js source files + * `phing -Dmodule=my-module translation-generate-javascript-for-module` + * - Push up all source translations + * `tx push -s` */ class UpdateTranslations extends Step { + /** + * Min tx client version + + * @var string + */ + protected $txVersion = '0.11'; + + /** + * Min % difference required for tx updates + * + * @var int + */ + protected $txMinimumPerc = 10; + + /** + * @var Project + */ + protected $project; + + /** + * + * @var array + */ + protected $modules; + + /** + * Create a new translation step + * + * @param Command $command + * @param string $directory Where to translate + * @param array $modules Optional list of modules to limit translation to + */ + public function __construct(Command $command, $directory, $modules = array()) { + parent::__construct($command); + + $this->modules = $modules; + $this->project = new Project($directory); + } + + /** + * @return Project + */ + public function getProject() { + return $this->project; + } + + /** + * + * @return array + */ + public function getModules() { + return $this->modules; + } + public function getStepName() { return 'translations'; } public function run(InputInterface $input, OutputInterface $output) { - // Basic process follows: - // * Pull all source files from transifex with the below: - // `tx pull -a -s -f --minimum-perc=10` - // * Detect all new translations, making sure to merge in changes - // `./framework/sake dev/tasks/i18nTextCollectorTask "flush=all" "merge=1" - // * Detect all new JS translations in a similar way (todo) - // * Generate javascript from js source files - // `phing -Dmodule=my-module translation-generate-javascript-for-module` - // * Push up all source translations - // `tx push -s` + $modules = $this->getModuleItems($output); + $this->log($output, sprintf("Updating translations for %d module(s)", count($modules))); + $this->checkVersion($output); + $this->pullSource($output, $modules); + $this->collectStrings($output, $modules); + $this->generateJavascript($output, $modules); + $this->pushSource($output, $modules); + $this->commitChanges($output, $modules); + $this->log($output, 'Translations complete'); + } + + /** + * Test that tx tool is installed + */ + protected function checkVersion(OutputInterface $output) { + $result = $this->runCommand($output, array("tx", "--version")); + if(!version_compare($result, $this->txVersion, '<')) { + throw new InvalidArgumentException( + "translate requires transifex {$this->txVersion} at least. " + ."Run 'pip install transifex-client==0.11b3' to update. " + ."Current version: ".$result + ); + } + + $this->log($output, "Using transifex CLI version: $result"); + } + + /** + * Update sources from transifex + * + * @param OutputInterface $output + * @param Module[] $modules List of modules + */ + protected function pullSource(OutputInterface $output, $modules) { + $this->log($output, "Pulling sources from transifex (min %{$this->txMinimumPerc} delta)"); + + foreach($modules as $module) { + // Set mtime to a year ago so that transifex will see these as obsolete + $touchCommand = sprintf( + 'find %s -type f \( -name "*.yml" \) -exec touch -t %s {} \;', + $module->getLangDirectory(), + date('YmdHi.s', strtotime('-1 year')) + ); + $this->runCommand($output, $touchCommand); + + // Run tx pull + $pullCommand = sprintf( + '(cd %s && tx pull -a -s -f --minimum-perc=%d)', + $module->getDirectory(), + $this->txMinimumPerc + ); + $this->runCommand($output, $pullCommand); + } + } + + /** + * Run text collector on the given modules + * + * @param OutputInterface $output + * @param Module[] $modules List of modules + */ + protected function collectStrings(OutputInterface $output, $modules) { + $this->log($output, "Running i18nTextCollectorTask"); + + // Get code dirs for each module + $dirs = array(); + foreach($modules as $module) { + $dirs[] = $module->getCodeDirectory(); + } + + $sakeCommand = sprintf( + '(cd %s && ./framework/sake dev/tasks/i18nTextCollectorTask "flush=all" "merge=1" "module=%s")', + $this->getProject()->getDirectory(), + implode(',', $dirs) + ); + $this->runCommand($output, $sakeCommand); + } + + /** + * Generate javascript for all modules + * + * @param OutputInterface $output + * @param type $modules + */ + public function generateJavascript(OutputInterface $output, $modules) { + // @todo + } + + /** + * Push source updates to transifex + * + * @param OutputInterface $output + * @param type $modules + */ + public function pushSource(OutputInterface $output, $modules) { + // @todo + } + + /** + * Commit changes for all modules + * + * @param OutputInterface $output + * @param type $modules + */ + public function commitChanges(OutputInterface $output, $modules) { + // @todo + } + + /** + * Get the list of module objects to translate + * + * @param OutputInterface + * @return Module[] + */ + protected function getModuleItems(OutputInterface $output) { + $modules = $this->getProject()->getModules(); + $filter = $this->getModules(); + + // Get only modules with translations + $self = $this; + return array_filter($modules, function($module) use ($output, $filter, $self) { + // Automatically skip un-translateable modules + if(empty($filter)) { + return $module->isTranslatable(); + } + + // Skip filtered + if(!in_array($module->getName(), $filter)) { + return false; + } + + // Warn if this module has no translations + if(!$module->isTranslatable()) { + $self->log( + $output, + sprintf("Selected module %s has no .tx/config directory", $module->getName()), + "error" + ); + return false; + } + + return true; + }); } } diff --git a/src/Steps/Step.php b/src/Steps/Step.php index 8fc124b..9d5b6dc 100644 --- a/src/Steps/Step.php +++ b/src/Steps/Step.php @@ -24,7 +24,7 @@ abstract public function getStepName(); abstract public function run(InputInterface $input, OutputInterface $output); - protected function log(OutputInterface $output, $message, $format = '') { + public function log(OutputInterface $output, $message, $format = '') { $name = $this->getStepName(); $text = "[{$name}] "; if($format) {