Problem/Motivation

JSON:API does not fully support translatable content entities, for instance https://github.com/jsdrupal/drupal-admin-ui will need this.

Regarding GET support, JSON:API already has implicit translation support, that is /fr/jsonapi/node/article/UUID will return a JSON:API response containing the French translation of the given article. The problem with this is that it depends on and varies by each site's language negotiation configuration. This means translations may behave differently on Drupal-powered JSON:API instances A versus B. This is suboptimal because:

  • it violates the assumption that one JSON:API instance works the same as another
  • it makes it hard (potentially impossible) to write client libraries that can be reused across projects by the community
  • it makes it impossible to standardize in a JSON:API profile (coming in the upcoming 1.1 version of the JSON:API specification).
  • it results in automagic fallback behavior in edge cases that may be fine for HTML use cases but not for JSON:API use cases (see @corbacho in #69 for a concrete example)

CUD operations on translations have little to no support, at the moment (see #80-#89 and #3038308: Avoid translations DELETE data loss and unintended changes with PATCH and test all methods against entity route parameter translation upcasting).

Proposed resolution

Define a single, non-configurable, well-defined language negotiation mechanism. That defines JSON:API's responses in the edge cases of requesting a non-existing translation and a non-existing langcode. Implement full CUD translation support on top of that.

The following specification covers individual operations (see #100-#109 for the related discussion):

https://github.com/gabesullice/drupal-jsonapi-translations/blob/master/i...

This issue cannot remove the current automatic behavior (that'd be too disruptive), but users would have to update to the formal/official mechanism if they want the edge cases to be handled in a consistent, sensible manner. Instead this will introduce a new experimental module (JSON:API Translation), implementing all the new/missing logic. This would allow people to opt-in without requiring any additional configuration. The goal would be to deprecate the legacy (GET) logic and merge the module into JSON:API in D10.

Remaining tasks

How collections should behave is still being discussed (see #69 and #110).

Done

CommentFileSizeAuthor
#126 2794431-jsonapi_translation-126.patch57.12 KBplach
#126 2794431-jsonapi_translation-126.interdiff.txt2.57 KBplach
#121 2794431-jsonapi_translation-121.patch54.32 KBplach
#121 2794431-jsonapi_translation-121.interdiff.txt7.48 KBplach
#118 2794431-jsonapi_translation-118.interdiff.txt1.4 KBplach
#118 2794431-jsonapi_translation-118.patch49.24 KBplach
#110 JSON-API Translation.postman_collection.json_.zip2.83 KBplach
#110 drupal_9x.sql_.gz848.48 KBplach
#110 2794431-jsonapi_translation-110.patch49.24 KBplach
#102 jsonapi_add_and_update_translations_support-2794431-102.patch3.56 KBcgoffin
#58 jsonapi-support_translations-2794431-58.patch15.42 KBgrimreaper
#55 2794431-55.patch16.59 KBwim leers
#55 interdiff.txt1.78 KBwim leers
#52 2794431-52.patch14.85 KBwim leers
#52 interdiff.txt14.05 KBwim leers
#50 2794431-50.patch3.27 KBwim leers
#50 interdiff.txt938 byteswim leers
#49 2794431-49.patch2.39 KBwim leers
#49 interdiff.txt1.71 KBwim leers
#48 2794431-48.patch1.07 KBwim leers
#28 2794431--interdiff--24-28.txt21.58 KBe0ipso
#28 2794431--multilingual-support--28.patch21.94 KBe0ipso
#24 interdiff.txt5.59 KBdawehner
#24 2794431-24.patch8.96 KBdawehner
#23 interdiff.txt4.97 KBdawehner
#23 2794431-23.patch6.6 KBdawehner
#21 interdiff.txt1.81 KBdawehner
#21 2794431-21.patch10.3 KBdawehner
#19 2794431-19.patch8.5 KBdawehner
#19 interdiff.txt18.02 KBdawehner
#19 2794431-fail.patch4.21 KBdawehner
#11 2794431-11.patch16.04 KBdawehner
#11 2794431-fail.patch12.38 KBdawehner
#7 feature_provide_translated_content-2794431-7.patch3.27 KBheldercor
#6 feature_provide_translated_content-2794431-6.patch3.17 KBheldercor
#2 feature_provide_translated_content-2794431-2.patch2.62 KBgabesullice

Comments

gabesullice created an issue. See original summary.

gabesullice’s picture

I know this is opening a can of worms, but it's a current business need. Attached patch is by no means complete, hopefully it will help get the ball rolling though.

Edit: I should note that this actually does work as expected and without errors.

e0ipso’s picture

Added related REST core issue.

janlaureys’s picture

Not sure if this is still relevant. But I took #2 and modified two lines so that it's possible to use the Accept-Language header.

        $acceptLanguage = $context['request']->headers->get('accept-language');
        $entity = $this->entityRepository->getTranslationFromContext($entity, $acceptLanguage);

When the header isn't set acceptLanguage will be null but the $langcode parameter of getTranslationFromContext is optional so it works out.

/me is horrible at naming variables

heldercor’s picture

Actually without this I can't use jsonapi. All I have is multilingual sites.

heldercor’s picture

Re-patching #2 against HEAD.

heldercor’s picture

StatusFileSize
new3.27 KB

Just got a TypeError exception.

e0ipso’s picture

Status: Active » Needs review
Issue tags: +Needs tests

This looks good. It needs a test though.

e0ipso’s picture

Status: Needs review » Needs work
dawehner’s picture

Assigned: Unassigned » dawehner

When the header isn't set acceptLanguage will be null but the $langcode parameter of getTranslationFromContext is optional so it works out.

I believe the current patch does the right thing, as in, just follow what core provides us as a language, see #2135829: EntityResource: translations support for a discussion how to determine this language.

I'm working on some test coverage here.

dawehner’s picture

Assigned: dawehner » Unassigned
Status: Needs work » Needs review
StatusFileSize
new12.38 KB
new16.04 KB

Here is a test

Status: Needs review » Needs work

The last submitted patch, 11: 2794431-11.patch, failed testing.

dawehner’s picture

This failure really doesn't make any sense :( So yeah this is certainly needs work.

hampercm’s picture

@dawehner The tests are failing because they instantiate a JsonApiDocumentTopLevelNormalizer. Since that class's constructor has changed to additionally pass in an EntityRepositoryInterface, this parameter will need to be added in the tests as well.

dawehner’s picture

@hampercm
Good point, well, what I don't get it why the -fail patch, with just the test changes includes, doesn't fail.

e0ipso’s picture

+++ b/tests/src/Functional/JsonApiFunctionalTest.php
@@ -192,31 +197,53 @@ class JsonApiFunctionalTest extends BrowserTestBase {
+   * @dataProvider providerTestRead

We are not providing mock data in the providerTestRead. We should find a less confusing technique to test the multilingual capabilities.

Additionally, most of these tests are testing business logic that is independent from multilingual stuff. I'm not sure about the value of duplicating the execution time for this. Maybe a multilingual specific test + Kernel test for the JsonApiDocumentTopLevelNormalizer would be more appropriate.

e0ipso’s picture

Good point, well, what I don't get it why the -fail patch, with just the test changes includes, doesn't fail.

Can it be that the EntityStorageBase::loadMultiple loads the entity for the appropriate language already? We are using that method to load entities for individual items and for collections.

e0ipso’s picture

Finally (and I'm sorry to provide this feedback now, since this code has been there 5 months already):

+++ b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
@@ -160,7 +172,11 @@ class JsonApiDocumentTopLevelNormalizer extends NormalizerBase implements Denorm
+          $entity = $entity_repository->getTranslationFromContext($entity);

If we are to override the $entity object with a translation, we should do it right after we load the entity. In any case we need to make sure that we need to do this, since @dawehner's tests indicate that we do not. That is:
1. In the ParamConverter.
2. In the Controller for collections.

dawehner’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
StatusFileSize
new4.21 KB
new18.02 KB
new8.5 KB

@dawehner's tests indicate that we do not. That is:
1. In the ParamConverter.
2. In the Controller for collections.

So right, this patch enables translations on collection level. It works already on the individual level.

We are not providing mock data in the providerTestRead. We should find a less confusing technique to test the multilingual capabilities.

Test coverage is a tricky topic. I started indeed with a dedicated method, but then thought we maybe want to follow the route of #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method which basically went the full multi-dimensional test coverage approach instead of the monte-carlo based testing approach, which tries to coverage everything but just choosing educated random points in the entire testing space.

For now I think keeping things simple, aka. a dedicated test coverage with a good name, is the better idea.

Status: Needs review » Needs work

The last submitted patch, 19: 2794431-19.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new10.3 KB
new1.81 KB

... So this time for real

hampercm’s picture

+++ b/src/Controller/EntityResource.php
@@ -698,7 +709,8 @@ class EntityResource {
+      $translated_entity = $this->entityRepository->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']);
+      $output[$entity->id()] = static::getEntityAndAccess($translated_entity);

1. getEntityAndAccess() is called in multiple places. Would it make more sense to get the translated entity inside that method?

dawehner’s picture

StatusFileSize
new6.6 KB
new4.97 KB

1. getEntityAndAccess() is called in multiple places. Would it make more sense to get the translated entity inside that method?

Good point. I totally think it makes sense.

dawehner’s picture

StatusFileSize
new8.96 KB
new5.59 KB

I expanded the test coverage and figured out we have another place where we missed loading the right entity.

hampercm’s picture

Overall, the patch from #24 looks good. Some comments:

  1. +++ b/src/Controller/EntityResource.php
    @@ -715,6 +715,9 @@ class EntityResource {
    +    $entity_repository = \Drupal::service('entity.repository');
    

    Make the $entity_repository a static member so we can use DI, instead?

  2. +++ b/tests/src/Functional/JsonApiFunctionalTest.php
    @@ -414,10 +418,38 @@ class JsonApiFunctionalTest extends BrowserTestBase {
    +    $included_tags = array_filter($output['included'], function ($entry) {
    +      return $entry['type'] === 'taxonomy_term--tags';
    +    });
    

    Good idea! There are other functional tests where this could be reused, too (note to self)

  3. +++ b/tests/src/Functional/JsonApiFunctionalTest.php
    @@ -765,4 +813,21 @@ class JsonApiFunctionalTest extends BrowserTestBase {
    +  protected function setupMultilingual() {
    

    Moving the multilingual tests to a separate class might make sense. A benefit of this would be that the functional tests (which are typically the longest-running) could run in parallel

e0ipso’s picture

Assigned: Unassigned » e0ipso
Status: Needs review » Needs work

Thanks @dawehner and @hampercm. I'm gonna finish this one off!

+++ b/tests/src/Functional/JsonApiFunctionalTest.php
@@ -707,14 +739,21 @@ class JsonApiFunctionalTest extends BrowserTestBase {
+        $term->addTranslation('es', ['name' => $term->getName() . ' (es)']);

FYI, I'm going to change 'es' to 'ca'. For non-technical reasons ;-)

e0ipso’s picture

Make the $entity_repository a static member so we can use DI, instead?

I will skip this for now, since there is no unit test in that class yet. With kernel tests we can replace the service and be done. Is that ok?

e0ipso’s picture

Status: Needs work » Needs review
StatusFileSize
new21.94 KB
new21.58 KB

I run this command to compare the speed to run the tests:

sudo -u www-data php ./core/scripts/run-tests.sh --clean && time sudo -u www-data php ./core/scripts/run-tests.sh --verbose --color --keep-results --concurrency "31" --types "PHPUnit-Functional" --php /usr/bin/php --url "http://d8dev.local" --sqlite "/var/lib/drupalci/artifacts/simpleteststandard.sqlite" --dburl "mysql://d8dev:d8dev@localhost/d8dev" --directory modules/contrib/jsonapi; cp /var/lib/drupalci/artifacts/simpleteststandard.sqlite /vagrant/simpleteststandard.sqlite

Before splitting: 46.33s
After splitting: 47.91s

So I don't think there are significant differences. However, I believe that the code is a bit cleaner after splitting.

I'll be merging if tests come back green.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Moving the multilingual tests to a separate class might make sense. A benefit of this would be that the functional tests (which are typically the longest-running) could run in parallel

Sure why not

e0ipso’s picture

Status: Reviewed & tested by the community » Fixed

  • e0ipso committed 905e5e0 on 8.x-1.x authored by dawehner
    feat(Mutilingual) Provide GET support for translated entities (#2794431...
e0ipso’s picture

Commit is was not showing up. This was committed in http://drupalcode.org/jsonapi/commit/?id=905e5e0

wim leers’s picture

I'm a bit concerned, because IIRC there was no consensus yet at #2135829: EntityResource: translations support on how to support requesting translation. Quoting #2135829-22: EntityResource: translations support:

We should not go ways to support languages in the domain/path in any way. That's going against restful principles.

Yet that is what this patch introduced! I don't see this discussed here at all. We need to be very careful here.


Also, a review of the last patch:

  1. +++ b/src/Controller/EntityResource.php
    @@ -715,6 +715,9 @@ class EntityResource {
    +    $entity_repository = \Drupal::service('entity.repository');
    +    $entity = $entity_repository->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']);
    

    Looks good!

  2. +++ b/src/Normalizer/EntityReferenceFieldNormalizer.php
    @@ -86,7 +86,10 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
    +      // Get the referenced entity.
    +      $entity = $item->get('entity')->getValue();
    +      // And get the translation in the requested language.
    +      return $this->entityRepository->getTranslationFromContext($entity);
    

    Woah this is tricky!

  3. +++ b/tests/src/Functional/JsonApiFunctionalBaseTest.php
    @@ -0,0 +1,280 @@
    +  /**
    +   * Performs a HTTP request. Wraps the Guzzle HTTP client.
    +   *
    +   * Why wrap the Guzzle HTTP client? Because any error response is returned via
    +   * an exception, which would make the tests unnecessarily complex to read.
    +   *
    +   * @see \GuzzleHttp\ClientInterface::request()
    +   *
    +   * @param string $method
    +   *   HTTP method.
    +   * @param \Drupal\Core\Url $url
    +   *   URL to request.
    +   * @param array $request_options
    +   *   Request options to apply.
    +   *
    +   * @return \Psr\Http\Message\ResponseInterface
    +   */
    +  protected function request($method, Url $url, array $request_options) {
    +    $url->setOption('query', ['_format' => 'api_json']);
    +    try {
    +      $response = $this->httpClient->request($method, $url->toString(), $request_options);
    +    }
    +    catch (ClientException $e) {
    +      $response = $e->getResponse();
    +    }
    +    catch (ServerException $e) {
    +      $response = $e->getResponse();
    +    }
    +
    +    return $response;
    

    This can be massively simplified, see \Drupal\Tests\rest\Functional\ResourceTestBase::request.

wim leers’s picture

Status: Fixed » Active
e0ipso’s picture

As I see it, we should not leave the code as is in the current state since there were translation inconsistencies where some things were translated and some not. Meaning that the user was still able to hit the language variant URL (/ca/jsonapi/node/article) before this patch and get some mixed results.

This commit/issue sets some honesty to that limitation. I agree that the URL negotiation is REST impure, but with this code we could have support for any language negotiation. Someone may add a contrib that negotiates language based on Accept-Language header, and with this code we support that.

That's going against restful principles.

I'm going to be the unpopular guy and say that we're not being RESTful purists to start with. Core does not achieve the RESTful holy grail either. The main difference is that core tries to. We are playing the practicality tune here, and I'd rather have this fixed in an imperfect way than wait on consensus in a 14 Nov 2013 issue. When that happens we need to see if it makes sense for us. Core outputs 1 entity, we output many, someone may argue for a different language for each (?) …

That being said, let's keep this as Active to show contributors that we are still open to adopt a more REST compliant resolution.

wim leers’s picture

As I see it, we should not leave the code as is in the current state since there were translation inconsistencies where some things were translated and some not. Meaning that the user was still able to hit the language variant URL (/ca/jsonapi/node/article) before this patch and get some mixed results.

That's fair. But we could have changed it to never do any translation.

I'm going to be the unpopular guy and say that we're not being RESTful purists to start with. Core does not achieve the RESTful holy grail either. The main difference is that core tries to. We are playing the practicality tune here, and I'd rather have this fixed in an imperfect way than wait on consensus in a 14 Nov 2013 issue.

That's also fair. But it should've been discussed prior to commit. And now that it is committed, we should explicitly state this in the docs (i.e. in README.md).

Once it is documented, I think it's fine to close this.

dawehner’s picture

We should not go ways to support languages in the domain/path in any way. That's going against restful principles.
Yet that is what this patch introduced! I don't see this discussed here at all. We need to be very careful here.

Well to be honest if you care about this restfull ness you can configure Drupal to take into account something else. The language negotation system is really flexible, so you could configure your site to be more pure.

Note: We've learned that the pure REST stuff doesn't serve us really well anyway, just look at HAL and its complexity.

e0ipso’s picture

@Wim is this something you want to take on?

This can be massively simplified, see \Drupal\Tests\rest\Functional\ResourceTestBase::request.

Maybe we can create a separate issue for it.

e0ipso’s picture

Category: Feature request » Task
Issue tags: +Needs documentation

We still need docs on this.

wim leers’s picture

Title: [FEATURE] Provide GET support for translated entities » [FEATURE] Support for translations
Assigned: e0ipso » Unassigned
Related issues: +#2897230: [PP-1] Creating translations (via PATCHing entities?)
wim leers’s picture

Title: [FEATURE] Support for translations » [PP-2] [FEATURE] Support for translations
Status: Active » Postponed
wim leers’s picture

Version: 8.x-1.x-dev » 8.x-2.x-dev
gabesullice’s picture

Title: [PP-2] [FEATURE] Support for translations » [PP-1] [FEATURE] Support for translations
Issue tags: +API-First Initiative
Related issues: +#2904423: Translated field denormalization creates duplicate values

Let's not postpone this on support in REST module, let's just postpone it on the underlying issue: #2904423: Translated field denormalization creates duplicate values, which looks like it got stalled, but could use some new life.

I'm not even convinced that that issue block us, since we don't use the same normalizations as HAL+JSON. Maybe @Wim Leers could do a little digging?


Thinking about CUD in CRUD, I think we may want to transition from a URL-path based approach to language negotiation to a query parameter based method. E.g. /jsonapi/node/article/{uuid}?lang_code=en-us

Why?

  1. As I understand it, `Accept-Language` has a lot of the same issues with caching and support that forced the `_format` query parameter to be a thing.
  2. If we want to use HATEOAS links to navigate translations, header-based negotiation will be difficult
  3. If we end up making a JSON API profile extension, it will be easier with query params since URL paths are not at all part of the spec.
wim leers’s picture

Title: [PP-1] [FEATURE] Support for translations » Support for translations
Priority: Normal » Major
Status: Postponed » Active
Issue tags: +Needs change record

We don't even need to block this on #2904423: Translated field denormalization creates duplicate values then, because that issue is about fixing a bug in the hal module's normalizers. Which don't affect JSON API: JSON API only reuses some of the serialization module's normalizers.

I'm not even convinced that that issue block us, since we don't use the same normalizations as HAL+JSON. Maybe @Wim Leers could do a little digging?

You were exactly right!

wim leers’s picture

Category: Task » Feature request

I omitted the [FEATURE] prefix … but it's marked as a task. Let's mark it as a feature request.

wim leers’s picture

@e0ipso, do you have feedback on @gabesullice's proposal in #43?

wim leers’s picture

Assigned: Unassigned » wim leers

I started working on this!

wim leers’s picture

Status: Active » Needs review
StatusFileSize
new1.07 KB

The initial patch was committed in #31.

Let's start by expanding the test coverage for what is working today.

wim leers’s picture

StatusFileSize
new1.71 KB
new2.39 KB

Adds test coverage for all hitherto untested routes, to verify that translations are in fact respected there too!

wim leers’s picture

StatusFileSize
new938 bytes
new3.27 KB

With #49, we have at least basic test coverage for each of the routes (plus includes). This allowed me to attempt to remove either of the three getTranslationFromContext() calls that were added to JSON API in #31.

I was able to remove only 1 out of 3: the one in \Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer::normalize()! This makes sense, because by the normalizer is given an existing entity object, and that should already be respecting translations. (This also happens to be the one I criticized in #33.2.)

The remaining two are in:

  1. \Drupal\jsonapi\Controller\EntityResource::getAccessCheckedEntity() (necessary for loading the relevant translation after performing an entity query to get a particular translation)
  2. \Drupal\jsonapi\ParamConverter\EntityUuidConverter (necessary when the entity object is a route parameter passed to the controller directly, for example the individual route)

I think I should be able to remove the latter too.

wim leers’s picture

I was able to remove the getTranslationFromContext() call from EntityUuidConverter if it weren't for the test coverage that I added in #49. Already the test coverage is proving useful… 🙏

Having only getAccessCheckedEntity() call getTranslationFromContext() is not enough because \Drupal\jsonapi\Controller\EntityResource::getRelationship() does not call getAccessCheckedEntity().

So for now, we'll stick with the two remaining getTranslationFromContext() calls.

wim leers’s picture

Assigned: wim leers » Unassigned
StatusFileSize
new14.05 KB
new14.85 KB

Next: trying to make #43 happen. @gabesullice made good points about why header-based negotiation is not a good fit, and why this is a place where core's pluggability/extensibility wrt language negotiation makes things harder: because we can't guarantee a certain DX.

This is what that would look like.

And that actually allowed us to remove EntityUuidConverter's getTranslationFromContext() too! Now there's only one getTranslationFromContext() left!

Before we go further in this direction, this needs to get reviews from @gabesullice and @e0ipso.

wim leers’s picture

Further questions:

  1. Do we simply not deal with POST, PATCH and DELETE for now?
  2. How do we ensure we still keep the ability to add that in the future? (I don't think anything in HEAD or in the current patch prevents us from adding it in the future per se.)
  3. Should we add support for sending multiple translations in a single JSON API document?

EDIT: quoting @e0ipso in #2987205-22: FormatSetter doesn't set the format to `api_json` when accessing just `/jsonapi`:

Oddly enough no one ever asked about more than that when it comes to translations.

That suggests the answer to my first question is "it's not worth doing right now".

Status: Needs review » Needs work

The last submitted patch, 52: 2794431-52.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new1.78 KB
new16.59 KB

This should fix most failures.

Status: Needs review » Needs work

The last submitted patch, 55: 2794431-55.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

wim leers’s picture

Questions still to be answered:

  1. Plan for POST/PATCH/DELETE.
  2. links entries pointing to other translations?
  3. What happens when requesting a non-existing translation (for a particular entity or for a collection)?
  4. What happens when requesting a non-installed langcode?
  5. … probably more questions still?
grimreaper’s picture

Hello,

Here is a rebased version of the patch from comment 55.

I can't get an interdiff.

I need to test it.

Also what should be done for POST / PATCH / DELETE?

I need it for a multilingual project.

wim leers’s picture

Also what should be done for POST / PATCH / DELETE?

What do you propose? How do you think it should work?

grimreaper’s picture

How I think it should work.

Create a new content in the specified langcode (or current langcode if not specified): POST jsonapi/node/article?lang_code=LANGCODE

Create a new translation/Update an existing translation (lang_code parameter mandatory): PATCH jsonapi/node/article/UUID?lang_code=LANGCODE

Delete a translation: DELETE jsonapi/node/article/UUID?lang_code=LANGCODE

Delete the whole entity: jsonapi/node/article/UUID

What happens when requesting a non-existing translation (for a particular entity or for a collection)?

Use the native behavior to display the entity in another language depending on languages weight.

For deletion of a non-existing translation, returns an explicit error message.

What happens when requesting a non-installed langcode?

Use the native behavior to display the entity in another language depending on languages weight.

If it PATCH / POST / DELETE operation on a non installed langcode, returns an explicit error message.

gabesullice’s picture

Since #43, I've become much more familiar with RFC8288 and web linking. It defines an "hreflang" link parameter for links. Which implies that a single URL href would/could serve multiple languages. This is correct. Because GET, POST, PATCH and DELETE can use the Accept-Language and Content-Language headers on request and responses.

I would like to argue against myself in #43 then and say this should be primarily header based.

Why?

Because if we want to use links to navigate languages, we can either have:

{
  "links": {
    "self": {
      "href": "https:/my.example.com/jsonapi/foo/bar/{{some-uuid-here}}",
      "meta": {
        "hreflang": "nl,fr,de",
      }
    }
  }
}

and have clients use Accept-Language to request the various languages... or, we can have the much uglier query parameter-based negotiation:

{
  "links": {
    "self:f0a9ihfa": {
      "href": "https:/my.example.com/jsonapi/foo/bar/{{some-uuid-here}}?lang_code=nl",
      "meta": {
        "hreflang": "nl",
      }
    }
    "self:0aijfd3x": {
      "href": "https:/my.example.com/jsonapi/foo/bar/{{some-uuid-here}}?lang_code=fr",
      "meta": {
        "hreflang": "fr",
      }
    }
    "self:lk2y3iaf": {
      "href": "https:/my.example.com/jsonapi/foo/bar/{{some-uuid-here}}?lang_code=de",
      "meta": {
        "hreflang": "de",
      }
    }
  }
}

Yuck.


So, why not header-based negotiation?

Because some CDNs do not have very good support for varying their caches by Accept-Language and naively varying by Accept-Language can destroy cache performance (there are ways to do it right though).

That said, I don't think that's reason enough to abandon header-based negotiation.

The reason Accept-Language can destroy cache performance is because browsers produce wildly different values for that header.

However, JSON API doesn't need to be very concerned about that because JSON API requests are not typically made directly via a browser. Instead, they're most frequently made via JavaScript or some other system where the Accept-Language header can be tightly controlled by a developer.

There's one caveat. Some CDNs simply don't support cache variation by Accept-Language at all.

In that case, I'll propose a compromise. A container parameter which enables negotiation via a query parameter. This should be off by default. In that way, we won't leave anyone completely stuck, but we'll be doing things "right" by default.

gabesullice’s picture

Actually, the "compromise" I suggested in #61 could be a way to keep the current path-based negotiation in place without breaking BC. If it's not on by default, I'm ambivalent about whether we use a query parameter or a path prefix to do language negotiation in the URL.

e0ipso’s picture

@gabesullice we should probably use the first applicable active language negotiator on the site. Applicable in the sense of being a URL based negotiator, but I'm unsure how to detect that a negotiator is URL based.

wim leers’s picture

Actually, like I've said since the very beginning: I am concerned about using Drupal's language negotiation system. That system is intended for web sites, not for HTTP APIs. For a HTTP API, we IMHO want it to be documentable, and not for it to have dozens of possible permutations of how it might work depending on how some module is configured.

Am I the only one who feels this way?

gabesullice’s picture

I think I agree with @Wim Leers here. We want our solution to be documentable, deterministic and API-First.

@e0ipso, maybe @Wim Leers and I are overlooking something? You seem confident in your proposal. But I don't see what problems doing what you say would avoid or what it makes possible. IOW, what would we lose by not using language negotiators?

Here's my reasoning for not using them:

AFAICT, the language negotiation stuff is pretty much just a menu of workarounds for browsers being bad at Accept-Language. And that's just another way of saying "the system is intended for web sites".

Since we don't have the same constraints to contend with in an API-First/decoupled environment, I think we can ignore the language negotiators (or have our own and only use that one).

What we gain is an easily documentable solution that doesn't change from application to application and that can make use HTTP and web linking as it's meant to be used (which buys us robustness and compatibility).

wim leers’s picture

AFAICT, the language negotiation stuff is pretty much just a menu of workarounds for browsers being bad

I disagree with this.

The language negotiation system used on a specific website depends on the locale: in Belgium it's common to see company.be/nl and company.be/fr. I've got the impression that in the primarily Anglo-Saxon countries (U.K., U.S., Australia, Canada, and others), it's based on TLD: company.co.uk, company.com.aucompany.ca and company.com.
This makes sense for those cases where the URL is the interface used by entire non-technical peoples. Because in that case, it's essentially joint conventions — much like hyperlinks are always underlined.

But in the context of HTTP APIs, the user is not a non-technical one, but a developer. And hence in this case, we need less flexibility and more predictability.

gabesullice’s picture

@Wim Leers, I'm not convinced, but it'd just be noise to continue debating about why we've come to the same conclusion :P

wim leers’s picture

Assigned: Unassigned » e0ipso

Heh, fair :)

@e0ipso, can you explain your rationale for why you think using Drupal's existing language negotiation system does not cause DX and maintainability problems & risks?

corbacho’s picture

Sorry to hijack the issue, to expose a practical real problem related to the discussion:

We are using JSON:API to fetch content for a Gatsby multilingual site.
We are relying currently on the "implicit entity translation magic" that core does. So we have 2 endpoints in Gatsby config: one for Finnish/fi/jsonapi, one for English content /en/jsonapi.
Gatsby recursively will grab every single entity exposed via JSON:API by default, in 2 iterations. The order is important, as you will see:
1st) in Finnish /fi/jsonapi
2nd) in English /en/jsonapi
It collects all the data into a single big JS variable, indexed by "language-id" (if it would be indexed only by id, it would get overwritten)
Setup explained here https://github.com/gatsbyjs/gatsby/issues/10020

This setup works perfectly if all the articles are translated. But if an article is only provided in Finnish, then the second iteration Gatsby does (fetching articles in English), will ALSO get the Finnish version (because of the un-wanted language fallback). This will result in duplicated content in Gatsby, by indexing two nodes with the same id:
fi-98303 (Finnish article)
en-98303 (Finnish article)

To fix this problem we tried a simple solution: append ?filter[langcode][value]=fi for every request that Gatsby does, but it didn't work because:
1) There are many entities that will throw error Invalid nested filtering. The field 'langcode', given in the path 'langcode, does not exist
2) Paragraph entities are not filterable by langcode (even if they have the attribute). They just return an empty array. (no error)

So our current solution is a hack to detect in Gatsby Drupal plugin code if it's fetching URLs that contain the string /node/. Then, we add the langcode filter. This works well for nodes, and taxonomy terms so far.

It would be good if JSON:API module would offer a simpler way to avoid fallback translations.

wim leers’s picture

Thanks, @corbacho! 🙏 Much appreciated, that concrete use case helps ensure we will settle on a solution here that takes that use case into account 👍

gabesullice’s picture

@corbacho, would it be possible to make Gatsby work using a Accept-Language header and not use a different URL/query param per language? That would be what I would like to implement here.

robertoperuzzo’s picture

I would like to share a rough solution I thought yesterday to fix node translation in our multilingual site project. We use different endpoint to "PATCH" node in different language:

  • /it/node/1095?_format=hal_json, to update original node
  • /en/node/1095?_format=hal_json, to update english translation

In our site the default language is IT and we noticed that: if the node has no EN translation the PATCH request overwrite the IT node values. So I decided to intercept the jsonapi call and check if that node has a translation, if not, I create it.

Probably you can suggest me a better way to fix that. Here is my event subscriber snippet:

<?php

/**
 * @file
 * Contains \Drupal\sv_jsonapi\EventSubscriber\TranslationSubscriber.
 */

namespace Drupal\sv_jsonapi\EventSubscriber;

use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\node\Entity\Node;

/**
 * Event Subscriber Transaltion.
 */
class TranslationSubscriber implements EventSubscriberInterface {


  /**
   * Code that should be triggered on event specified
   */
  public function onRequest(GetResponseEvent $event) {

    // Only preload on json/api requests.
    if ($event->getRequest()->getRequestFormat() == 'hal_json') {
      $default_language = \Drupal::languageManager()->getDefaultLanguage()->getId();
      list(,$language,,$nid) = explode('/', $event->getRequest()->getPathInfo());

      if (empty($language)) {
        $language = $default_language;
      }

      if ($language != $default_language) {
        $node = Node::load($nid);
        if (!$node->hasTranslation($language)) {
          \Drupal::logger('sv_jsonapi')->debug(
            "Node with ID @id has no '@lang' translation: create it!", [
              '@id' => $nid,
              '@lang' => $language,
          ]);
          $node->addTranslation($language, ['title' => $node->label()])->save();
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    // Set a high priority so it is executed before routing.
    $events[KernelEvents::REQUEST][] = ['onRequest', 1000];
    return $events;
  }

}
wim leers’s picture

#72: thanks for sharing that! 🙏 But it's about the hal_json format provided by core's rest module, not the jsonapi contrib module :) Feel free to leave that comment here, but could you repeat it in #2135829: EntityResource: translations support? Thanks!

This is still very valuable to keep here, because it highlights exactly the reason why I want to make sure we have a plan for not just reading translations, but also writing translations. Because otherwise edge cases like the one you encountered won't be taken into account and may be impossible to solve without breaking backwards compatibility.

bbuchert’s picture

Just to add my usecase here:
It would make sense to also include a property that indicates which translations of a page are available. If I built a language switch that would be really helpful to only show available translations there. And not having to make an extra request for that.

wim leers’s picture

It would make sense to also include a property that indicates which translations of a page are available.

Absolutely! The good news is that that's exactly what #2994193: [META] The Last Link: A Hypermedia Story is about! IOW: for any given entity, there'll be links to the other available translations, much like content_translation_page_attachments() does for HTML responses Many details still to be worked out, especially for cases like possible translations but that don't exist yet, or translations with automatic fallbacks (e.g. U.K. English falling back to U.S. English).

It's great to have explicit use cases listed here, so don't be shy, keep commenting :)

corbacho’s picture

@gabesullice

would it be possible to make Gatsby work using an Accept-Language header and not use a different URL/query param per language?

Yes, it could work. Gatsby drupal plugin is implemented with axios, so it's a matter of adding the header to the axios request.
A minor negative consequence would be that it makes impossible to see/debug JSON API language issues directly in the browser, true? you would need Postman, or some tool to see JSON API responses per language, but it's acceptable.

wim leers’s picture

Title: Support for translations » [META] Translations support
Assigned: e0ipso » Unassigned
Category: Feature request » Plan
Issue summary: View changes

This issue was in desperate need of an issue summary update!

Also marking this a "plan" issue, making this a sibling of #2795279: [PP-2] [META] Revisions support.

wim leers’s picture

Issue summary: View changes
wim leers’s picture

Title: [META] Translations support » [META] Formalize translations support

I think this may be a more accurate title.

xjm’s picture

Yesterday I discussed this issue with @Wim Leers, @gabesullice, and @effulgentsia, and we came up with the need for at least #3037804: Document the extent of JSON:API's multilingual support. and

I've since discussed this issue with both @larowlan and @gaborhojstsy. Since core REST suffers from similar problems, we are leaning toward making an exception to policy and adding JSON:API to core even with these feature gaps in place.

Some additional concerns we have before core inclusion (via @gaborhojtsy):

  • What "contracts" does JSON:API want to keep for multilingual functionality, since they need to keep BC with these bugs for X (define X, if we know that is only until D9 do we have something to mark deprecated?)
  • Are there code paths that need @todos added?
  • Is there tests to cover that this is how broken it is and to the ensure we keep supporting how broken it is now until we promise we do, etc.?
wim leers’s picture

What "contracts" does JSON:API want to keep for multilingual functionality, since they need to keep BC with these bugs for X (define X, if we know that is only until D9 do we have something to mark deprecated?)

It wants to keep everything working that happens to work automagically today.

Are there code paths that need @todos added?

No, that's what we have this issue for.

Is there tests to cover that this is how broken it is and to the ensure we keep supporting how broken it is now until we promise we do, etc.?

No. What works only happens to work due to magic in core's entity route param converter.

plach’s picture

Disclaimer: all the following reasoning is based on the IS, I didn't have a look at the code and I'm not familiar at all with it, so take it for what it's worth :)


@xjm asked me to have a look at this with particular focus on data loss/integrity issues. I think an obvious candidate for more scrutiny is the following remaining task:

Plan for POST + PATCH + DELETE.

In particular, I think it's concerning that we are silently applying fallback rules while performing update and delete operations. Drupal's HTML UI does apply fallback rules as well, but performs a GET first, so users have a chance to notice that they are dealing with a translation not matching the content language: both on the entity form and the deletion confirmation form this is explicitly signaled. In this case we might be happily performing changes to the wrong target without the client even noticing it, until the next GET.

Additionally, deleting a translation and deleting the whole entity are very different operations, so we can't really apply a DELETE operation to a non-default translation and fall back to the default one if the former is missing, because that would mean deleting all existing translations and the entity itself.

I'm wondering whether a possible mitigation strategy while we figure out a proper way forward (the solution proposed in the IS makes sense to me FWIW), could be to ignore the fallback logic in controllers responsible to handle the PATCH and DELETE requests and return a 404 if an explicit check reveals that there is no translation matching the negotiated content language.

POST operations do not seem as much concerning at first sight, since there is no fallback possible given that no translation exists yet.

wim leers’s picture

(Writing this on my phone.)

I realized after posting my last comment that I hadn't said I'd be happy to work on test coverage for this.

#82: Note that JSON:API behaves *identically* to core's REST module wrt translations. I'd be fine with implementing the mitigation strategy you propose, but note that some sites (I expect it to be an extremely tiny number though) may be relying on the current automagic behavior.

plach’s picture

Note that JSON:API behaves *identically* to core's REST module wrt translations.

I'm not sure whether that's a good reason to add another way to break things, if that's actually the case, of course :)

I'd be fine with implementing the mitigation strategy you propose, but note that some sites (I expect it to be an extremely tiny number though) may be relying on the current automagic behavior.

Could it be possible to opt-out via a setting or somehow conditionally enable the fix?

plach’s picture

Btw, tests to confirm that what #82 is describing actually happens (or not) would be more than welcome :)

xjm’s picture

Thanks @Wim Leers and @plach.

Re:

Are there code paths that need @todos added?

No, that's what we have this issue for.

For core, our best practice is to place @todos referencing the issue node in the relevant code paths and in the tests where the behavior needs to be changed. So it would be good to add those if possible.

Do we have core criticals already for the REST translation issues?

I think it's worth considering #83 and #84 (although #84 may be tricky if configuration is disallowed). However, we've made changes before to deny access for update operations that worked sometimes but caused data loss other times, as temporary workarounds until criticals are fixed. Either way, it sounds like maybe we'd want to have issues to do the same to REST as well -- or maybe there is a way to patch core to temporarily mitigate both until it's fixed?

Tests are definitely a good first step either way.

wim leers’s picture

I completely forgot that we already do have explicit test coverage for what is supported WRT translations! 😅Hurray!

See \Drupal\Tests\jsonapi\Functional\JsonApiFunctionalMultilingualTest, which has existed since January 2017, it was committed in #31. The issue was reopened shortly after that due to concerns about relying on the automagic context-dependent translation loading.

Working to expand that now.

wim leers’s picture

Issue summary: View changes

(the solution proposed in the IS makes sense to me FWIW)

Thanks for saying that in #83, that's great to know for the future, when we make JSON:API's translation support feature-complete :)

Working to expand that now.

Done: #3038308: Avoid translations DELETE data loss and unintended changes with PATCH and test all methods against entity route parameter translation upcasting. A sequence of tiny interdiffs with focused tests per use case is awaiting, and I’ve posted my analysis + proposals: https://www.drupal.org/project/jsonapi/issues/3038308

niklan’s picture

Comment deleted, move to separate issue as asked in #91.

wim leers’s picture

#90/@Niklan: Could you please post that as a separate issue? This issue is for planning, not for concrete questions. Thanks!

e0ipso’s picture

During DrupalCon we talked about adding hypermedia support for translation. Ideally we would have a standard way to link to the translation versions of an entity in the document.

e0ipso’s picture

tresti88’s picture

Copying from an initial slack conversation

"attributes": {
        	"name": [
		      {
		        "data": "Top",
		        "locale": "en"
		      },
		      {
		        "data": "Débardeur",
		        "locale": "fr"
		      }
		    ]
		} 

"So i have loosely looked at Spryker and Akeno. It looks like their implementations are slightly different in terms of dealing with different locals. Akeno uses the following format within attributes which i think mostly conforms to the JSON:API specification? although i'm not sure if having data as an object property is right? If we were to implement this type of approach then we would probably have to refactor the `GET` logic so it matches with this format. This would work for when we create `POST` and `PATCH` requests as we would know which entity the translations get attached to. This definitely feels like it could work.

In Spryker they use an `Accept-Language` header that's used to get an entity for that language, this is kind of similar to how Drupal's JASON:API currently works albeit the difference being that the url has the language code in it. (@e0ipso mentioned that Drupal can be configured to do this too). Obviously if we were to use a `POST` request to create a 'French' version of an 'English' entity then one of the main issues is that we don't know which entity to attach the translation to unless we send some sort of relationship identifier in the request. Can we do this with the JSON:api spec? If not this might mean instead we use PATCH to create/update languages against an entity via the correct language endpoint or Accept-Language header. One thing to note is that we aren't actually creating new entity instances because we are just updating an entity although again this doesn't feel 100% correct. What are your thoughts?"

j1mb0b’s picture

I created a sandbox which is a decoupled module for JSON:API https://www.drupal.org/sandbox/kingmackenzie/3061283, similar in the way JSON:API Extra's is working in terms of overriding Normalizers but purpose-built to handle entity translation. It's inspired based on patches in this issue: https://www.drupal.org/project/drupal/issues/2135829

wim leers’s picture

Project: JSON:API » Drupal core
Version: 8.x-2.x-dev » 8.8.x-dev
Component: Code » jsonapi.module

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

arla’s picture

Filtering does not seem to take current language into account.

I have a news node whose field_slug is dog in English and hund in Swedish. When I use JSON:API with filter[field_slug]=hund then that news node is in the result regardless of what language I call JSON:API in (i.e. both for /jsonapi/node/news and /sv/jsonapi/node/news).

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

spokje’s picture

Issue summary: View changes

I have a client who wishes to push translations in JSON:API forward and is willing to sponsor me to work on this.
To get to the actual (development) work, we first need to agree on what is the best way forward.

Below is my proposal on a way forward. Where applicable/possible I will try to explain why I've made specific choices that might differ from what was discussed (but not really agreed upon) in this very issue.

1) Use a query parameter for translations, let's stick with the already proposed lang_code=[2-letter-langcode].

I've decide not to go with the discussed Accept-Language header. After a brief discussion with @gabesullice yesterday, we both think that the chance that CDNs/Proxies don't use/accept this specific header is too big.

2) One URL to rule them all!

I propose to drop all the fallback magic used for the HTML side of Drupal, meaning we ignore all the language negotiation logic that's in place on a site. Which would mean that the endpoint URL /jsonapi/foo/bar/ will consistently be used for any kind of JSON:API call, in the case of working with translations with the above mentioned query parameter lang_code=[2-letter-langcode].

As far as I can see this is the only way to get a consistent endpoint for any Drupal multilangual site no matter what Content language detection method is used on that site.

Also by not using the fallback logic we can get rid of the use of ContentEntityBase::getTranslationFromContext (which returns a fallback if a node doesn't have a translation) and use ContentEntityBase::getTranslation which only returns an entity when there is a translation in the requested language available. If not, an Exception is thrown.

The fallback magic is very useful when serving HTML pages and returning a 404-page will make the user experience bad, but in JSON:API land, where the nerdy cool kids hang out, I think we need a binary answer. You ask for a specific translated entity, you either get it or a "Does Not Exist", nothing else.

3) Return a list of available translations in the meta of each entity, need validated by @bbuchert's comment in #74 and proposed by @gabesullice in #61

{
  "links": {
    "self": {
      "href": "https:/my.example.com/jsonapi/foo/bar/{{some-uuid-here}}",
      "meta": {
        "hreflang": "nl,fr,de",
      }
    }
  }
}

4) There will be no opt-in for this new behaviour described above.

This will be BC-breaking, but only for users already working with the current JSON:API for translations. Usage of JSON:API without the translations will be "Business As Usual". The added meta in 3) should not be breaking anything.

The reason for not using an opt-in is because the current translation JSON:API is flawed at best (See for example #98 and Language negotiation doesn't respect langcode filter). IMHO that would mean most/all people wanting to use the "new" translation JSON:API will opt-in anyway.

Also we would have the (again IMHO) "nasty" situation of supporting 2 solutions for the same functionality, where one is better than the other. Let alone this being confusing for users/developers and most probably create a lot of noise with support topics on d.o.

On top of that, if we do choose for an opt-in, at some point we have to deprecate one of the 2 solutions anyway, so I think we need to bite the bullet, and make the people who are currently using translations in JSON:API to update their code. (Side-note: By the looks of it people are forced to jump through hoops with the current setup anyway, and might end up with cleaner code, see for example #69)

Disclaimer: I am by no means a JSON:API nor multilingual expert, so there might/probably will be gigantic errors/loopholes in the above proposal. That's why I call upon "The Big Brains" around here to look and shoot at this proposal.

After we reach a consensus on the above, I plan to write up a proposal on the POST + PATCH + DELETE, which should be a lot easier when the above is formalized.

Then we (read I...) break up this whole monolith in sub-issues and we can move forward and get this done! (#FamousLastWords)

gabesullice’s picture

Wow, thanks for the write up!

I have a client who wishes to push translations in JSON:API forward and is willing to sponsor me to work on this.

🎉

1) Yes, unfortunately, I think we (read I) can't keep trying to do the proactive content negotiation thing—as much as it pains me. @bradjones1 and I tried to make it work in #3028501: Enable header-based proactive content negotiation with optimizations and opt-outs available. and I think we figured out how to do it technically, but I don't think the internet is ready for it (see @Wim Leers comment #35.

2) I agree that getting rid of fallbacks is key. I also agree with your conclusion that getTranslationFromContext() is great for HTML, but not an HTTP API. Therefore, I definitely agree that if a matching translation doesn't exist for a particular URL either because the langcode doesn't exist or because an appropriate translation doesn't exist then returning a 404 (not found) response is the right thing to do™

3) My suggestion in #61 for a single link only applies to header-based neg. I don't think this link style will work because the href doesn't have a langcode in it. We'll need a link for each translation with a unique href unless we want to go down the URI Template path (we probably don't).

4) Breaking BC just ain't gonna fly 😞 (been there, done that). We're going to have to maintain BC for any URLs that aren't very obviously "broken". We may not need to maintain BC for filtering collections because, as you pointed out, those have very confusing behaviors, but we'll definitely have to preserve the fallback behaviors for GETing individual resources. That likely means a BC flag to toggle between using ContentEntityBase::getTranslationFromContext and ContentEntityBase::getTranslation.

That BC flag doesn't have to be opt-in, per se. We can automatically "opt in" existing installations (which can then "opt out"). New installations of JSON:API would use then use the correct, long-term behavior.


Questions

  1. Do you think it would be easier to preserve BC if we used URL prefixes instead of a query string? E.g. /fr/jsonapi/... vs. /jsonapi/...?langCode=fr
    • The best arguments against this is that it's a) fugly and b) not very well supported naive JSON:API clients that manually construct links using a "base path"
  2. Are 2-letter langcodes what we want? If so, why? (it might be as simple as "that's what Drupal supports", I honestly don't remember)

Final thoughts

I'd like to wait a few days to get @e0ipso's thoughts about fallbacks.
I'd like to hear @Wim Leers' thoughts too.

cgoffin’s picture

Because of the need to add translations using the JSON API, I started investigating and came with a solution. I hope it's a start to get this into core.

The JSON:API documentation states that a resource object is unique defined by a type and an id and since the uuid is the same for all translations of a resource I decided to add the translations using the PATCH method instead of the POST method. I see the translations as part of the resource object and not a different resource object.
To know if a translation needs to be created I use the langcode attribute (only if the entity type is translatable). If the langcode attribute is set, I create a new translation (if it doesn't exists already). Otherwise I change the loaded entity to the existing translation.

If this needs to be addressed in another ticket, please let me know.

bbrala’s picture

When I was reading the proposal of Spokje the first thing i though is it would be awesome to know the available languages for a resource. I saw in #61 @gabesullice made a point on this. But couldn't we just use a convention for that?

{
  "links": {
    "self": {
      "href": "https:/my.example.com/jsonapi/foo/bar/{{some-uuid-here}}?lang_code=nl",
      "meta": {
        "hreflang": "nl",
      }
    }
    "self:fr": {
      "href": "https:/my.example.com/jsonapi/foo/bar/{{some-uuid-here}}?lang_code=fr",
      "meta": {
        "hreflang": "fr",
      }
    }
    "self:de": {
      "href": "https:/my.example.com/jsonapi/foo/bar/{{some-uuid-here}}?lang_code=de",
      "meta": {
        "hreflang": "de",
      }
    }
  }
}

So we at least get the value of knowing what languages are available for that content?

This might get a little confisuing though when looking at recourse lists in a certain language, since you wouldn't include resources that are not included in the selected language...

Just my 2cts

blazey’s picture

It might be a good idea to make it possible to check access without actually translating the entity (either with getTranslationFromContext or the langcode argument).

Use case: jsonapi_views would be able to return different language versions in the same listing and still check access (see #3175437: Translations not applied).

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

wim leers’s picture

1) Use a query parameter for translations […]

👍

2) One URL to rule them all! […]

💯

Also by not using the fallback logic we can get rid of the use of ContentEntityBase::getTranslationFromContext (which returns a fallback if a node doesn't have a translation) and use ContentEntityBase::getTranslation which only returns an entity when there is a translation in the requested language available. If not, an Exception is thrown.

💯

3) Return a list of available translations in the meta of each entity, […] proposed by @gabesullice in #61

But didn't @gabesullice in #61 point out a problem with this approach? Plus, it's only for header

4) There will be no opt-in for this new behaviour described above.

This will be BC-breaking, but only for users already working with the current JSON:API for translations. Usage of JSON:API without the translations will be "Business As Usual". The added meta in 3) should not be breaking anything.

… as I was responding to the first 3 points, I was very worried about backwards compatibility. Fair point that the current design is broken. (I think I've always argued the same too, actually, and I think I was opposed to even adding this at all originally?) And it's indeed actively causing problems as you can see for example in #69's write-up.

But even then, we just cannot break BC. I do think that we can simply mirror the existing behavior by creating an additional event subscriber that is only enabled by default on existing sites, and it does continue to run the current ::getTranslationFromContext() behavior and then does a 301 redirect to the new URL. Then in Drupal 10 we'll be able to simply drop this event subscriber! This means all weirdness would be concentrated in a single place.


#101:

1) Yes, unfortunately, […] but I don't think the internet is ready for it (see @Wim Leers comment #35.

Well … as the author of said comment … I do want to point out that this means we're choosing not to pursue the technically best solution for the sake of a single use case: anonymous JSON:API instances served over HTTP/1 without TLS, and even then only for users behind a crappy proxy. It is fair to question whether we want to support that scenario at all.

b) not very well supported naive JSON:API clients that manually construct links using a "base path"

I think this is the best argument.

Are 2-letter langcodes what we want? If so, why? (it might be as simple as "that's what Drupal supports", I honestly don't remember)

I think we should simply use langcodes that hreflang prescribes: https://en.wikipedia.org/wiki/Hreflang. That means languages (ISO 639-1), countries (ISO 3166-1) and potentially also script variations (ISO 15924). We should definitely pay close attention to complying with these specs (and specifically with RFCs defining the behavior of hreflang) rather than exposing Drupal's internal defaults.

plach’s picture

I got some funding to work on this and got in touch with @Wim Leers and @gabesullice. We had a call last Friday during which we discussed how to bring this forward. The main aspects we discussed were how to keep BC and how to handle the existing language negotiation logic.

We agreed that a possible way forward is to introduce a new experimental module (JSON:API Translation), that would implement all the new/missing logic. This would allow people to opt-in without requiring any additional configuration. The goal would be to deprecate the legacy (GET) logic and merge the module into JSON:API in D10.

Implementation-wise we agreed that it should be ok to rely on the Accept-Language and Content-Language headers for the reasons stated at #3028501-35: Enable header-based proactive content negotiation with optimizations and opt-outs available.

@gabesullice and I had a follow-up call during which we drafted a "spec" proposal for the entity translation API exposed by the JSON:API Translation module. You can find it at:

https://github.com/gabesullice/drupal-jsonapi-translations/blob/master/i...

I'll start working on the implementation outlined over there, so that we have something to look at, but of course feedback on this proposal would be welcome :)

plach’s picture

From https://github.com/gabesullice/drupal-jsonapi-translations/blob/master/i...

* Existing article, non-default translation, using the canonical URL:

GET /jsonapi/node/article/{id}
Accept-Language: fr

// If translation exists:
200 OK
Content-Language: fr
Content-Location: /jsonapi/node/article/{id}?lang=fr

// If translation does not exist:
// option a)
// provide a default without redirecting
200 OK
Content-Language: en

// option b)
// do not provide a default or redirect, simply provide alternatives
406 Not acceptable
Link: </jsonapi/node/article/{id}?lang=en>; rel="alternate" hreflang="en"
Link: </jsonapi/node/article/{id}?lang=nl>; rel="alternate" hreflang="nl"
{links: {// same links, in json}}

// option c)
// redirect to a default and also provide alternatives
300 Multiple choices
Location: /jsonapi/node/article/{id}?lang=en
Link: </jsonapi/node/article/{id}?lang=en>; hreflang="en"
Link: </jsonapi/node/article/{id}?lang=nl>; hreflang="nl"
{links: {// same links, in json}}

Gabe and I would both go with option C, since that would both inform clients that the translation they requested is missing and allow them to decide whether performing the suggested redirect (to the default translation) or applying their own fallback logic.

wim leers’s picture

#108: +1 to option C 👍

Thoughts:

  • One addition to the Existing article, non-default translation, using a translation-specific URL: case, if possible: it'd be cool if we'd be able to still know when a translation used to exist and in that case not respond with 404 Not Found but with 410 Gone.
  • Why does POSTing a translation result in Location: /jsonapi/node/article/{id} without the ?lang query string? — ah because we're creating a new entity in a non-default translation!
  • The "Content-Language" header is just an alternative way to specify the entity "langcode" field. sounds cool but … do they have the exact same representations? Does Drupal core's langcode field conform to the same spec/does it use the same values as the Content-Language header? It'd be good to double-check.
  • Is the If no "langcode" and no "content-language" header are specified, the default logic configured at "/admin/config/regional/content-language" is applied. statement not equivalent to New article in default language:?
  • OMG THIS LOOKS SO GREAT! 🤩🥳 I'm so glad this is happening now! 💪😄

P.S.: this is the first time I've ever seen 300 Multiple Choices 🤓

plach’s picture

Status: Needs work » Needs review
Issue tags: +Needs tests, +Needs issue summary update
StatusFileSize
new49.24 KB
new848.48 KB
new2.83 KB

Here is a first patch for this. I was wondering whether a new issue would be in order, but actually all the relevant discussion is here and patch is not huge, so it maybe worth turning this meta into an operative issue. Reviews welcome :)


The patch defines a new experimental module which overrides the JSON:API controller with a translation-aware one. The language negotiated by core is simply ignored and all the actual translation negotiation logic is based on the spec draft. The main difference with the latter is that the query string parameter is not lang, which apparently is not JSON:API-compliant, but lang_code.

Also, wrt #108, the behavior implemented so far was A. This is of course not set in stone, but @Berdir brought up that not doing A could introduce performance issues, due to additional requests, especially when dealing with listings.

Listings is an area that the current patch does not address, since it wasn't covered by the spec Gabe and I came up with. Personally I think that we could use the freshly-introduced concept of translation as a standalone resource to provide language-specific listings:

  • GET /jsonapi/node/article would return a list of all nodes respecting the current Accept-Language header + fallback to the default translation, basically option A from #108.
  • GET /jsonapi/node/article?lang_code=fr would return a list of French translations (including default translations, i.e. articles originally created in French). Articles with no French translation would not be listed.

I think the latter may address the use case presented in #69.

I did not work on test coverage yet, since I have a limited budget, this is my next task unless there are big concerns with the current patch. People wishing to test the patch may use the Postman collection I used and the attached DB dump, if they do not wish to set an installation up. Please remember to configure the collection variables if you use it.


@Wim Leers, #109:

Sorry, I missed your comment, so the patch does not include any of the following, but here are a few replies:

1: If versioning is enabled, it may be possible to tell whether a previous version had a translation that now is missing, however IIRC with pending revisions it may not always be possible to distinguish the case where a translation was removed from one where it was added but not published yet. Worth investigating more.
3: Content-Language relies on the language tags specified in BCP 47, which in turn relies (also) on 2-digit ISO-639 language codes, which are the language codes of predefined languages exposed by the language subsystem. The latter is configurable, so actually it is possible to install custom languages with invalid language codes, but I don't think should be a concern of ours.
4: It depends :) By default yes, however the site default language may be different from the one resulting from the default logic configured in the content language settings. For instance that may hard code a specific non-default language as the default content language. OTOH this logic would apply for all entities, regardless of how they are created, if the language value is missing, so it is basically what have been happening so far already.

plach’s picture

Re #108, according to MDN, option A is recommended:

If the server cannot serve any matching language, it can theoretically send back a 406 (Not Acceptable) error code. But, for a better user experience, this is rarely done and more common way is to ignore the Accept-Language header in this case.

plach’s picture

I forgot to mention this:

+++ b/core/modules/jsonapi_translation/jsonapi_translation.info.yml
@@ -0,0 +1,8 @@
+dependencies:
+# TODO Should we add this?
+#  - drupal:content_translation
+  - drupal:jsonapi

+++ b/core/modules/jsonapi_translation/src/Routing/Routes.php
@@ -0,0 +1,147 @@
+    $translation_creation_route = new Route($individual_route_path);
+    $translation_creation_route->addDefaults([RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER_SERVICE_NAME . ':createIndividualTranslation']);
+    $translation_creation_route->setMethods(['POST']);
+    // TODO Allow users with translation permissions and no edit permissions to
+    //   handle translations.
+    $translation_creation_route->setRequirement('_entity_access', 'entity.update');

Currently only users with update entity access can perform CUD operations. I think we should allow translators, i.e. users not having update access to create, update, delete non-default translations (based on their permissions). These permissions are defined by the Content Translation module, so maybe it should also define the JSON:API routes to perform these operations? Or would it make more sense to make JSON:API's access control alterable so that CT can tweak it?

Status: Needs review » Needs work

The last submitted patch, 110: 2794431-jsonapi_translation-110.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

plach’s picture

Status: Needs work » Needs review

gabesullice’s picture

  1. +++ b/core/modules/jsonapi/src/Access/EntityAccessChecker.php
    @@ -187,6 +187,19 @@ public function getAccessCheckedResourceObject(EntityInterface $entity, AccountI
    +  protected function getEntityTranslation(EntityInterface $entity): EntityInterface {
    

    This is a nice helper.

  2. +++ b/core/modules/jsonapi/src/Controller/EntityResource.php
    @@ -786,6 +786,83 @@ public function removeFromRelationshipData(ResourceType $resource_type, EntityIn
    +  protected function getParsedEntity(ResourceType $resource_type, Request $request): EntityInterface {
    

    Suggestion: getEntityFromRequest

  3. +++ b/core/modules/jsonapi/src/Controller/EntityResource.php
    @@ -786,6 +786,83 @@ public function removeFromRelationshipData(ResourceType $resource_type, EntityIn
    +    $parsed_entity = $this->getRequestAttribute($request, 'jsonapi_parsed_entity', function (Request $request) use ($resource_type) {
    ...
    +    return $this->getRequestAttribute($request, 'jsonapi_field_names', function (Request $request) use ($resource_type) {
    ...
    +    return $this->getRequestAttribute($request, 'jsonapi_body_decoded', function (Request $request) {
    

    Interesting, you've turned all these into request attributes... I assume I'll see why a little further into the review.

  4. +++ b/core/modules/jsonapi/src/Controller/EntityResource.php
    @@ -786,6 +786,83 @@ public function removeFromRelationshipData(ResourceType $resource_type, EntityIn
    +  protected function getRequestFieldNames(ResourceType $resource_type, Request $request): array {
    

    I really like these helpers that you've added.

  5. +++ b/core/modules/jsonapi/src/Controller/EntityResource.php
    @@ -786,6 +786,83 @@ public function removeFromRelationshipData(ResourceType $resource_type, EntityIn
    +  protected function getRequestAttribute(Request $request, string $key, callable $value_callback) {
    +    $value = $request->attributes->get($key);
    +    if ($value) {
    +      return $value;
    +    }
    +    $value = $value_callback($request);
    +    $request->attributes->set($key, $value);
    

    Maybe check for $request->attributes->has($key) and then run the callback if the attribute is missing. The current implementation means that if the value is truly FALSE or NULL the callback will have to run no matter what. E.g.:

        if (!$request->attributes->has($key)) {
          $request->attributes->set($key, $value_callback($request));
        }
        return $request->attributes->get($key);
    
  6. +++ b/core/modules/jsonapi_translation/jsonapi_translation.info.yml
    @@ -0,0 +1,8 @@
    +description: Allows to translate content entities via the web API provided by JSON:API.
    

    Nit: "Allows the translation of content entities via JSON:API endpoints."

  7. +++ b/core/modules/jsonapi_translation/jsonapi_translation.info.yml
    @@ -0,0 +1,8 @@
    +# TODO Should we add this?
    +#  - drupal:content_translation
    

    I think so.

  8. +++ b/core/modules/jsonapi_translation/jsonapi_translation.services.yml
    @@ -0,0 +1,27 @@
    +  # Controller.
    +  jsonapi_translation.entity_resource:
    +    class: Drupal\jsonapi_translation\Controller\EntityResource
    +    arguments:
    +      - '@entity_type.manager'
    +      - '@entity_field.manager'
    +      - '@jsonapi.resource_type.repository'
    +      - '@renderer'
    +      - '@entity.repository'
    +      - '@jsonapi.include_resolver'
    +      - '@jsonapi.entity_access_checker'
    +      - '@jsonapi.field_resolver'
    +      - '@jsonapi.serializer'
    +      - '@datetime.time'
    +      - '@current_user'
    +      - '@language_manager'
    

    Oof, this is screaming that we have a missing abstraction in JSON:API proper (obviously, not your responsibility)

  9. +++ b/core/modules/jsonapi_translation/src/Access/EntityAccessChecker.php
    @@ -0,0 +1,28 @@
    +class EntityAccessChecker extends JsonApiEntityAccessChecker {
    

    maybe add a final?

  10. +++ b/core/modules/jsonapi_translation/src/Controller/EntityResource.php
    @@ -0,0 +1,616 @@
    + *   "Accept-Language" header, when accessing entities via the canonical URL.
    

    Thought: we must be sure that this is added in the Vary response header.

  11. +++ b/core/modules/jsonapi_translation/src/Controller/EntityResource.php
    @@ -0,0 +1,616 @@
    +  const PARAM_LANGCODE = 'lang_code';
    

    Thanks for using a constant here! Given the spec's recommendation, I think we should make this langCode (i.e. camel-cased)

  12. +++ b/core/modules/jsonapi_translation/src/Controller/EntityResource.php
    @@ -0,0 +1,616 @@
    +    $this->languageManager = $language_manager;
    

    Maybe we could use setter injection for this so that we don't have to copy all the arguments and use statements.

  13. +++ b/core/modules/jsonapi_translation/src/Controller/EntityResource.php
    @@ -0,0 +1,616 @@
    +  public function getIndividual(EntityInterface $entity, Request $request) {
    

    I'm wondering if instead of overriding this method, we should add a protected helper method and only call it if this module is installed, then override that method in this class. I suspect it might create a looser coupling (not 100% sure).

  14. +++ b/core/modules/jsonapi_translation/src/Controller/EntityResource.php
    @@ -0,0 +1,616 @@
    +        throw new UnprocessableEntityHttpException('The specified resource does not support translation.');
    

    This might be a little more helpful if we indicate that we determined that the client wanted a translation because for the request. E.g. "The request specified a preferred language, but the requested resource type does not support translations."

    Additionally, I don't think this is the right exception. I've only ever seen 422 used for something wrong in the body of an HTTP request, never the URL or headers. I think the word "entity" refers to the thing that's represented by the content of the request body, but I'm not 100% confident about that.

    I appreciate that you went for a more specific status code, but I think 400 really is the most appropriate here.

  15. +++ b/core/modules/jsonapi_translation/src/Controller/EntityResource.php
    @@ -0,0 +1,616 @@
    +    // If a translation resource was accessed via the "Accept-Language" header,
    +    // we inform the client about its canonical URL.
    +    if (!$resource_language) {
    +      $query = $request->query->all();
    +      $query[static::PARAM_LANGCODE] = $translation->language()->getId();
    +      $url = static::getRequestLink($request, $query)
    +        ->setAbsolute()
    +        ->toString(TRUE);
    +      $response->addCacheableDependency($url);
    +      $response->headers->set('Content-Location', $url->getGeneratedUrl());
    +    }
    

    😍


Ran out of time to finish this review, but I'm posting what I have so far. More soon!

plach’s picture

Thanks, in the meantime I'm posting a minor update to fix an issue that was preventing the patch from working on case-sensitive file systems.

plach’s picture

And now with 100% more patch attached!

Status: Needs review » Needs work

The last submitted patch, 118: 2794431-jsonapi_translation-118.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

plach’s picture

Status: Needs work » Needs review
plach’s picture

This should fix cache support for GET requests. Now I think it might make sense to split this into a few subtasks.

Status: Needs review » Needs work

The last submitted patch, 121: 2794431-jsonapi_translation-121.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

plach’s picture

Status: Needs work » Needs review
gabesullice’s picture

A bit more... overall, I am very impressed! I like where this is headed :)

Did you consider overriding EntityResource::getEntityTranslation? If so, I assume you did not do that because it doesn't have access to the $request (we could change that)?


  1. +++ b/core/modules/jsonapi_translation/src/Controller/EntityResource.php
    @@ -0,0 +1,628 @@
    +        $langcode_key = $this->entityTypeManager
    +          ->getDefinition($resource_type->getEntityTypeId())
    +          ->getKey('langcode');
    +
    +        if (!isset($body['data']['attributes'][$langcode_key])) {
    

    JSON:API allows field names to be aliased, so we need to do a little limbo here, like so:

    $field_name = $resource_type->getPublicName($langcode_key);
    if (!isset($body['data']['attributes'][$field_name]))
    
  2. +++ b/core/modules/jsonapi_translation/src/Controller/EntityResource.php
    @@ -0,0 +1,616 @@
    +    $language = $this->getRequestAttribute($request, 'jsonapi_translation_language', function (Request $request) {
    

    Should jsonapi_translation_language be a static class constant too?

  3. +++ b/core/modules/jsonapi_translation/src/Controller/EntityResource.php
    @@ -0,0 +1,616 @@
    +      $header_langcode = $request->headers->get(static::HEADER_CONTENT_LANGUAGE);
    

    Nit: Maybe s/$header_langcode/$content_language_header because while I was reviewing this I kept forgetting that this variable contained the value of the Content-Language header, not the Accept-Language header.

  4. +++ b/core/modules/jsonapi_translation/src/Controller/EntityResource.php
    @@ -0,0 +1,616 @@
    +
    

    I think we should throw a 400 if $header_langcode && $request->isMethodCacheable() since the getIndividual() method is calling $this->getResourceLanguage() to determine if a langcode was explicitly provided and we should not support a request like:

    GET /jsonapi/node/article/{id}
    Content-Language: fr
    

    As an accidental way to get the fr translation from the canonical URL.

  5. +++ b/core/modules/jsonapi_translation/src/Controller/EntityResource.php
    @@ -0,0 +1,616 @@
    +        $message = 'Translation resource language mismatch: "%s" ("%s" query string parameter) vs "%s" ("%s" header).';
    

    ❤️

  6. +++ b/core/modules/jsonapi_translation/src/Controller/EntityResource.php
    @@ -0,0 +1,616 @@
    +        throw new UnprocessableEntityHttpException(sprintf($message, $param_langcode, static::PARAM_LANGCODE, $header_langcode, static::HEADER_CONTENT_LANGUAGE));
    

    Same as above, I think this should be a plain 400 status code. This is fine because it's a mismatch between the URL and the content-language, which, unlike above is about the request entity.

  7. +++ b/core/modules/jsonapi_translation/src/Controller/EntityResource.php
    @@ -0,0 +1,616 @@
    +      throw new UnprocessableEntityHttpException(sprintf('Invalid language "%s" specified', $langcode));
    

    Nit: maybe "The specified language ("%s") is invalid or has not been configured."

  8. +++ b/core/modules/jsonapi_translation/src/Controller/EntityResource.php
    @@ -0,0 +1,628 @@
    +  protected function buildWrappedResponse(TopLevelDataInterface $primary_data, Request $request, IncludedData $includes, $response_code = 200, array $headers = [], LinkCollection $links = NULL, array $meta = []) {
    +    $translation = $request->attributes->get(static::ATTR_TRANSLATION_RESOURCE);
    

    I know that this attribute will not be set on collection requests, but can we add a guard like this so that we never accidentally apply this to collection responses?

    if ($primary_data instanceof Data && $primary_data->getCardinality() !== 1) {
      return parent::buildWrappedResponse($primary_data, $request, $includes, $response_code, $headers, $links, $meta);
    }
    

    (We should probably add getCardinality to TopLevelDataInterface while we're at it so we don't need a check against the concrete class, but that could be a follow up)

plach’s picture

Updated IS.

I will create a couple of children issues and move the related patches over there.

plach’s picture

Small fix to address some edge-cases related to UUID param conversion.

Be sure to rebuild caches after applying the patch.

franmako’s picture

Category: Plan » Bug report
Priority: Major » Normal

I tried the latest patch at #126 on a project that I'm working on and it seems that the included fields that I've added to the default include list via the jsonapi_extras do not work, when the jsonapi_translation module is enabled.

gabesullice’s picture

Category: Bug report » Plan
Priority: Normal » Major

Thanks for reporting a problem @franmako! I think the issue metadata can stay the same though.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

bbrala’s picture

Status: Needs review » Needs work

It seems the patch doesn't apply anymore. Also #123 has a lot of feedback and i think that feedback has not been applied, so i'm setting this back to needs work.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

bbrala’s picture

bbrala’s picture

Issue summary: View changes

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

b.khouy’s picture

This is a little bit confused, the final question still on the table: How to update an existing entity translation using Json:API PATCH method?
What headers/meta/attributes... should developers use in their PATCH request to determine the concerned translation after applying #126 patch?
I think a clear documentation is also needed for this topic.
Thanks guys

pozi’s picture

I am new to the topic and was able to add new translation by commenting out those lines from #126 patch:

    if ($resource_langcode !== $parsed_langcode) {
      $message = 'Translation resource language mismatch: "%s" (request metadata) vs "%s" (request payload).';
      throw new UnprocessableEntityHttpException(sprintf($message, $resource_langcode, $parsed_langcode));
    }

@b.khouy you can add transaltion either by using queryParam "lang_code" or by setting POST request "Content-Language" header, of course both set to translation destination language. Sending PATCH request to whatever entity endpoint language modifies default language.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

sam.foster’s picture

Hey guys,

What is the current state of play for this issue? We have Drupal 8.9.2 and are using JSON:API to create English nodes, but we need to create translations of those nodes into Welsh. The Content Type is correctly set up to allow translation, and we can of course create a translation with the GUI, but is this now possible using the JSON:API module or via some other means?

Can any of these patches here be successfully applied to deliver such functionality? I've tried patching in the drupal/core section of my composer.json but the patch won't apply. Is that because it is now in the core or is it because the patches no longer work with the current version I have as part of Drupal 8.9.2?

Any pointers will be greatly appreciated

Cheers

Sam

sam.foster’s picture

Guys

Any update at all - can REST API actually create translated nodes and establish the relationship between the original node and the translated one?

If not are there any work arounds?

Surely someone out there knows something?

Thanks

Sam

bradjones1’s picture

Hi Sam - This is not an area I'm actively working on, yet I can speak to your repeated requests for an update. Drupal is maintained by open-source contributors and there's no dedicated paid developers working on JSON:API specifically. You mention "REST API" which would be something different from JSON:API module, but i'm guessing you're talking about the items in this issue. There is some work on this already, as you can see on this issue... but it's waiting on people (maybe people like you?) to help finish it up.

I can say that if there are ever updates, you'll see them here. If this is important to your use case, you can help us get this done, or I'm sure there are developers experienced in this area who would be open to doing sponsored work.

bbrala’s picture

A little note about this we need to take into consideration. If we add a query parameter, the JSON:API specification has some rules around them. They need to be a valid member name, but also contain a special character. See:

https://jsonapi.org/format/#query-parameters-custom

bradjones1’s picture

Per #143 My read is that it must contain at least one non a-z character, but a capital letter suffices.

It is RECOMMENDED that a capital letter (e.g. camelCasing) be used to satisfy the above requirement.

So not much of a strong requirement but something we should enforce.

bbrala’s picture

Wow you're right. Yay! :)

plach’s picture

@bbrala @bradjones1

I resumed work on this today at the DrupalCon Lille sprint. Regarding #143 - #144, the latest code uses a langCode parameter. That looks compliant, doesn't it?

bbrala’s picture

yeah langCode is fine.

Would've loved to be able to sit with you, but the fact this week is the kids vacation made that too hard :(

plach’s picture

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.