Skip to content

Commit 58b9c8d

Browse files
committed
Added standalone command
1 parent e427fc9 commit 58b9c8d

7 files changed

Lines changed: 241 additions & 8 deletions

File tree

.github/workflows/php.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,4 @@ jobs:
5858
docker compose run --rm phpfpm composer install
5959
# https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/doc/usage.rst#the-check-command
6060
docker compose run --rm phpfpm vendor/bin/php-cs-fixer fix --dry-run --diff
61+
docker compose run --rm phpfpm vendor/bin/php-cs-fixer fix --dry-run --diff bin/drupal-translation-extract

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
* [PR-7](https://github.com/itk-dev/drupal_translation_extractor/pull/7)
11+
Added standalone command for running outside Drupal context
12+
1013
## [1.1.0] - 2026-02-02
1114

1215
* [PR-4](https://github.com/itk-dev/drupal_translation_extractor/pull/4)

README.md

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ This Drupal translation extractor stands on the shoulders of giants:
1111
## Installation
1212

1313
``` shell
14-
composer require --dev itk-dev/drupal_translation_extractor:^1.0
14+
composer require --dev itk-dev/drupal_translation_extractor
1515
drush pm:install drupal_translation_extractor
1616
```
1717

18-
## Use
18+
## Usage
1919

2020
The main entrypoint is the `drupal_translation_extractor:translation:extract` Drush command. This command is basically
2121
Symfony's [`translation:extract` console
@@ -38,7 +38,10 @@ A new argument has been added:
3838
in the value and will be expanded to the full path to the module and/or theme respectively, i.e.
3939
`module:my_custom_module` will be expanded to `web/modules/custom/my_custom_module`, say.
4040

41-
A new option has been added:
41+
New options have been added:
42+
43+
`--project-name` Then project name, e.g. `My Drupal module`. If not set, a project name will be computed based on any
44+
module or theme references in the source path.
4245

4346
`--output` The output path. The value can use these placeholders:
4447

@@ -55,17 +58,34 @@ A new option has been added:
5558
[^1]: Matching placeholders used the Locale module (cf.
5659
[locale.api.php](https://git.drupalcode.org/project/drupal/-/blob/11.x/core/modules/locale/locale.api.php)).
5760

61+
`--fill-from-string-storage` If set, the generated translation files will be filled with translations from Drupal's
62+
string storage.
63+
5864
### Example
5965

6066
Running
6167

6268
``` shell
63-
drush drupal_translation_extractor:translation:extract da --dump-messages --force module:my_modules --output=%source/translation/%module.%locale.po
69+
drush drupal_translation_extractor:translation:extract da --dump-messages --sort asc --force module:my_modules --output=%source/translation/%module.%locale.po
6470
```
6571

6672
will find translations in all PHP and Twig files in the `web/modules/custom/my_module` directory and write the result to
6773
`web/modules/custom/my_module/translation/my_module.da.po`.
6874

69-
> [!NOTE]
70-
> Much of the code in this module is ~stolen from~based on Symfony components and therefore we do not use Drupal coding
71-
> standards.
75+
## Usage outside Drupal context
76+
77+
It may be necessary to run the Drupal translation extractor outside Drupal context, e.g when developing a Drupal module:
78+
79+
``` shell
80+
composer require --dev itk-dev/drupal_translation_extractor
81+
./vendor/bin/drupal-translation-extract da --dump-messages --sort asc .
82+
```
83+
84+
> [!CAUTION]
85+
> The `--fill-from-string-storage` options cannot be used when running outside Drupal context. Nor can `module:` and
86+
> `theme:` placeholders be used in the `source` argument.
87+
88+
## Development
89+
90+
Much of the code in this module is ~stolen from~based on Symfony components and therefore we do not use Drupal coding
91+
standards.

Taskfile.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ tasks:
8484
cmds:
8585
# Cf. .github/workflows/php.yaml
8686
- docker compose run --rm phpfpm vendor/bin/php-cs-fixer fix
87+
- docker compose run --rm phpfpm vendor/bin/php-cs-fixer fix bin/drupal-translation-extract
8788
silent: true
8889

8990
coding-standards:php:check:
@@ -92,6 +93,7 @@ tasks:
9293
- task: coding-standards:php:apply
9394
# Cf. .github/workflows/php.yaml
9495
- docker compose run --rm phpfpm vendor/bin/php-cs-fixer fix --dry-run --diff
96+
- docker compose run --rm phpfpm vendor/bin/php-cs-fixer fix --dry-run --diff bin/drupal-translation-extract
9597
silent: true
9698

9799
coding-standards:styles:apply:

bin/drupal-translation-extract

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
// @see https://getcomposer.org/doc/articles/vendor-binaries.md#finding-the-composer-autoloader-from-a-binary
5+
require $_composer_autoload_path ?? __DIR__.'/../vendor/autoload.php';
6+
7+
use Drupal\Component\Gettext\PoStreamReader;
8+
use Drupal\Core\Extension\ExtensionPathResolver;
9+
use Drupal\Core\Template\TwigTransTokenParser;
10+
use Drupal\drupal_translation_extractor\Command\TranslationExtractCommand;
11+
use Drupal\drupal_translation_extractor\Translation\Dumper\PoFileDumper;
12+
use Drupal\drupal_translation_extractor\Translation\Extractor\PhpExtractor;
13+
use Drupal\drupal_translation_extractor\Translation\Extractor\Visitor\TranslatableMarkupVisitor;
14+
use Drupal\drupal_translation_extractor\Translation\Extractor\Visitor\TransMethodVisitor;
15+
use Drupal\drupal_translation_extractor\Twig\Extension\ItkTranslationExtractorTwigExtension;
16+
use Drupal\drupal_translation_extractor\Twig\Translation\Extractor\TwigExtractor;
17+
use Drupal\locale\StringStorageInterface;
18+
use Symfony\Component\Console\Application;
19+
use Symfony\Component\Console\Command\Command;
20+
use Symfony\Component\Console\Exception\RuntimeException;
21+
use Symfony\Component\Console\Input\ArgvInput;
22+
use Symfony\Component\Console\Input\InputInterface;
23+
use Symfony\Component\Console\Output\OutputInterface;
24+
use Symfony\Component\Translation\Extractor\ChainExtractor;
25+
use Symfony\Component\Translation\Writer\TranslationWriter;
26+
use Twig\Environment;
27+
use Twig\Loader\FilesystemLoader;
28+
29+
$application = createApplication();
30+
$command = createCommand();
31+
32+
$application->addCommand($command);
33+
34+
$application->setDefaultCommand($command->getName(), true);
35+
$application->run();
36+
37+
/**
38+
* Create the application with some bespoke exception handling.
39+
*/
40+
function createApplication(): Application
41+
{
42+
return new class extends Application {
43+
public function __construct()
44+
{
45+
parent::__construct('drupal_translation_extractor', '1.0.0');
46+
}
47+
48+
protected function doRunCommand(
49+
Command $command,
50+
InputInterface $input,
51+
OutputInterface $output,
52+
): int {
53+
$tempInput = new ArgvInput();
54+
try {
55+
$tempInput->bind($command->getDefinition());
56+
} catch (RuntimeException) {
57+
}
58+
59+
if (false !== $tempInput->getOption('fill-from-string-storage')) {
60+
parent::renderThrowable(
61+
new RuntimeException('Option --fill-from-string-storage cannot be used when running outside Drupal context.'),
62+
$output
63+
);
64+
65+
return Command::FAILURE;
66+
}
67+
68+
return parent::doRunCommand($command, $input, $output);
69+
}
70+
71+
public function renderThrowable(Throwable $e, OutputInterface $output): void
72+
{
73+
if ($e instanceof ExtensionPathResolverException) {
74+
parent::renderThrowable(
75+
new RuntimeException('Cannot resolve module and theme paths when running outside Drupal context.'),
76+
$output
77+
);
78+
} else {
79+
parent::renderThrowable($e, $output);
80+
}
81+
}
82+
};
83+
}
84+
85+
/**
86+
* Create the command with injected dependencies.
87+
*/
88+
function createCommand(): TranslationExtractCommand
89+
{
90+
$twig = new Environment(new FilesystemLoader());
91+
$trans = fn () => null;
92+
$twig->addFilter(new Twig\TwigFilter('t', $trans));
93+
$twig->addFilter(new Twig\TwigFilter('trans', $trans));
94+
$twig->addExtension(new ItkTranslationExtractorTwigExtension());
95+
$twig->addTokenParser(new TwigTransTokenParser());
96+
97+
$writer = new TranslationWriter();
98+
$writer->addDumper('po', new PoFileDumper());
99+
100+
$reader = new PoStreamReader();
101+
102+
$extractor = new ChainExtractor();
103+
$extractor->addExtractor('php',
104+
new PhpExtractor(visitors: [
105+
new TransMethodVisitor(),
106+
new TranslatableMarkupVisitor(),
107+
]));
108+
$extractor->addExtractor('twig', new TwigExtractor($twig));
109+
110+
$extensionPathResolver = new class extends ExtensionPathResolver {
111+
public function __construct()
112+
{
113+
// No call to parent::__construct() here.
114+
}
115+
116+
public function getPathname(string $type, string $name): ?string
117+
{
118+
throw new ExtensionPathResolverException(__FUNCTION__);
119+
}
120+
};
121+
122+
$stringStorage = new class implements StringStorageInterface {
123+
public function getStrings(array $conditions = [], array $options = [])
124+
{
125+
throw new StringStorageRuntimeException(__FUNCTION__);
126+
}
127+
128+
public function getTranslations(array $conditions = [], array $options = [])
129+
{
130+
throw new StringStorageRuntimeException(__FUNCTION__);
131+
}
132+
133+
public function getLocations(array $conditions = [])
134+
{
135+
throw new StringStorageRuntimeException(__FUNCTION__);
136+
}
137+
138+
public function findString(array $conditions)
139+
{
140+
throw new StringStorageRuntimeException(__FUNCTION__);
141+
}
142+
143+
public function findTranslation(array $conditions)
144+
{
145+
throw new StringStorageRuntimeException(__FUNCTION__);
146+
}
147+
148+
public function save($string)
149+
{
150+
throw new StringStorageRuntimeException(__FUNCTION__);
151+
}
152+
153+
public function delete($string)
154+
{
155+
throw new StringStorageRuntimeException(__FUNCTION__);
156+
}
157+
158+
public function deleteStrings($conditions)
159+
{
160+
throw new StringStorageRuntimeException(__FUNCTION__);
161+
}
162+
163+
public function deleteTranslations($conditions)
164+
{
165+
throw new StringStorageRuntimeException(__FUNCTION__);
166+
}
167+
168+
public function countStrings()
169+
{
170+
throw new StringStorageRuntimeException(__FUNCTION__);
171+
}
172+
173+
public function countTranslations()
174+
{
175+
throw new StringStorageRuntimeException(__FUNCTION__);
176+
}
177+
178+
public function createString($values = [])
179+
{
180+
throw new StringStorageRuntimeException(__FUNCTION__);
181+
}
182+
183+
public function createTranslation($values = [])
184+
{
185+
throw new StringStorageRuntimeException(__FUNCTION__);
186+
}
187+
};
188+
189+
return new TranslationExtractCommand($writer, $reader, $extractor, $extensionPathResolver, $stringStorage);
190+
}
191+
192+
class ExtensionPathResolverException extends \RuntimeException
193+
{
194+
}
195+
196+
class StringStorageRuntimeException extends \RuntimeException
197+
{
198+
}

composer.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"require": {
77
"php": "^8.3",
88
"drupal/core": "^10 || ^11",
9+
"nikic/php-parser": "^4.0 || ^5.0",
910
"symfony/translation": "^6 || ^7 || ^8",
1011
"symfony/twig-bridge": "^6 || ^7 || ^8"
1112
},
@@ -16,13 +17,20 @@
1617
"phpunit/phpunit": "^12.5",
1718
"vincentlanglet/twig-cs-fixer": "^3.11"
1819
},
20+
"autoload": {
21+
"psr-4": {
22+
"Drupal\\drupal_translation_extractor\\": "src/"
23+
}
24+
},
1925
"autoload-dev": {
2026
"psr-4": {
21-
"Drupal\\drupal_translation_extractor\\": "src/",
2227
"Drupal\\drupal_translation_extractor\\Test\\": "tests/",
2328
"Drupal\\locale\\": "vendor/drupal/core/modules/locale/src/"
2429
}
2530
},
31+
"bin": [
32+
"bin/drupal-translation-extract"
33+
],
2634
"config": {
2735
"allow-plugins": {
2836
"drupal/core-composer-scaffold": false,

phpstan.dist.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ parameters:
33
paths:
44
- src
55
- tests
6+
- bin
67
excludePaths:
78
- tests/resources/*
89

0 commit comments

Comments
 (0)