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,53 @@ 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+ if (!is_file ($ path )) {
392+ throw new InvalidArgumentException (sprintf ('Message file "%s" does not exist. ' , $ path ));
393+ }
394+
395+ $ currentCatalogue = new MessageCatalogue ($ locale );
396+ $ this ->reader ->setURI ($ path );
397+ $ this ->reader ->open ();
398+ while ($ item = $ this ->reader ->readItem ()) {
399+ if (!empty ($ item ->getTranslation ())) {
400+ $ currentCatalogue ->set (
401+ PoItem::joinStrings ((array ) $ item ->getSource ()),
402+ PoItem::joinStrings ((array ) $ item ->getTranslation ()),
403+ PoItem::fromContext ($ item ->getContext ()),
404+ );
405+ }
406+ }
407+ $ this ->reader ->close ();
408+
409+ return $ currentCatalogue ;
410+ }
411+
382412 private function loadTranslatedMessages (MessageCatalogue $ extractedCatalogue ): MessageCatalogue
383413 {
384- $ currentCatalogue = new MessageCatalogue ($ extractedCatalogue ->getLocale ());
414+ $ translatedMessages = new MessageCatalogue ($ extractedCatalogue ->getLocale ());
385415
386416 foreach ($ extractedCatalogue ->getDomains () as $ domain ) {
387417 $ translations = $ this ->stringStorage ->getTranslations ([
388- 'language ' => $ currentCatalogue ->getLocale (),
418+ 'language ' => $ translatedMessages ->getLocale (),
389419 'context ' => PoItem::formatContext ($ domain ),
390420 ]);
391421 // Index by source
392422 $ translations = array_column ($ translations , null , 'source ' );
393423 foreach ($ extractedCatalogue ->all ($ domain ) as $ source => $ _ ) {
394424 if ($ translation = ($ translations [$ source ] ?? null )) {
395425 if ($ string = $ translation ->getString ()) {
396- $ currentCatalogue ->set ($ source , $ string , $ domain );
426+ $ translatedMessages ->set ($ source , $ string , $ domain );
397427 }
398428 }
399429 }
400430 }
401431
402- return $ currentCatalogue ;
432+ return $ translatedMessages ;
403433 }
404434
405435 private function removeNoFillTranslations (MessageCatalogueInterface $ operation ): void
@@ -421,15 +451,25 @@ private function removeNoFillTranslations(MessageCatalogueInterface $operation):
421451 private function getRootCodePaths (InputInterface $ input ): array
422452 {
423453 $ info = [];
424- $ source = $ input ->getOption ('source ' );
454+ $ source = $ input ->getArgument ('source ' );
455+
456+ if (empty ($ source )) {
457+ throw new InvalidArgumentException ('Argument "source" is required. ' );
458+ }
425459
426460 // Expand module and theme paths.
427461 $ source = preg_replace_callback (
428462 '/(module|theme):([a-z0-9_]+)/i ' ,
429463 function (array $ matches ) use (&$ info ): string {
430464 $ info [$ matches [1 ]] = $ matches [2 ];
431465
432- return $ this ->extensionPathResolver ->getPath ($ matches [1 ], $ matches [2 ]);
466+ $ path = $ this ->extensionPathResolver ->getPath ($ matches [1 ], $ matches [2 ]);
467+
468+ if (empty ($ path )) {
469+ throw new InvalidArgumentException (sprintf ('Invalid %s: %s ' , $ matches [1 ], $ matches [2 ]));
470+ }
471+
472+ return $ path ;
433473 },
434474 $ source ,
435475 );
0 commit comments