44
55namespace Drupal \drupal_translation_extractor \Command ;
66
7+ use Drupal \Component \Gettext \PoStreamReader ;
78use Drupal \Core \Extension \ExtensionPathResolver ;
9+ use Drupal \drupal_translation_extractor \Exception \InvalidArgumentException ;
810use Drupal \drupal_translation_extractor \Translation \Dumper \PoItem ;
911use Drupal \locale \StringStorageInterface ;
1012use Symfony \Component \Console \Attribute \AsCommand ;
@@ -42,10 +44,11 @@ final class TranslationExtractCommand extends Command
4244 private const NO_FILL_PREFIX = "\0NoFill \0" ;
4345
4446 public function __construct (
47+ private readonly TranslationWriterInterface $ writer ,
48+ private PoStreamReader $ reader ,
49+ private readonly ExtractorInterface $ extractor ,
4550 private readonly ExtensionPathResolver $ extensionPathResolver ,
4651 private readonly StringStorageInterface $ stringStorage ,
47- private readonly ExtractorInterface $ extractor ,
48- private readonly TranslationWriterInterface $ writer ,
4952 ) {
5053 parent ::__construct ();
5154
@@ -59,7 +62,6 @@ protected function configure(): void
5962 $ this
6063 ->setDefinition ([
6164 new InputArgument ('locale ' , InputArgument::REQUIRED , 'The locale ' ),
62- // new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages'),
6365 new InputOption ('prefix ' , null , InputOption::VALUE_REQUIRED , 'Override the default prefix ' , '__ ' ),
6466 new InputOption ('no-fill ' , null , InputOption::VALUE_NONE , 'Extract translation keys without filling in values ' ),
6567 new InputOption ('format ' , null , InputOption::VALUE_REQUIRED , 'Override the default output format ' , 'po ' ),
@@ -70,36 +72,33 @@ protected function configure(): void
7072 new InputOption ('sort ' , null , InputOption::VALUE_REQUIRED , 'Return list of messages sorted alphabetically ' ),
7173 new InputOption ('as-tree ' , null , InputOption::VALUE_REQUIRED , 'Dump the messages as a tree-like structure: The given value defines the level where to switch to inline YAML ' ),
7274
73- new InputOption ('source ' , null , InputOption:: VALUE_REQUIRED , 'Source path. ' ),
75+ new InputArgument ('source ' , InputArgument:: REQUIRED , 'Source path. ' ),
7476 new InputOption ('output ' , null , InputOption::VALUE_REQUIRED , 'Output path. Required if --force is specified. ' ),
77+ new InputOption ('fill-from-string-storage ' , null , InputOption::VALUE_NONE , 'Fill translations with values from string storage. ' ),
7578 ])
7679 ->setHelp (<<<'EOF'
7780The <info>%command.name%</info> command extracts translation strings from templates
78- of a given bundle or the default translations directory . It can display them or merge
81+ of a given module or theme or another path . It can display them or merge
7982the new ones into the translation files.
8083
8184When new translation strings are found it can automatically add a prefix to the translation
8285message. However, if the <comment>--no-fill</comment> option is used, the <comment>--prefix</comment>
8386option has no effect, since the translation values are left empty.
8487
85- Example running against a Bundle (AcmeBundle )
88+ Example running against a module (my_module )
8689
87- <info>php %command.full_name% --dump-messages en AcmeBundle </info>
88- <info>php %command.full_name% --force --prefix="new_" fr AcmeBundle </info>
90+ <info>php %command.full_name% --dump-messages en module:my_module </info>
91+ <info>php %command.full_name% --force --prefix="new_" fr module:my_module </info>
8992
90- Example running against default messages directory
93+ Example running against a theme (my_theme)
9194
92- <info>php %command.full_name% --dump-messages en</info>
93- <info>php %command.full_name% --force --prefix="new_" fr</info>
95+ <info>php %command.full_name% --dump-messages en theme:my_theme </info>
96+ <info>php %command.full_name% --force --prefix="new_" fr theme:my_theme </info>
9497
9598You can sort the output with the <comment>--sort</> flag:
9699
97- <info>php %command.full_name% --dump-messages --sort=asc en AcmeBundle</info>
98- <info>php %command.full_name% --force --sort=desc fr</info>
99-
100- You can dump a tree-like structure using the yaml format with <comment>--as-tree</> flag:
101-
102- <info>php %command.full_name% --force --format=yaml --as-tree=3 en AcmeBundle</info>
100+ <info>php %command.full_name% --dump-messages --sort=asc en …</info>
101+ <info>php %command.full_name% --force --sort=desc fr …</info>
103102
104103EOF
105104 )
@@ -152,9 +151,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int
152151 $ extractedCatalogue = $ this ->extractMessages ($ input ->getArgument ('locale ' ), $ codePaths , $ prefix );
153152
154153 $ io ->comment ('Loading translated messages... ' );
155- $ currentCatalogue = true === $ input ->getOption ('no-fill ' )
156- ? $ extractedCatalogue
157- : $ this ->loadTranslatedMessages ($ extractedCatalogue );
154+ $ outputPath = $ this ->getOutputPath ($ input , $ sourceInfo + [
155+ 'locale ' => $ input ->getArgument ('locale ' ),
156+ ]);
157+
158+ $ currentCatalogue = $ this ->loadCurrentMessages ($ input ->getArgument ('locale ' ), $ outputPath );
159+ if (true === $ input ->getOption ('fill-from-string-storage ' )) {
160+ // Merge in translations from string storage.
161+ $ currentCatalogue = new MergeOperation (
162+ $ this ->loadTranslatedMessages ($ extractedCatalogue ),
163+ $ currentCatalogue )->getResult ();
164+ }
158165
159166 if (null !== $ domain = $ input ->getOption ('domain ' )) {
160167 if ('' === $ domain ) {
@@ -211,7 +218,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
211218 sort ($ list );
212219 }
213220
214- $ io ->section (\sprintf ('Messages extracted for domain "<info>%s</info>" (%d message%s) ' , $ domain , $ domainMessagesCount , $ domainMessagesCount > 1 ? 's ' : '' ));
221+ $ io ->section (\sprintf ('Messages extracted for context "<info>%s</info>" (%d message%s) ' , PoItem:: formatContext ( $ domain) , $ domainMessagesCount , $ domainMessagesCount > 1 ? 's ' : '' ));
215222 $ io ->listing ($ list );
216223
217224 $ extractedMessagesCount += $ domainMessagesCount ;
@@ -238,9 +245,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
238245 $ this ->removeNoFillTranslations ($ operationResult );
239246 }
240247
241- $ outputPath = $ this ->getOutputPath ($ input , $ sourceInfo + [
242- 'locale ' => $ operationResult ->getLocale (),
243- ]);
244248 $ dumperOptions = [
245249 'path ' => dirname ($ outputPath ),
246250 'output_name ' => basename ($ outputPath ),
@@ -379,27 +383,54 @@ private function filterDuplicateTransPaths(array $transPaths): array
379383 return $ filteredPaths ;
380384 }
381385
386+ /**
387+ * Load current messages from po file.
388+ */
389+ private function loadCurrentMessages (string $ locale , string $ path ): MessageCatalogue
390+ {
391+ $ currentCatalogue = new MessageCatalogue ($ locale );
392+
393+ if (!is_file ($ path )) {
394+ return $ currentCatalogue ;
395+ }
396+
397+ $ this ->reader ->setURI ($ path );
398+ $ this ->reader ->open ();
399+ while ($ item = $ this ->reader ->readItem ()) {
400+ if (!empty ($ item ->getTranslation ())) {
401+ $ currentCatalogue ->set (
402+ PoItem::joinStrings ((array ) $ item ->getSource ()),
403+ PoItem::joinStrings ((array ) $ item ->getTranslation ()),
404+ PoItem::fromContext ($ item ->getContext ()),
405+ );
406+ }
407+ }
408+ $ this ->reader ->close ();
409+
410+ return $ currentCatalogue ;
411+ }
412+
382413 private function loadTranslatedMessages (MessageCatalogue $ extractedCatalogue ): MessageCatalogue
383414 {
384- $ currentCatalogue = new MessageCatalogue ($ extractedCatalogue ->getLocale ());
415+ $ translatedMessages = new MessageCatalogue ($ extractedCatalogue ->getLocale ());
385416
386417 foreach ($ extractedCatalogue ->getDomains () as $ domain ) {
387418 $ translations = $ this ->stringStorage ->getTranslations ([
388- 'language ' => $ currentCatalogue ->getLocale (),
419+ 'language ' => $ translatedMessages ->getLocale (),
389420 'context ' => PoItem::formatContext ($ domain ),
390421 ]);
391422 // Index by source
392423 $ translations = array_column ($ translations , null , 'source ' );
393424 foreach ($ extractedCatalogue ->all ($ domain ) as $ source => $ _ ) {
394425 if ($ translation = ($ translations [$ source ] ?? null )) {
395426 if ($ string = $ translation ->getString ()) {
396- $ currentCatalogue ->set ($ source , $ string , $ domain );
427+ $ translatedMessages ->set ($ source , $ string , $ domain );
397428 }
398429 }
399430 }
400431 }
401432
402- return $ currentCatalogue ;
433+ return $ translatedMessages ;
403434 }
404435
405436 private function removeNoFillTranslations (MessageCatalogueInterface $ operation ): void
@@ -421,15 +452,25 @@ private function removeNoFillTranslations(MessageCatalogueInterface $operation):
421452 private function getRootCodePaths (InputInterface $ input ): array
422453 {
423454 $ info = [];
424- $ source = $ input ->getOption ('source ' );
455+ $ source = $ input ->getArgument ('source ' );
456+
457+ if (empty ($ source )) {
458+ throw new InvalidArgumentException ('Argument "source" is required. ' );
459+ }
425460
426461 // Expand module and theme paths.
427462 $ source = preg_replace_callback (
428463 '/(module|theme):([a-z0-9_]+)/i ' ,
429464 function (array $ matches ) use (&$ info ): string {
430465 $ info [$ matches [1 ]] = $ matches [2 ];
431466
432- return $ this ->extensionPathResolver ->getPath ($ matches [1 ], $ matches [2 ]);
467+ $ path = $ this ->extensionPathResolver ->getPath ($ matches [1 ], $ matches [2 ]);
468+
469+ if (empty ($ path )) {
470+ throw new InvalidArgumentException (sprintf ('Invalid %s: %s ' , $ matches [1 ], $ matches [2 ]));
471+ }
472+
473+ return $ path ;
433474 },
434475 $ source ,
435476 );
0 commit comments