diff --git a/README.md b/README.md index 4ddb2ae..f89c8e7 100644 --- a/README.md +++ b/README.md @@ -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; @@ -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; @@ -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: @@ -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": { @@ -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. diff --git a/context7.json b/context7.json new file mode 100644 index 0000000..4ab7a8b --- /dev/null +++ b/context7.json @@ -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" + ] +}