Skip to content

Commit c690f72

Browse files
committed
feat: make translation errors translatable
1 parent 6d62b49 commit c690f72

File tree

126 files changed

+1528
-755
lines changed

Some content is hidden

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

126 files changed

+1528
-755
lines changed

packages/console/src/Components/InteractiveComponentRenderer.php

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
use Tempest\Console\InteractiveConsoleComponent;
1212
use Tempest\Console\Key;
1313
use Tempest\Console\Terminal\Terminal;
14+
use Tempest\Intl\Translator;
1415
use Tempest\Reflection\ClassReflector;
1516
use Tempest\Reflection\MethodReflector;
16-
use Tempest\Validation\Exceptions\ValueWasInvalid;
17-
use Tempest\Validation\Rule;
17+
use Tempest\Support\Arr;
1818
use Tempest\Validation\Validator;
1919

2020
use function Tempest\Support\arr;
@@ -27,6 +27,10 @@ final class InteractiveComponentRenderer
2727

2828
private bool $shouldRerender = true;
2929

30+
public function __construct(
31+
private readonly Validator $validator,
32+
) {}
33+
3034
public function render(Console $console, InteractiveConsoleComponent $component, array $validation = []): mixed
3135
{
3236
$clone = clone $this;
@@ -144,12 +148,12 @@ private function applyKey(InteractiveConsoleComponent $component, Console $conso
144148
// If something's returned, we'll need to validate the result
145149
$this->validationErrors = [];
146150

147-
$failingRule = $this->validate($return, $validation);
151+
$validationError = $this->validate($return, $validation);
148152

149153
// If invalid, we'll remember the validation message and continue
150-
if ($failingRule !== null) {
154+
if ($validationError !== null) {
151155
$component->setState(ComponentState::ERROR);
152-
$this->validationErrors[] = $failingRule->message();
156+
$this->validationErrors[] = $validationError;
153157
Fiber::suspend();
154158

155159
continue;
@@ -224,9 +228,17 @@ private function resolveHandlers(InteractiveConsoleComponent $component): array
224228
/**
225229
* @param \Tempest\Validation\Rule[] $validation
226230
*/
227-
private function validate(mixed $value, array $validation): ?Rule
231+
private function validate(mixed $value, array $validation): ?string
228232
{
229-
return new Validator()->validateValue($value, $validation)[0] ?? null;
233+
$failingRules = $this->validator->validateValue($value, $validation);
234+
235+
if ($failingRules === []) {
236+
return null;
237+
}
238+
239+
return $this->validator->getErrorMessage(
240+
rule: Arr\first($failingRules),
241+
);
230242
}
231243

232244
public function isComponentSupported(Console $console, InteractiveConsoleComponent $component): bool

packages/database/src/Builder/ModelInspector.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ final class ModelInspector
3131

3232
private(set) object|string $instance;
3333

34+
private Validator $validator {
35+
get => get(Validator::class);
36+
}
37+
3438
public function __construct(
3539
private(set) object|string $model,
3640
) {
@@ -339,7 +343,6 @@ public function validate(mixed ...$data): void
339343
return;
340344
}
341345

342-
$validator = new Validator();
343346
$failingRules = [];
344347

345348
foreach ($data as $key => $value) {
@@ -349,7 +352,7 @@ public function validate(mixed ...$data): void
349352
continue;
350353
}
351354

352-
$failingRulesForProperty = $validator->validateValueForProperty(
355+
$failingRulesForProperty = $this->validator->validateValueForProperty(
353356
$property,
354357
$value,
355358
);
@@ -360,7 +363,7 @@ public function validate(mixed ...$data): void
360363
}
361364

362365
if ($failingRules !== []) {
363-
throw new ValidationFailed($this->reflector->getName(), $failingRules);
366+
throw new ValidationFailed($failingRules, $this->reflector->getName());
364367
}
365368
}
366369

packages/http/src/Mappers/RequestToObjectMapper.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919

2020
final readonly class RequestToObjectMapper implements Mapper
2121
{
22+
public function __construct(
23+
private Validator $validator,
24+
) {}
25+
2226
public function canMap(mixed $from, mixed $to): bool
2327
{
2428
return $from instanceof Request;
@@ -53,10 +57,10 @@ public function map(mixed $from, mixed $to): array|object
5357
];
5458
}
5559

56-
$failingRules = new Validator()->validateValuesForClass($to, $data);
60+
$failingRules = $this->validator->validateValuesForClass($to, $data);
5761

5862
if ($failingRules !== []) {
59-
throw new ValidationFailed($from, $failingRules);
63+
throw $this->validator->createValidationFailureException($failingRules, $from);
6064
}
6165

6266
return map($data)->with(ArrayToObjectMapper::class)->to($to);

packages/http/src/Responses/Invalid.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,22 @@
99
use Tempest\Http\Response;
1010
use Tempest\Http\Session\Session;
1111
use Tempest\Http\Status;
12+
use Tempest\Intl\Translator;
1213
use Tempest\Support\Json;
1314
use Tempest\Validation\Rule;
15+
use Tempest\Validation\Validator;
1416

17+
use function Tempest\get;
1518
use function Tempest\Support\arr;
1619

1720
final class Invalid implements Response
1821
{
1922
use IsResponse;
2023

24+
public Validator $validator {
25+
get => get(Validator::class);
26+
}
27+
2128
public function __construct(
2229
Request $request,
2330
/** @var \Tempest\Validation\Rule[][] $failingRules */
@@ -36,7 +43,7 @@ public function __construct(
3643
'x-validation',
3744
Json\encode(
3845
arr($failingRules)->map(fn (array $failingRulesForField) => arr($failingRulesForField)->map(
39-
fn (Rule $rule) => $rule->message(),
46+
fn (Rule $rule) => $this->validator->getErrorMessage($rule),
4047
)->toArray())->toArray(),
4148
),
4249
);

packages/intl/src/GenericTranslator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public function translateForLocale(Locale $locale, string $key, mixed ...$argume
3333
}
3434

3535
try {
36-
return $this->formatter->format($message, ...$arguments);
36+
return $this->formatter->format(mb_trim($message), ...$arguments);
3737
} catch (\Throwable $exception) {
3838
$this->eventBus?->dispatch(new TranslationFailure(
3939
locale: $locale,

packages/intl/src/MessageFormat/Formatter/LocalVariable.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
namespace Tempest\Intl\MessageFormat\Formatter;
44

5+
use Tempest\Intl\MessageFormat\FormattingFunction;
56
use Tempest\Intl\MessageFormat\SelectorFunction;
67

78
final readonly class LocalVariable
89
{
910
public function __construct(
1011
public string $identifier,
1112
public mixed $value,
12-
public ?SelectorFunction $function = null,
13+
public ?SelectorFunction $selector = null,
14+
public ?FormattingFunction $formatter = null,
1315
public array $parameters = [],
1416
) {}
1517
}

packages/intl/src/MessageFormat/Formatter/MessageFormatter.php

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ private function formatMessage(MessageNode $message): string
8888
$this->variables[$variableName] = new LocalVariable(
8989
identifier: $variableName,
9090
value: $parameters['default'],
91-
function: $this->getSelectorFunction((string) $expression->function?->identifier),
91+
selector: $this->getSelectorFunction((string) $expression->function?->identifier),
92+
formatter: $this->getFormattingFunction((string) $expression->function?->identifier),
9293
parameters: $parameters,
9394
);
9495
} else {
@@ -100,7 +101,8 @@ function: $this->getSelectorFunction((string) $expression->function?->identifier
100101
$this->variables[$variableName] = new LocalVariable(
101102
identifier: $variableName,
102103
value: $this->variables[$variableName]->value,
103-
function: $this->getSelectorFunction((string) $expression->function->identifier),
104+
selector: $this->getSelectorFunction((string) $expression->function->identifier),
105+
formatter: $this->getFormattingFunction((string) $expression->function?->identifier),
104106
parameters: $this->evaluateOptions($expression->function->options),
105107
);
106108
}
@@ -110,7 +112,8 @@ function: $this->getSelectorFunction((string) $expression->function->identifier)
110112
$localVariables[$variableName] = new LocalVariable(
111113
identifier: $variableName,
112114
value: $this->evaluateExpression($declaration->expression)->value,
113-
function: $this->getSelectorFunction($declaration->expression->function?->identifier),
115+
selector: $this->getSelectorFunction($declaration->expression->function?->identifier),
116+
formatter: $this->getFormattingFunction($declaration->expression->function?->identifier),
114117
parameters: $declaration->expression->attributes,
115118
);
116119
}
@@ -188,8 +191,8 @@ private function formatMatcher(Matcher $matcher): string
188191
$variantKey = $keyNode->value;
189192
$isMatch = false;
190193

191-
if ($variable->function) {
192-
$isMatch = $variable->function->match($variantKey, $variable->value, $variable->parameters);
194+
if ($variable->selector) {
195+
$isMatch = $variable->selector->match($variantKey, $variable->value, $variable->parameters);
193196
} else {
194197
$isMatch = $variable->value === $variantKey;
195198
}
@@ -259,6 +262,8 @@ private function formatPlaceholder(Placeholder $placeholder): string
259262
private function evaluateExpression(Expression $expression): FormattedValue
260263
{
261264
$value = null;
265+
$formattingFunction = null;
266+
$parameters = [];
262267

263268
if ($expression instanceof LiteralExpression) {
264269
$value = $expression->literal->value;
@@ -270,26 +275,31 @@ private function evaluateExpression(Expression $expression): FormattedValue
270275
}
271276

272277
$value = $this->variables[$variableName]->value;
278+
$formattingFunction = $this->variables[$variableName]->formatter;
279+
$parameters = $this->variables[$variableName]->parameters;
273280
} elseif ($expression instanceof FunctionExpression) {
274281
$value = null; // Function-only expressions start with null
275282
}
276283

277284
if ($expression->function !== null) {
278285
$functionName = (string) $expression->function->identifier;
279-
$options = $this->evaluateOptions($expression->function->options);
286+
$parameters = $this->evaluateOptions($expression->function->options);
280287

281-
if ($function = $this->getFormattingFunction($functionName)) {
282-
return $function->format($value, $options);
283-
} else {
288+
if (is_null($formattingFunction = $this->getFormattingFunction($functionName))) {
284289
throw new FormattingException("Unknown function `{$functionName}`.");
285290
}
286291
}
287292

288-
$formatted = $value !== null ? ((string) $value) : '';
293+
if ($formattingFunction) {
294+
return $formattingFunction->format($value, $parameters);
295+
}
289296

290-
return new FormattedValue($value, $formatted);
297+
return new FormattedValue($value, $value !== null ? ((string) $value) : '');
291298
}
292299

300+
/**
301+
* @param array<Option> $options
302+
*/
293303
private function evaluateOptions(array $options): array
294304
{
295305
$result = [];

packages/intl/src/MessageFormat/Functions/BooleanFunction.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,27 @@
22

33
namespace Tempest\Intl\MessageFormat\Functions;
44

5+
use Tempest\Intl\MessageFormat\Formatter\FormattedValue;
6+
use Tempest\Intl\MessageFormat\FormattingFunction;
57
use Tempest\Intl\MessageFormat\SelectorFunction;
68

7-
final class BooleanFunction implements SelectorFunction
9+
final class BooleanFunction implements SelectorFunction, FormattingFunction
810
{
911
public string $name = 'boolean';
1012

1113
public function match(string $key, mixed $value, array $parameters): bool
1214
{
13-
return match ((bool) $value) {
14-
true => in_array($key, ['true', true], strict: true),
15-
false => in_array($key, ['false', false], strict: true),
15+
return match ($value) {
16+
[], null, false, 'false' => in_array($key, ['false', false, null, 'no', 0, '0'], strict: true),
17+
default => in_array($key, ['true', true, 'yes', 1, '1'], strict: true),
1618
};
1719
}
20+
21+
public function format(mixed $value, array $parameters): FormattedValue
22+
{
23+
return new FormattedValue($value, match ($value) {
24+
[], null, false, 'false' => 'false',
25+
default => 'true',
26+
});
27+
}
1828
}

packages/intl/src/MessageFormat/Functions/DateTimeFunction.php

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
namespace Tempest\Intl\MessageFormat\Functions;
44

5+
use Tempest\DateTime\DateStyle;
56
use Tempest\DateTime\DateTime;
7+
use Tempest\DateTime\TimeStyle;
68
use Tempest\Intl\MessageFormat\Formatter\FormattedValue;
79
use Tempest\Intl\MessageFormat\FormattingFunction;
810
use Tempest\Support\Arr;
@@ -18,8 +20,32 @@ public function format(mixed $value, array $parameters): FormattedValue
1820
}
1921

2022
$datetime = DateTime::parse($value);
21-
$formatted = $datetime->format(Arr\get_by_key($parameters, 'pattern'));
2223

23-
return new FormattedValue($value, $formatted);
24+
if ($pattern = Arr\get_by_key($parameters, 'pattern')) {
25+
return new FormattedValue($value, $datetime->format($pattern));
26+
}
27+
28+
return new FormattedValue(
29+
value: $value,
30+
formatted: $datetime->toString(
31+
dateStyle: match (Arr\get_by_key($parameters, 'date_style')) {
32+
'full' => DateStyle::FULL,
33+
'long' => DateStyle::LONG,
34+
'medium' => DateStyle::MEDIUM,
35+
'short' => DateStyle::SHORT,
36+
'none' => DateStyle::NONE,
37+
'relative' => DateStyle::RELATIVE_MEDIUM,
38+
default => DateStyle::LONG,
39+
},
40+
timeStyle: match (Arr\get_by_key($parameters, 'time_style')) {
41+
'full' => TimeStyle::FULL,
42+
'long' => TimeStyle::LONG,
43+
'medium' => TimeStyle::MEDIUM,
44+
'short' => TimeStyle::SHORT,
45+
'none' => TimeStyle::NONE,
46+
default => TimeStyle::SHORT,
47+
},
48+
),
49+
);
2450
}
2551
}

packages/intl/src/MessageFormat/Functions/NumberFunction.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
use Tempest\Intl\Number;
1212
use Tempest\Intl\PluralRules\PluralRulesMatcher;
1313
use Tempest\Support\Arr;
14-
use Tempest\Support\Str;
1514

1615
final class NumberFunction implements FormattingFunction, SelectorFunction
1716
{
@@ -26,8 +25,12 @@ public function match(string $key, mixed $value, array $parameters): bool
2625
{
2726
$number = Number\parse($value);
2827

28+
if (Arr\get_by_key($parameters, 'select') === 'exists') {
29+
return $this->matchExists($key, $value);
30+
}
31+
2932
if (Arr\get_by_key($parameters, 'select') === 'exact') {
30-
return $key === Str\parse($value);
33+
return Number\parse($key) === $value;
3134
}
3235

3336
if (Number\parse($key) === $number || $key === $value) {
@@ -41,6 +44,19 @@ public function match(string $key, mixed $value, array $parameters): bool
4144
return false;
4245
}
4346

47+
private function matchExists(string $key, mixed $value): bool
48+
{
49+
if ($key === 'true') {
50+
return $value !== null;
51+
}
52+
53+
if ($key === 'false' || $key === 'null') {
54+
return $value === null;
55+
}
56+
57+
return false;
58+
}
59+
4460
public function format(mixed $value, array $parameters): FormattedValue
4561
{
4662
$number = Number\parse($value);

0 commit comments

Comments
 (0)