Skip to content

Commit faad3ea

Browse files
committed
Add TransModifier to support localized template interpolation
Another need we had in Respect\Validation was the ability to translate parameters into a different language, and I wanted to port that functionality to this library as well. I’ve ported the `TransModifier` from Validation, allowing us to bridge the gap between template interpolation and localization. Instead of reinventing the wheel, I’ve integrated this modifier with `symfony/translation`. By relying on the TranslatorInterface, we ensure robust translation ecosystems without being locked into a specific implementation. Assisted-by: OpenCode (GLM-4.6) Assisted-by: Gemini 3 (Thinking)
1 parent b9c6859 commit faad3ea

9 files changed

Lines changed: 486 additions & 3 deletions

File tree

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ If no modifier is provided, the formatter uses `StringifyModifier` by default.
5252
- **[ListModifier](ListModifier.md)** - Formats arrays as human-readable lists with conjunctions
5353
- **[QuoteModifier](QuoteModifier.md)** - Quotes string values using a stringifier quoter
5454
- **[StringifyModifier](StringifyModifier.md)** - Converts values to strings (default)
55+
- **[TransModifier](TransModifier.md)** - Translates string values using a Symfony translator
5556

5657
## Creating Custom Modifiers
5758

docs/modifiers/TransModifier.md

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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($nextModifier, $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|trans}}"` | `"hello"` |
60+
| `['msg' => 'welcome']` | `"{{msg|trans}}"` | `"welcome"` |
61+
| `['msg' => 'unknown']` | `"{{msg|trans}}"` | `"unknown"` |
62+
63+
### With Real Translator
64+
65+
| Parameters | Template | Output |
66+
| ----------------------- | ---------------------- | --------------- |
67+
| `['msg' => 'greeting']` | `"{{msg|trans}}"` | `"Hello World"` |
68+
69+
### Mixed with Regular Placeholders
70+
71+
| Parameters | Template | Output |
72+
| ------------------------------------------- | ------------------------------------- | --------------- |
73+
| `['name' => 'John', 'greeting' => 'hello']` | `"{{greeting|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+
107+
- `symfony/translation-contracts`: Provides the `TranslatorInterface` contract
108+
109+
**Optional/Suggested:**
110+
111+
- `symfony/translation`: Implementation of the translation interface
112+
- Install with: `composer require symfony/translation`
113+
- Required for actual translation functionality
114+
- Package is suggested and will be installed when needed
115+
116+
## Default Behavior
117+
118+
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.
119+
120+
## Real Translations
121+
122+
To enable actual translations, install symfony/translation and inject it into the constructor:
123+
124+
```php
125+
use Respect\StringFormatter\Modifier\TransModifier;
126+
use Symfony\Component\Translation\Translator;
127+
use Symfony\Component\Translation\Loader\ArrayLoader;
128+
129+
$translator = new Translator('en');
130+
$translator->addLoader('array', new ArrayLoader());
131+
$translator->addResource('array', ['hello' => 'Hello World'], 'en');
132+
133+
// Inject into specific TransModifier instance
134+
$modifier = new TransModifier($nextModifier, $translator);
135+
```
136+
137+
## Testing
138+
139+
For testing purposes, the `TestingTranslator` implementation is provided:
140+
141+
```php
142+
use Respect\StringFormatter\Test\Helper\TestingTranslator;
143+
144+
$translator = new TestingTranslator([
145+
'hello' => 'Hello World',
146+
'welcome' => 'Welcome',
147+
]);
148+
```
149+
150+
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/BypassTranslator.php

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

src/Modifiers/TransModifier.php

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

src/PlaceholderFormatter.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Respect\StringFormatter\Modifiers\ListModifier;
88
use Respect\StringFormatter\Modifiers\QuoteModifier;
99
use Respect\StringFormatter\Modifiers\StringifyModifier;
10+
use Respect\StringFormatter\Modifiers\TransModifier;
1011

1112
use function array_key_exists;
1213
use function is_string;
@@ -17,7 +18,7 @@
1718
/** @param array<string, mixed> $parameters */
1819
public function __construct(
1920
private array $parameters,
20-
private Modifier $modifier = new QuoteModifier(new ListModifier(new StringifyModifier())),
21+
private Modifier $modifier = new TransModifier(new QuoteModifier(new ListModifier(new StringifyModifier()))),
2122
) {
2223
}
2324

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> $translations */
20+
public function __construct(
21+
private array $translations = [],
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->translations[$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)