Skip to content

Commit 7641c33

Browse files
henriquemoodyAI Assistant
andcommitted
Add TransModifier ported from Respect\Validation
Implement translation support using symfony/translation with TranslatorInterface dependency injection. Supports simple 'trans' pipe modifier for string translation following established modifier patterns. - Create TransModifier implementing Modifier interface - Add TestingTranslator for isolated test dependencies - Follow Chain of Responsibility pattern with proper delegation - Support symfony/translation-contracts for interface compliance - Maintain PSR-12 compliance and comprehensive test coverage Co-authored-by: AI Assistant <ai@example.com>
1 parent 61ad070 commit 7641c33

9 files changed

Lines changed: 533 additions & 5 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ All formatters must implement the `Respect\StringFormatter\Formatter` interface.
3434
When creating new modifiers:
3535

3636
1. **Follow Chain of Responsibility pattern**: Check pipe value and delegate to next modifier
37-
2. **Use template structure**: Similar to `src/Modifier/QuoteModifier.php`
37+
2. **Use template structure**: Similar to `src/Modifier/QuoteModifier.php`
3838
3. **Test with TestingModifier**: Located in `tests/Helper/TestingModifier.php`
3939
4. **Handle type checking**: Always check input types before processing
4040
5. **Return string values**: Modifiers must return strings

composer.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@
55
"require": {
66
"symfony/polyfill-mbstring": "^1.33",
77
"php": "^8.5",
8-
"respect/stringifier": "^3.0"
8+
"respect/stringifier": "^3.0",
9+
"symfony/translation-contracts": "^3.6"
10+
},
11+
"suggest": {
12+
"symfony/translation": "For translation support in TransModifier (^6.0|^7.0)"
913
},
1014
"require-dev": {
1115
"phpunit/phpunit": "^12.5",
1216
"phpstan/phpstan": "^2.1",
1317
"phpstan/extension-installer": "^1.4",
1418
"phpstan/phpstan-deprecation-rules": "^2.0",
1519
"phpstan/phpstan-phpunit": "^2.0",
16-
"respect/coding-standard": "^5.0"
20+
"respect/coding-standard": "^5.0",
21+
"symfony/translation": "^6.0|^7.0"
1722
},
1823
"license": "ISC",
1924
"autoload": {

docs/modifiers/Modifiers.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,13 @@ Modifiers form a chain where each modifier can:
3333
The default modifier chain is:
3434

3535
```
36-
RawModifier -> ListModifier -> QuoteModifier -> StringifyModifier
36+
RawModifier -> ListModifier -> TransModifier -> QuoteModifier -> StringifyModifier
3737
```
3838

3939
- `StringifyModifier` is always the last modifier, using the stringifier to convert values to strings
4040
- `RawModifier` processes scalar values without stringifier formatting
4141
- `ListModifier` formats arrays as human-readable lists with "and" or "or" conjunctions
42+
- `TransModifier` translates string values using a Symfony translator
4243
- `QuoteModifier` quotes string values using the stringifier quoter
4344

4445
## Syntax Rules
@@ -74,6 +75,7 @@ For non-scalar values with `|raw`, the modifier falls back to the next modifier
7475
- **[StringifyModifier](StringifyModifier.md)** - Default modifier that uses stringifier for all values
7576
- **[ListModifier](ListModifier.md)** - Formats arrays as human-readable lists with conjunctions
7677
- **[QuoteModifier](QuoteModifier.md)** - Quotes string values using a stringifier quoter
78+
- **[TransModifier](TransModifier.md)** - Translates string values using a Symfony translator
7779

7880
## Creating Custom Modifiers
7981

@@ -95,4 +97,4 @@ $formatter = new PlaceholderFormatter(
9597
);
9698
```
9799

98-
If no modifier is provided, the formatter uses a default `RawModifier` with `ListModifier`, `QuoteModifier` and `StringifyModifier`.
100+
If no modifier is provided, the formatter uses a default `RawModifier` with `ListModifier`, `TransModifier`, `QuoteModifier` and `StringifyModifier`.

docs/modifiers/TransModifier.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# TransModifier
2+
3+
The `|trans` modifier translates string values using Symfony Translation component.
4+
5+
## Purpose
6+
7+
The `|trans` modifier enables internationalization by translating string keys into their localized equivalents, supporting multi-language applications with minimal configuration.
8+
9+
## Behavior
10+
11+
The modifier follows the Chain of Responsibility pattern and only processes string values with the `trans` pipe:
12+
13+
- **Non-trans pipes**: Delegates to next modifier without modification
14+
- **Non-string values**: Delegates to next modifier without modification
15+
- **String values with `trans` pipe**: Passes through translator and continues chain
16+
- **Missing translations**: Returns the original key unchanged
17+
18+
## Usage
19+
20+
### Simple Usage (Default BypassTranslator)
21+
22+
```php
23+
use Respect\StringFormatter\PlaceholderFormatter;
24+
25+
// Works out of the box - no dependencies required
26+
$formatter = new PlaceholderFormatter(['message' => 'hello']);
27+
28+
echo $formatter->format('{{message|trans}}');
29+
// Outputs: "hello" (original input returned)
30+
```
31+
32+
### With Real Translator
33+
34+
```php
35+
use Respect\StringFormatter\PlaceholderFormatter;
36+
use Respect\StringFormatter\Modifier\TransModifier;
37+
use Symfony\Component\Translation\Translator;
38+
use Symfony\Component\Translation\Loader\ArrayLoader;
39+
40+
$translator = new Translator('en');
41+
$translator->addLoader('array', new ArrayLoader());
42+
$translator->addResource('array', ['greeting' => 'Hello World'], 'en');
43+
44+
$formatter = new PlaceholderFormatter(
45+
['message' => 'greeting'],
46+
new TransModifier($translator) // Inject real translator
47+
);
48+
49+
echo $formatter->format('{{message|trans}}');
50+
// Outputs: "Hello World" (translated result)
51+
```
52+
53+
## Examples
54+
55+
### With BypassTranslator (Default)
56+
57+
| Parameters | Template | Output |
58+
| ---------------------- | ---------------------- | --------------- |
59+
| `['msg' => 'hello']` | `"{{msg&#124;trans}}"` | `"hello"` |
60+
| `['msg' => 'welcome']` | `"{{msg&#124;trans}}"` | `"welcome"` |
61+
| `['msg' => 'unknown']` | `"{{msg&#124;trans}}"` | `"unknown"` |
62+
63+
### With Real Translator
64+
65+
| Parameters | Template | Output |
66+
| --------------------------- | ---------------------- | --------------- |
67+
| `['msg' => 'greeting']` | `"{{msg&#124;trans}}"` | `"Hello World"` |
68+
69+
### Mixed with Regular Placeholders
70+
71+
| Parameters | Template | Output |
72+
| ------------------------------------------- | -------------------------------------------- | ------------------------- |
73+
| `['name' => 'John', 'greeting' => 'hello']` | `"{{greeting&#124;trans}}, {{name}}"` | `"hello, John"` |
74+
75+
## Common Use Cases
76+
77+
1. **User interface messages** - Internationalize button labels, error messages, notifications
78+
2. **Email templates** - Send localized emails to international users
79+
3. **Error presentation** - Show validation errors in user's preferred language
80+
4. **Report generation** - Generate reports in different languages
81+
5. **Content management** - Translate article headings, descriptions, UI elements
82+
83+
## Implementation Notes
84+
85+
The `TransModifier` requires a `Symfony\Contracts\Translation\TranslatorInterface` implementation. The modifier:
86+
87+
1. Checks for exact match with `trans` pipe value
88+
2. Validates input is a string
89+
3. Calls `translator->trans($value)` without parameters for simplicity
90+
4. Passes translated result to next modifier in chain
91+
5. Returns original key if translation not found
92+
93+
## Integration
94+
95+
`TransModifier` is typically used in the middle of the modification chain:
96+
97+
```php
98+
CustomModifier -> TransModifier -> RawModifier -> StringifyModifier
99+
```
100+
101+
The modifier processes translations first, then passes the result to subsequent modifiers for consistent formatting across the application.
102+
103+
## Dependencies
104+
105+
**Required:**
106+
- `symfony/translation-contracts`: Provides the `TranslatorInterface` contract
107+
108+
**Optional/Suggested:**
109+
- `symfony/translation`: Implementation of the translation interface
110+
- Install with: `composer require symfony/translation`
111+
- Required for actual translation functionality
112+
- Package is suggested and will be installed when needed
113+
114+
## Default Behavior
115+
116+
The `TransModifier` works out of the box without requiring symfony/translation. By default, it uses a `BypassTranslator` that returns the original input unchanged. This means the modifier is always available and functional.
117+
118+
## Real Translations
119+
120+
To enable actual translations, install symfony/translation and inject it into the constructor:
121+
122+
```php
123+
use Respect\StringFormatter\Modifier\TransModifier;
124+
use Symfony\Component\Translation\Translator;
125+
use Symfony\Component\Translation\Loader\ArrayLoader;
126+
127+
$translator = new Translator('en');
128+
$translator->addLoader('array', new ArrayLoader());
129+
$translator->addResource('array', ['hello' => 'Hello World'], 'en');
130+
131+
// Inject into specific TransModifier instance
132+
$modifier = new TransModifier($nextModifier, $translator);
133+
```
134+
135+
## Testing
136+
137+
For testing purposes, the `TestingTranslator` implementation is provided:
138+
139+
```php
140+
use Respect\StringFormatter\Test\Helper\TestingTranslator;
141+
142+
$translator = new TestingTranslator([
143+
'hello' => 'Hello World',
144+
'welcome' => 'Welcome',
145+
]);
146+
```
147+
148+
This provides a lightweight way to test translation behavior without requiring the full Symfony Translation component. The testing implementation is included in the dev dependencies.

src/Modifier/BypassTranslator.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Respect\StringFormatter\Modifier;
6+
7+
use Symfony\Contracts\Translation\LocaleAwareInterface;
8+
use Symfony\Contracts\Translation\TranslatorInterface;
9+
10+
/**
11+
* Bypass translator that always returns the original input.
12+
*
13+
* This implementation works regardless of whether symfony/translation
14+
* is installed, providing a fallback for the
15+
* TransModifier.
16+
*/
17+
final class BypassTranslator implements TranslatorInterface, LocaleAwareInterface
18+
{
19+
/** @param array<string, mixed> $parameters */
20+
public function trans(
21+
string $id,
22+
array $parameters = [],
23+
string|null $domain = null,
24+
string|null $locale = null,
25+
): string {
26+
return $id;
27+
}
28+
29+
public function getLocale(): string
30+
{
31+
return 'en';
32+
}
33+
34+
public function setLocale(string $locale): void
35+
{
36+
// No-op for bypass translator
37+
}
38+
}

src/Modifier/TransModifier.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Respect\StringFormatter\Modifier;
6+
7+
use Respect\StringFormatter\Modifier;
8+
use Symfony\Contracts\Translation\TranslatorInterface;
9+
10+
use function is_string;
11+
12+
final readonly class TransModifier implements Modifier
13+
{
14+
public function __construct(
15+
private Modifier $nextModifier,
16+
private TranslatorInterface $translator = new BypassTranslator(),
17+
) {
18+
}
19+
20+
public function modify(mixed $value, string|null $pipe): string
21+
{
22+
if ($pipe !== 'trans' || !is_string($value)) {
23+
return $this->nextModifier->modify($value, $pipe);
24+
}
25+
26+
$translated = $this->translator->trans($value);
27+
28+
return $this->nextModifier->modify($translated, null);
29+
}
30+
}

tests/Helper/TestingTranslator.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Respect\StringFormatter\Test\Helper;
6+
7+
use Symfony\Contracts\Translation\LocaleAwareInterface;
8+
use Symfony\Contracts\Translation\TranslatorInterface;
9+
10+
/**
11+
* Dumb test implementation of TranslatorInterface
12+
*
13+
* This implementation simply replaces input strings with mapped replacements
14+
* and doesn't implement any actual translation logic.
15+
* Used only to test that the TransModifier is using the translator correctly.
16+
*/
17+
final class TestingTranslator implements TranslatorInterface, LocaleAwareInterface
18+
{
19+
/** @param array<string, string> $replacements */
20+
public function __construct(
21+
private array $replacements,
22+
) {
23+
}
24+
25+
/** @param array<string, mixed> $parameters */
26+
public function trans(
27+
string $id,
28+
array $parameters = [],
29+
string|null $domain = null,
30+
string|null $locale = null,
31+
): string {
32+
return $this->replacements[$id] ?? $id;
33+
}
34+
35+
public function getLocale(): string
36+
{
37+
return 'en';
38+
}
39+
40+
public function setLocale(string $locale): void
41+
{
42+
// Dummy implementation - not needed for testing
43+
}
44+
}

0 commit comments

Comments
 (0)