Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 107 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Implementation:
- Add virtual fields for all translatable fields; their getters and setters delegate to the translation object

Example:
``` php
```php
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
Expand Down Expand Up @@ -149,7 +149,7 @@ class Article extends AbstractTranslatable
- Add the `translations` serialization group to all fields, plus your usual read/write groups

Example:
``` php
```php
use Doctrine\ORM\Mapping as ORM;
use Locastic\ApiPlatformTranslationBundle\Model\AbstractTranslation;
use Locastic\ApiPlatformTranslationBundle\Model\TranslatableInterface;
Expand Down Expand Up @@ -202,7 +202,7 @@ class ArticleTranslation extends AbstractTranslation

For `PUT` you **must** disable API Platform's `standard_put`, either per operation (`extraProperties: ['standard_put' => false]`, as above) or once for the whole API:

``` yaml
```yaml
# config/packages/api_platform.yaml
api_platform:
defaults:
Expand All @@ -215,43 +215,114 @@ With `standard_put` on, API Platform deserializes into a brand-new object and co
Usage:
------

**Language param for displaying a single translation:**
### Request a single locale

Pass the locale as a query parameter:

`?locale=de`
```
GET /articles/1?locale=de
```

**Or use the Accept-Language http header**
Or use the `Accept-Language` HTTP header:

`Accept-Language: de`
```
GET /articles/1
Accept-Language: de
```

Translatable fields are returned in the requested locale; when no translation
exists for it, the fallback locale is used.

**Restricting locales:** if [`framework.enabled_locales`](https://symfony.com/doc/current/reference/configuration/framework.html#enabled-locales) or the bundle's own `enabled_locales` option (which takes precedence) is configured, only those locales are accepted: a `?locale=` value outside the list and non-matching `Accept-Language` headers fall back to the default locale. When neither is configured (Symfony's default), any requested locale is accepted.

**Serialization group for displaying all translations:**
### Return all translations in a response

Add the `translations` serialization group through the `translation.groups`
filter (registered by this bundle, enabled on the resource via
`filters: ['translation.groups']`):

```
GET /articles/1?groups[]=translations
```

`?groups[]=translations`
The group is added on top of the operation's normalization groups, so the
response contains the single-locale virtual fields plus the full collection:

**POST translations example**
``` json
```json
{
"datetime":"2017-10-10",
"@id": "/articles/1",
"title": "test",
"translations": {
"en":{
"title":"test",
"content":"test",
"locale":"en"
"en": {
"id": 2,
"title": "test",
"content": "test",
"locale": "en"
},
"de":{
"title":"test de",
"content":"test de",
"locale":"de"
"de": {
"id": 3,
"title": "test de",
"content": "test de",
"locale": "de"
}
}
}
```

**EDIT translations example**
### Create a resource with translations (POST)

Submit `translations` as an object keyed by locale; each entry must repeat its
`locale` field:

Send the `id` of each existing translation so it is updated instead of replaced:
``` json
```json
{
"datetime": "2017-10-10",
"translations": {
"en": {
"title": "test",
"content": "test",
"locale": "en"
},
"de": {
"title": "test de",
"content": "test de",
"locale": "de"
}
}
}
```

### Update translations (PATCH, recommended)

A merge patch updates only the submitted locales and leaves the others
untouched; existing translation rows are updated in place, no `id` needed:

```
PATCH /articles/1
Content-Type: application/merge-patch+json
```

```json
{
"translations": {
"de": {
"title": "test edit de",
"locale": "de"
}
}
}
```

Here the `de` title is updated while the `en` translation is left as is.

### Replace all translations (PUT)

`PUT` is a full replace: locales absent from the payload are removed. It
requires `standard_put` to be disabled (see the editing notes under
[Implementation](#implementation) above). Send the `id` of each
existing translation so it is updated instead of replaced:

```json
{
"datetime": "2017-10-10T00:00:00+02:00",
"translations": {
Expand All @@ -271,6 +342,19 @@ Send the `id` of each existing translation so it is updated instead of replaced:
}
```

Limitations:
------------

- **Filtering and ordering by translated fields is not supported.** The
translated values live on the translation entity and are exposed through
virtual getters, so built-in API Platform filters (`SearchFilter`,
`OrderFilter`, ...) cannot target them on the resource. Filtering on
translation fields requires a custom filter joining the translation entity.
- **Collections issue one translation query per item.** The `translations`
association is `EXTRA_LAZY` and resolved per entity, so listing N resources
triggers N additional queries for their translations; there is no built-in
eager loading yet.

## Contribution

If you have an idea on how to improve this bundle, feel free to contribute. If you have problems or you found some bugs, please open an issue.
Expand Down
36 changes: 36 additions & 0 deletions context7.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"$schema": "https://context7.com/schema/context7.json",
"url": "https://context7.com/locastic/apiplatformtranslationbundle",
"public_key": "pk_6nLhWveJT5eYd7x7e5w1o",
"projectTitle": "Locastic ApiPlatformTranslationBundle",
"description": "API-first entity translations for API Platform (Symfony): per-locale translation entities exposed as embedded API objects, with the active locale resolved from each request.",
"folders": [],
"excludeFolders": [
"src",
"tests",
"config",
"bin",
"vendor"
],
"excludeFiles": [
"CONTRIBUTING.md",
"SECURITY.md"
],
"rules": [
"A translatable resource extends AbstractTranslatable, implements createTranslation(), and maps translations as OneToMany with indexBy: 'locale', cascade: ['persist'], orphanRemoval: true.",
"The translation entity extends AbstractTranslation and holds all translatable fields plus a writable locale field.",
"Virtual getters and setters on the translatable delegate to getTranslation(), which resolves the current locale and falls back to the fallback locale.",
"Put the 'translations' serialization group on the translations collection, on every field of the translation entity, and in the normalizationContext of POST, PUT, and PATCH operations so writes return all translation objects.",
"In write payloads, translations is an object keyed by locale, and each entry must repeat its 'locale' field: {\"translations\": {\"de\": {\"title\": \"...\", \"locale\": \"de\"}}}.",
"Prefer PATCH (application/merge-patch+json) for editing translations: it updates only the submitted locales. PUT is a full replace and removes locales absent from the payload.",
"PUT requires API Platform's standard_put to be disabled, either per operation via extraProperties: ['standard_put' => false] or globally under api_platform.defaults.extra_properties.",
"When editing via PUT, include each existing translation's 'id' so its row is updated in place instead of replaced.",
"Clients select the locale with the ?locale= query parameter or the Accept-Language header, and can request all translations at once with ?groups[]=translations (the bundle's 'translation.groups' filter).",
"Restrict accepted locales with framework.enabled_locales or the bundle's api_platform_translation.enabled_locales option (which takes precedence); unlisted locales fall back to the default locale.",
"Version 2.x requires PHP >= 8.2, API Platform ^3.4 || ^4.0, and Doctrine ORM ^3.0; use PHP 8 attributes, not Doctrine annotations, in mappings."
],
"previousVersions": [
"v2.0.1",
"v1.4.1"
]
}
Loading