Skip to content

Commit fb2c76a

Browse files
committed
Add localization system
1 parent 707be12 commit fb2c76a

31 files changed

+374
-106
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,17 @@ and this project adheres to
1313
- Replace `Resource::getId(object $model, Context $context): string` method with
1414
`Resource::id(): Id` method, which returns an `Id` field instance to define ID
1515
schema, validation, and writability
16+
- Custom deserializers on `ToOne` relationships now receive the resolved model
17+
rather than the raw relationship object, for consistency with `ToMany`
1618

1719
### Added
1820

21+
- Add localization system for customizable error messages
22+
- Add `JsonApi::messages()` method for merging custom error messages with
23+
defaults
24+
- Add `JsonApi::setTranslator()` method for providing custom translator
25+
implementations
26+
- Add `Context::translate()` method for accessing the translation system
1927
- Add `Id` field class for customizing resource ID behavior, including type
2028
constraints, client-generated IDs via `writableOnCreate()`, and validation
2129
- Add `linkageMeta()` method to relationship fields for adding meta to resource
@@ -26,6 +34,7 @@ and this project adheres to
2634

2735
### Changed
2836

37+
- Error messages now use the localization system
2938
- Various performance optimizations to improve serialization speed
3039
- Improve OpenAPI schema generation to properly handle ID field constraints and
3140
avoid redundant properties

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ support for:
2222
- **Deleting** resources (`DELETE /articles/1`)
2323
- **Content negotiation**
2424
- **Error handling**
25+
- **Localization** for customizing error messages
2526
- **Extensions** including Atomic Operations
2627
- **Generating OpenAPI definitions**
2728

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export default defineConfig({
5151
text: 'Heterogeneous Collections',
5252
link: '/collections',
5353
},
54+
{ text: 'Localization', link: '/localization' },
5455
{ text: 'OpenAPI Definitions', link: '/openapi' },
5556
],
5657
},

docs/context.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ class Context
5656
// Get the value of a query param
5757
public function queryParam(string $name, $default = null): mixed;
5858

59+
// Translate a message using the API's translator
60+
public function translate(string $key, array $replacements = []): string;
61+
5962
// Get the parsed JSON:API payload
6063
public function body(): ?array;
6164

docs/errors.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,9 @@ class ImATeapotException implements ErrorProvider
4949

5050
Exceptions that do not implement this interface will result in a generic
5151
`500 Internal Server Error` response.
52+
53+
## Customizing Error Messages
54+
55+
Many of the built-in error messages can be customized using the
56+
[localization system](localization.md). This allows you to provide localized or
57+
custom error messages throughout your API.

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ support for:
2121
- **Relationship** URLs (`/articles/1/relationships/author`)
2222
- **Content negotiation**
2323
- **Error handling**
24+
- **Localization** for customizing error messages
2425
- **Extensions** including Atomic Operations
2526
- **Generating OpenAPI definitions**
2627

docs/localization.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Localization
2+
3+
json-api-server includes a basic localization system that allows you to
4+
customize error messages in your API responses.
5+
6+
## Default Language
7+
8+
By default, the server uses English for all error messages. These are defined in
9+
the `Tobyz\JsonApi\Translation\EnglishCatalogue` class.
10+
11+
## Customizing Messages
12+
13+
You can customize messages in two ways:
14+
15+
### Merging Custom Messages
16+
17+
To override specific messages while keeping the defaults:
18+
19+
```php
20+
$api = new JsonApi();
21+
22+
$api->messages([
23+
'resource.not_found' => 'The requested resource could not be found',
24+
'pagination.size_exceeded' => 'The requested page size is too large',
25+
]);
26+
```
27+
28+
This will merge your custom messages with the default English messages.
29+
30+
### Using a Custom Translator
31+
32+
For complete control over localization, you can provide your own translator
33+
implementation:
34+
35+
```php
36+
use Tobyz\JsonApiServer\Translation\TranslatorInterface;
37+
38+
class CustomTranslator implements TranslatorInterface
39+
{
40+
public function translate(string $key, array $replacements = []): string
41+
{
42+
// Your custom translation logic here
43+
return $translation;
44+
}
45+
}
46+
47+
$api = new JsonApi();
48+
$api->setTranslator(new CustomTranslator());
49+
```
50+
51+
## Message Keys
52+
53+
Messages support placeholder replacements using the `:placeholder` syntax. The
54+
replacement values are passed as an array:
55+
56+
```php
57+
$context->translate('resource.not_found', ['identifier' => 'users']);
58+
// Results in: "Resource not found: users"
59+
```
60+
61+
## Using Localization in Custom Code
62+
63+
You can access the localization system in your custom code through the Context
64+
object:
65+
66+
```php
67+
use Tobyz\JsonApiServer\Context;
68+
69+
$message = $context->translate('custom.key', ['name' => 'value']);
70+
```

src/Context.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public function __construct(public JsonApi $api, public ServerRequestInterface $
4949
$this->resourceMeta = new WeakMap();
5050
}
5151

52+
public function translate(string $key, array $replacements = []): string
53+
{
54+
return $this->api->translator->translate($key, $replacements);
55+
}
56+
5257
/**
5358
* Get the value of a query param.
5459
*/
@@ -150,7 +155,7 @@ public function sparseFields(Resource $resource): array
150155

151156
if (!is_string($requested)) {
152157
throw (new BadRequestException(
153-
'Sparse fieldsets must be comma-separated strings.',
158+
$this->translate('request.fields_invalid'),
154159
))->setSource(['parameter' => "fields[$type]"]);
155160
}
156161

src/Endpoint/Concerns/FindsResources.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ private function findResource(Context $context, string $id)
2525
}
2626

2727
if (!($model = $collection->find($id, $context))) {
28-
throw new ResourceNotFoundException($collection->name(), $id);
28+
throw new ResourceNotFoundException(
29+
$collection->name(),
30+
$id,
31+
$context->translate('resource.not_found', [
32+
'identifier' => $collection->name() . '.' . $id,
33+
]),
34+
);
2935
}
3036

3137
return $model;

src/Endpoint/Concerns/IncludesData.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ private function validateInclude(
8787
continue 2;
8888
}
8989

90-
throw (new BadRequestException("Invalid include [$path$name]"))->setSource([
91-
'parameter' => 'include',
92-
]);
90+
throw (new BadRequestException(
91+
$context->translate('request.include_invalid', ['include' => $path . $name]),
92+
))->setSource(['parameter' => 'include']);
9393
}
9494
}
9595

0 commit comments

Comments
 (0)