Skip to content

Commit ccee19b

Browse files
authored
feat: provide --flatten option for hoisting selectors (#22)
1 parent 66ee59e commit ccee19b

82 files changed

Lines changed: 1196 additions & 37 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

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

1010
### Added
1111

12+
- Provide `--flatten` extraction option to tell the extractor to hoist selectors and flatten sentences as much as possible. For example, `I have {count, plural, one{a dog} other{many dogs}}` becomes `{count, plural, one{I have a dog} other{I have many dogs}}`. The goal is to provide as many full sentences as possible, since fragmented sentences are not translator-friendly.
1213
- Provide `--add-missing-ids` extraction option to update source code with auto-generated identifiers
1314
- Add `Util\FormatHelper` that provides `getReader()` and `getWriter()` methods
1415
- Introduce `Format\Format` final static class for format constants

src/Console/Command/ExtractCommand.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,16 @@ protected function configure(): void
167167
'Whether to preserve whitespace and newlines in extracted '
168168
. 'messages.',
169169
)
170+
->addOption(
171+
'flatten',
172+
null,
173+
InputOption::VALUE_NONE,
174+
'Whether to hoist selectors & flatten sentences as much as possible, '
175+
. 'e.g: "I have {count, plural, one{a dog} other{many dogs}}" '
176+
. 'becomes "{count, plural, one{I have a dog} other{I have many '
177+
. 'dogs}}". The goal is to provide as many full sentences as '
178+
. 'possible, since fragmented sentences are not translator-friendly.',
179+
)
170180
->addOption(
171181
'add-missing-ids',
172182
null,
@@ -239,6 +249,7 @@ private function buildOptions(InputInterface $input): MessageExtractorOptions
239249
$options->extractSourceLocation = (bool) $input->getOption('extract-source-location');
240250
$options->throws = (bool) $input->getOption('throws');
241251
$options->preserveWhitespace = (bool) $input->getOption('preserve-whitespace');
252+
$options->flatten = (bool) $input->getOption('flatten');
242253
$options->addGeneratedIdsToSourceCode = (bool) $input->getOption('add-missing-ids');
243254

244255
/** @var string $inputFunctionNames */

src/Descriptor.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ public function getDefaultMessage(): ?string
7272
return $this->defaultMessage;
7373
}
7474

75+
public function setDefaultMessage(string $defaultMessage): void
76+
{
77+
$this->defaultMessage = $defaultMessage;
78+
}
79+
7580
public function getDescription(): ?string
7681
{
7782
return $this->description;

src/Extractor/MessageExtractor.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
namespace FormatPHP\Extractor;
2424

2525
use Closure;
26+
use FormatPHP\Descriptor;
2627
use FormatPHP\DescriptorCollection;
28+
use FormatPHP\DescriptorInterface;
2729
use FormatPHP\Exception\FormatPHPExceptionInterface;
2830
use FormatPHP\Exception\ImproperContextException;
2931
use FormatPHP\Exception\InvalidArgumentException;
@@ -34,6 +36,9 @@
3436
use FormatPHP\Extractor\Parser\ParserErrorCollection;
3537
use FormatPHP\Format\WriterInterface;
3638
use FormatPHP\Format\WriterOptions;
39+
use FormatPHP\Icu\MessageFormat\Manipulator;
40+
use FormatPHP\Icu\MessageFormat\Parser as MessageFormatParser;
41+
use FormatPHP\Icu\MessageFormat\Printer;
3742
use FormatPHP\Util\FileSystemHelper;
3843
use FormatPHP\Util\FormatHelper;
3944
use FormatPHP\Util\Globber;
@@ -62,6 +67,8 @@ class MessageExtractor
6267
private MessageExtractorOptions $options;
6368
private ParserErrorCollection $errors;
6469
private FormatHelper $formatHelper;
70+
private Manipulator $manipulator;
71+
private Printer $printer;
6572

6673
public function __construct(
6774
MessageExtractorOptions $options,
@@ -76,6 +83,8 @@ public function __construct(
7683
$this->file = $file;
7784
$this->formatHelper = $formatHelper;
7885
$this->errors = new ParserErrorCollection();
86+
$this->manipulator = new Manipulator();
87+
$this->printer = new Printer();
7988
}
8089

8190
/**
@@ -210,6 +219,12 @@ private function loadDescriptorParser(string $parserNameOrScript): DescriptorPar
210219
*/
211220
private function write(callable $formatter, DescriptorCollection $descriptors): void
212221
{
222+
if ($this->options->flatten === true) {
223+
/** @var DescriptorInterface[] $flattened */
224+
$flattened = $descriptors->map($this->flattenMessage())->toArray();
225+
$descriptors = new DescriptorCollection($flattened);
226+
}
227+
213228
$file = $this->options->outFile ?? 'php://output';
214229

215230
$writerOptions = new WriterOptions();
@@ -251,4 +266,21 @@ public function __invoke(
251266
}
252267
};
253268
}
269+
270+
private function flattenMessage(): Closure
271+
{
272+
return function (Descriptor $descriptor): Descriptor {
273+
$message = $descriptor->getDefaultMessage();
274+
$messageFormatParser = new MessageFormatParser((string) $message);
275+
$result = $messageFormatParser->parse();
276+
277+
/** @var MessageFormatParser\Type\ElementCollection $messageAst */
278+
$messageAst = $result->val;
279+
280+
$hoistedAst = $this->manipulator->hoistSelectors($messageAst);
281+
$descriptor->setDefaultMessage($this->printer->printAst($hoistedAst));
282+
283+
return $descriptor;
284+
};
285+
}
254286
}

src/Extractor/MessageExtractorOptions.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
namespace FormatPHP\Extractor;
2424

25+
use FormatPHP\Icu\MessageFormat\Manipulator;
26+
2527
/**
2628
* MessageExtractor options
2729
*/
@@ -77,6 +79,13 @@ class MessageExtractorOptions
7779
*/
7880
public bool $preserveWhitespace = false;
7981

82+
/**
83+
* Whether to hoist selectors and flatten sentences as much as possible
84+
*
85+
* @see Manipulator::hoistSelectors()
86+
*/
87+
public bool $flatten = false;
88+
8089
/**
8190
* Function and method names to parse from the application source code
8291
*
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
/**
4+
* This file is part of skillshare/formatphp
5+
*
6+
* skillshare/formatphp is open source software: you can distribute
7+
* it and/or modify it under the terms of the MIT License
8+
* (the "License"). You may not use this file except in
9+
* compliance with the License.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14+
* implied. See the License for the specific language governing
15+
* permissions and limitations under the License.
16+
*
17+
* @copyright Copyright (c) Skillshare, Inc. <https://www.skillshare.com>
18+
* @license https://opensource.org/licenses/MIT MIT License
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace FormatPHP\Icu\MessageFormat;
24+
25+
use FormatPHP\Icu\MessageFormat\Parser\Type\ElementCollection;
26+
use FormatPHP\Icu\MessageFormat\Parser\Type\PluralElement;
27+
use FormatPHP\Icu\MessageFormat\Parser\Type\SelectElement;
28+
29+
use function array_slice;
30+
use function array_values;
31+
32+
/**
33+
* Provides functionality to manipulate a parsed AST
34+
*
35+
* @internal
36+
*/
37+
class Manipulator
38+
{
39+
/**
40+
* Hoist all selectors to the beginning of the AST & flatten the
41+
* resulting options
42+
*
43+
* For example,
44+
*
45+
* I have {count, plural, one{a dog} other{many dogs}}
46+
*
47+
* becomes
48+
*
49+
* {count, plural, one{I have a dog} other{I have many dogs}}
50+
*
51+
* If there are multiple selectors, the order of which one is hoisted 1st
52+
* is non-deterministic.
53+
*
54+
* The goal is to provide as many full sentences as possible since
55+
* fragmented sentences are not translator-friendly.
56+
*/
57+
public function hoistSelectors(ElementCollection $ast): ElementCollection
58+
{
59+
for ($i = 0; $i < $ast->count(); $i++) {
60+
$element = $ast[$i];
61+
62+
if ($element instanceof PluralElement || $element instanceof SelectElement) {
63+
$cloned = clone $element;
64+
$options = $cloned->options;
65+
foreach ($options as $option) {
66+
$option->value = $this->hoistSelectors(new ElementCollection([
67+
...array_values(array_slice($ast->toArray(), 0, $i)),
68+
...array_values($option->value->toArray()),
69+
...array_values(array_slice($ast->toArray(), $i + 1)),
70+
]));
71+
}
72+
73+
return new ElementCollection([$cloned]);
74+
}
75+
}
76+
77+
return $ast;
78+
}
79+
}

src/Icu/MessageFormat/Parser/Type/AbstractElement.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424

2525
abstract class AbstractElement implements ElementInterface
2626
{
27+
use DeepCloner;
28+
2729
public ElementType $type;
28-
public ?string $value = null;
29-
public ?Location $location = null;
30+
public string $value;
31+
public Location $location;
3032
}

src/Icu/MessageFormat/Parser/Type/AbstractSkeleton.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
abstract class AbstractSkeleton implements SkeletonInterface
2626
{
27+
use DeepCloner;
28+
2729
public SkeletonType $type;
2830
public Location $location;
2931
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
/**
4+
* This file is part of skillshare/formatphp
5+
*
6+
* skillshare/formatphp is open source software: you can distribute
7+
* it and/or modify it under the terms of the MIT License
8+
* (the "License"). You may not use this file except in
9+
* compliance with the License.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14+
* implied. See the License for the specific language governing
15+
* permissions and limitations under the License.
16+
*
17+
* @copyright Copyright (c) Skillshare, Inc. <https://www.skillshare.com>
18+
* @license https://opensource.org/licenses/MIT MIT License
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
namespace FormatPHP\Icu\MessageFormat\Parser\Type;
24+
25+
use ReflectionObject;
26+
27+
use function is_array;
28+
use function is_object;
29+
30+
trait DeepCloner
31+
{
32+
public function __clone()
33+
{
34+
$this->cloneMyProperties();
35+
}
36+
37+
private function cloneMyProperties(): void
38+
{
39+
$reflection = new ReflectionObject($this);
40+
41+
foreach ($reflection->getProperties() as $reflectionProperty) {
42+
/** @var mixed $propertyValue */
43+
$propertyValue = $reflectionProperty->getValue($this);
44+
$reflectionProperty->setValue($this, $this->cloneValue($propertyValue));
45+
}
46+
}
47+
48+
/**
49+
* @param mixed $value
50+
*
51+
* @return mixed The cloned value
52+
*/
53+
private function cloneValue($value)
54+
{
55+
if (is_array($value)) {
56+
return $this->cloneArray($value);
57+
}
58+
59+
if (is_object($value)) {
60+
return clone $value;
61+
}
62+
63+
return $value;
64+
}
65+
66+
/**
67+
* @param array<array-key, mixed> $value
68+
*
69+
* @return mixed[]
70+
*
71+
* @psalm-suppress MixedAssignment
72+
*/
73+
private function cloneArray(array $value): array
74+
{
75+
/** @var mixed[] $clone */
76+
$clone = [];
77+
78+
foreach ($value as $k => $v) {
79+
$clone[$k] = $this->cloneValue($v);
80+
}
81+
82+
return $clone;
83+
}
84+
}

src/Icu/MessageFormat/Parser/Type/ElementCollection.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,15 @@ public function jsonSerialize()
6565
{
6666
return $this->toArray();
6767
}
68+
69+
public function __clone()
70+
{
71+
$items = [];
72+
73+
foreach ($this->data as $datum) {
74+
$items[] = clone $datum;
75+
}
76+
77+
$this->data = $items;
78+
}
6879
}

0 commit comments

Comments
 (0)