Skip to content

Commit

Permalink
feat: resource translation
Browse files Browse the repository at this point in the history
  • Loading branch information
alanpoulain committed Jul 2, 2021
1 parent 6336f64 commit 9c3ef8f
Show file tree
Hide file tree
Showing 30 changed files with 1,141 additions and 20 deletions.
12 changes: 12 additions & 0 deletions features/jsonld/context.feature
Expand Up @@ -86,3 +86,15 @@ Feature: JSON-LD contexts generation
}
}
"""

Scenario: The JSON-LD context of a translatable resource contains the language
When I send a "GET" request to "/dummy_translatables"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "@context.@language" should be equal to "en"
When I send a "GET" request to "/dummy_translatables/fr"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "@context.@language" should be equal to "fr"
68 changes: 68 additions & 0 deletions features/translation/default_translation.feature
@@ -0,0 +1,68 @@
Feature: Use default translation for a resource if available
In order to translate API resources
As a client software developer
The API should return translated fields for a given locale

@createSchema
Scenario: A resource is translated for a given locale
Given there is a translatable dummy with its translations
When I send a "GET" request to "/dummy_translatables"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "hydra:member[0].name" should be equal to "Dummy translated in English"
And the JSON node "hydra:member[0].description" should be equal to "It's a dummy!"
When I send a "GET" request to "/dummy_translatables/fr"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "hydra:member[0].name" should be equal to "Dummy traduit en français"
And the JSON node "hydra:member[0].description" should be equal to "C'est un dummy !"
And the JSON node "hydra:member[0].notTranslatedField" should be equal to "not translated"

Scenario: A translation can be updated for a resource
When I add "Content-Type" header equal to "application/ld+json"
And I send a "PUT" request to "/dummy_translatables/1/fr" with body:
"""
{
"name": "Dummy mieux traduit en français",
"notTranslatedField": "really not translated"
}
"""
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "name" should be equal to "Dummy mieux traduit en français"
And the JSON node "description" should be equal to "C'est un dummy !"
And the JSON node "notTranslatedField" should be equal to "really not translated"
When I send a "GET" request to "/dummy_translatables/fr"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "hydra:member[0].name" should be equal to "Dummy mieux traduit en français"
And the JSON node "hydra:member[0].description" should be equal to "C'est un dummy !"
And the JSON node "hydra:member[0].notTranslatedField" should be equal to "really not translated"

Scenario: A translation can be added to a resource
When I add "Content-Type" header equal to "application/ld+json"
And I send a "PUT" request to "/dummy_translatables/1/es" with body:
"""
{
"name": "Dummy traducido al español",
"description": "¡Es un dummy!",
"notTranslatedField": "truly not translated"
}
"""
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "name" should be equal to "Dummy traducido al español"
And the JSON node "description" should be equal to "¡Es un dummy!"
And the JSON node "notTranslatedField" should be equal to "truly not translated"
When I send a "GET" request to "/dummy_translatables/es"
Then the response status code should be 200
And the response should be in JSON
And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
And the JSON node "hydra:member[0].name" should be equal to "Dummy traducido al español"
And the JSON node "hydra:member[0].description" should be equal to "¡Es un dummy!"
And the JSON node "hydra:member[0].notTranslatedField" should be equal to "truly not translated"
7 changes: 5 additions & 2 deletions src/Annotation/ApiResource.php
Expand Up @@ -71,7 +71,8 @@
* @Attribute("urlGenerationStrategy", type="int"),
* @Attribute("validationGroups", type="mixed"),
* @Attribute("exceptionToStatus", type="array"),
* @Attribute("queryParameterValidationEnabled", type="bool")
* @Attribute("queryParameterValidationEnabled", type="bool"),
* @Attribute("translation", type="array")
* )
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
Expand Down Expand Up @@ -174,6 +175,7 @@ final class ApiResource
* @param int $urlGenerationStrategy
* @param array $exceptionToStatus https://api-platform.com/docs/core/errors/#fine-grained-configuration
* @param bool $queryParameterValidationEnabled
* @param array $translation
*
* @throws InvalidArgumentException
*/
Expand Down Expand Up @@ -225,7 +227,8 @@ public function __construct(
?int $urlGenerationStrategy = null,
?bool $compositeIdentifier = null,
?array $exceptionToStatus = null,
?bool $queryParameterValidationEnabled = null
?bool $queryParameterValidationEnabled = null,
?array $translation = null
) {
if (!\is_array($description)) { // @phpstan-ignore-line Doctrine annotations support
[$publicProperties, $configurableAttributes] = self::getConfigMetadata();
Expand Down
Expand Up @@ -120,6 +120,7 @@ public function load(array $configs, ContainerBuilder $container): void
$this->registerDoctrineOrmConfiguration($container, $config, $loader);
$this->registerDoctrineMongoDbOdmConfiguration($container, $config, $loader);
$this->registerHttpCacheConfiguration($container, $config, $loader);
$this->registerTranslationConfiguration($loader);
$this->registerValidatorConfiguration($container, $config, $loader);
$this->registerDataCollectorConfiguration($container, $config, $loader);
$this->registerMercureConfiguration($container, $config, $loader);
Expand Down Expand Up @@ -616,6 +617,11 @@ private function getFormats(array $configFormats): array
return $formats;
}

private function registerTranslationConfiguration(XmlFileLoader $loader): void
{
$loader->load('translation.xml');
}

private function registerValidatorConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void
{
if (interface_exists(ValidatorInterface::class)) {
Expand Down
1 change: 1 addition & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/api.xml
Expand Up @@ -117,6 +117,7 @@
<argument type="tagged" tag="api_platform.data_transformer" on-invalid="ignore" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" on-invalid="ignore" />
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="ignore" />
<argument type="service" id="api_platform.translation.resource_translator" on-invalid="ignore" />

<!-- Run before serializer.normalizer.json_serializable -->
<tag name="serializer.normalizer" priority="-895" />
Expand Down
1 change: 1 addition & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
Expand Up @@ -222,6 +222,7 @@
<argument type="tagged" tag="api_platform.data_transformer" on-invalid="ignore" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" on-invalid="ignore" />
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="ignore" />
<argument type="service" id="api_platform.translation.resource_translator" on-invalid="ignore" />

<!-- Run before serializer.normalizer.json_serializable -->
<tag name="serializer.normalizer" priority="-890" />
Expand Down
1 change: 1 addition & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/hal.xml
Expand Up @@ -42,6 +42,7 @@
<argument type="tagged" tag="api_platform.data_transformer" on-invalid="ignore" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" on-invalid="ignore" />
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="ignore" />
<argument type="service" id="api_platform.translation.resource_translator" on-invalid="ignore" />

<!-- Run before serializer.normalizer.json_serializable -->
<tag name="serializer.normalizer" priority="-890" />
Expand Down
1 change: 1 addition & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml
Expand Up @@ -43,6 +43,7 @@
<argument type="collection" />
<argument type="tagged" tag="api_platform.data_transformer" on-invalid="ignore" />
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="ignore" />
<argument type="service" id="api_platform.translation.resource_translator" on-invalid="ignore" />

<!-- Run before serializer.normalizer.json_serializable -->
<tag name="serializer.normalizer" priority="-890" />
Expand Down
3 changes: 3 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml
Expand Up @@ -11,6 +11,8 @@
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
<argument type="service" id="api_platform.router" />
<argument>null</argument>
<argument type="service" id="api_platform.translation.resource_translator" />
</service>

<!-- Serializer -->
Expand All @@ -28,6 +30,7 @@
<argument type="collection" />
<argument type="tagged" tag="api_platform.data_transformer" on-invalid="ignore" />
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="ignore" />
<argument type="service" id="api_platform.translation.resource_translator" on-invalid="ignore" />

<!-- Run before serializer.normalizer.json_serializable -->
<tag name="serializer.normalizer" priority="-890" />
Expand Down
16 changes: 16 additions & 0 deletions src/Bridge/Symfony/Bundle/Resources/config/translation.xml
@@ -0,0 +1,16 @@
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="api_platform.translation.resource_translator" class="ApiPlatform\Core\Translation\ResourceTranslator" public="false">
<argument type="service" id="request_stack" />
<argument type="service" id="api_platform.property_accessor" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
</service>
<service id="ApiPlatform\Core\Translation\ResourceTranslatorInterface" alias="api_platform.translation.resource_translator" />
</services>

</container>
5 changes: 3 additions & 2 deletions src/GraphQl/Serializer/ItemNormalizer.php
Expand Up @@ -23,6 +23,7 @@
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
use ApiPlatform\Core\Serializer\ItemNormalizer as BaseItemNormalizer;
use ApiPlatform\Core\Translation\ResourceTranslatorInterface;
use ApiPlatform\Core\Util\ClassInfoTrait;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
Expand All @@ -46,9 +47,9 @@ final class ItemNormalizer extends BaseItemNormalizer

private $identifiersExtractor;

public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, IdentifiersExtractorInterface $identifiersExtractor, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, LoggerInterface $logger = null, iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null, ResourceAccessCheckerInterface $resourceAccessChecker = null)
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, IdentifiersExtractorInterface $identifiersExtractor, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, LoggerInterface $logger = null, iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null, ResourceAccessCheckerInterface $resourceAccessChecker = null, ResourceTranslatorInterface $resourceTranslator = null)
{
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, $allowPlainIdentifiers, $logger ?: new NullLogger(), $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker);
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, $allowPlainIdentifiers, $logger ?: new NullLogger(), $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker, $resourceTranslator);

$this->identifiersExtractor = $identifiersExtractor;
}
Expand Down
5 changes: 3 additions & 2 deletions src/JsonApi/Serializer/ItemNormalizer.php
Expand Up @@ -24,6 +24,7 @@
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
use ApiPlatform\Core\Serializer\CacheKeyTrait;
use ApiPlatform\Core\Serializer\ContextTrait;
use ApiPlatform\Core\Translation\ResourceTranslatorInterface;
use ApiPlatform\Core\Util\ClassInfoTrait;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\Type;
Expand Down Expand Up @@ -51,9 +52,9 @@ final class ItemNormalizer extends AbstractItemNormalizer

private $componentsCache = [];

public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor, ?NameConverterInterface $nameConverter, ResourceMetadataFactoryInterface $resourceMetadataFactory, array $defaultContext = [], iterable $dataTransformers = [], ResourceAccessCheckerInterface $resourceAccessChecker = null)
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor, ?NameConverterInterface $nameConverter, ResourceMetadataFactoryInterface $resourceMetadataFactory, array $defaultContext = [], iterable $dataTransformers = [], ResourceAccessCheckerInterface $resourceAccessChecker = null, ResourceTranslatorInterface $resourceTranslator = null)
{
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker);
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker, $resourceTranslator);
}

/**
Expand Down
8 changes: 7 additions & 1 deletion src/JsonLd/ContextBuilder.php
Expand Up @@ -18,6 +18,7 @@
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use ApiPlatform\Core\Translation\ResourceTranslatorInterface;
use ApiPlatform\Core\Util\ClassInfoTrait;
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;

Expand All @@ -42,15 +43,17 @@ final class ContextBuilder implements AnonymousContextBuilderInterface
* @var NameConverterInterface|null
*/
private $nameConverter;
private $resourceTranslator;

public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, UrlGeneratorInterface $urlGenerator, NameConverterInterface $nameConverter = null)
public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, UrlGeneratorInterface $urlGenerator, NameConverterInterface $nameConverter = null, ResourceTranslatorInterface $resourceTranslator = null)
{
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
$this->resourceMetadataFactory = $resourceMetadataFactory;
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
$this->propertyMetadataFactory = $propertyMetadataFactory;
$this->urlGenerator = $urlGenerator;
$this->nameConverter = $nameConverter;
$this->resourceTranslator = $resourceTranslator;
}

/**
Expand Down Expand Up @@ -151,6 +154,9 @@ public function getAnonymousResourceContext($object, array $context = [], int $r
private function getResourceContextWithShortname(string $resourceClass, int $referenceType, string $shortName): array
{
$context = $this->getBaseContext($referenceType);
if ($this->resourceTranslator && $this->resourceTranslator->isResourceClassTranslatable($resourceClass) && $locale = $this->resourceTranslator->getLocale()) {
$context['@language'] = $locale;
}

foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
Expand Down

0 comments on commit 9c3ef8f

Please sign in to comment.