Problem/Motivation
A /related/ read honours langCode when translating the resources it returns, but not when deciding which resources those are. It reads the relationship field off the host entity's default translation, so on a translatable entity reference field a client asking for French gets the English translation's targets.
The cause is a gap in this module's own fork. Core's EntityUuidConverter upcasts the routed entity through EntityRepository::getTranslationFromContext(), so a core controller receives an already-translated entity. This module deliberately removes that upcast, because it does its own request-driven language selection instead of relying on Drupal's language negotiation. EntityResource::getIndividual() compensates by resolving the requested translation itself. EntityResource::getRelated() never did: it calls $entity->get($field) on whatever the param converter produced, which is always the default translation.
The result is two endpoints disagreeing about the same data. These two requests read the same relationship, in the same language, and return different targets:
GET /jsonapi/node/article/{uuid}?include=field_related&langCode=fr→ the French targets, because core'sgetIncludes()walks from the translationgetIndividual()resolved.GET /jsonapi/node/article/{uuid}/field_related?langCode=fr→ the English targets.
This only bites where a reference field is translatable and its translations hold different values, which is an ordinary setup: "related articles" or a promoted-content field curated per language. Where every translation shares one value, the two paths agree and nothing is visibly wrong.
Steps to reproduce
- Site with
en(default) andfr, article translation enabled. - A translatable to-many
entity_referencefield on article, e.g.field_rel_multi. - Two target articles,
AandB, each with anenand anfrtranslation, so neither can be filtered out for language. - A host article whose
entranslation referencesAand whosefrtranslation referencesB. GET /jsonapi/node/article/{host-uuid}/field_rel_multi?langCode=fr- Observed:
A, the English translation's target. Expected:B.
Captured verbatim from the characterization test written before the fix:
Failed asserting that two arrays are identical. --- Expected +++ Actual @@ @@ Array &0 [ - 0 => '48932953-e847-43cd-8714-925386b5d91c' (the fr-referenced target) + 0 => '2df3ed4c-3187-45ef-a6bf-c144b3b52148' (the en-referenced target) ]
Both targets existing in both languages matters for the reproduction: it removes the existing per-target language filtering as an explanation, leaving the host translation as the only variable.
Proposed resolution
Resolve the host's translation before reading the relationship, exactly as getIndividual() already does.
getRelated()resolves the language selector first, then normalises the host withgetUntranslated()and, when the host has the requested translation, reads the field from it. The$field_listassignment moves below that block.- In fallback mode a host lacking the requested translation is resolved through
resolveViewableFallbackTranslation(), the same access-aware candidate walk an individual fallback read uses. The access results it consults are threaded onto the response's cacheability, so a cached body varies by the requesting user's access outcome rather than by URL alone. - The existing per-target language filtering, the to-one strict
404, and themeta.omittedreporting for excluded to-many targets are untouched. This changes which translation supplies the field, nothing about what happens to the targets afterwards.
Issue fork jsonapi_multilingual-3614409
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #4
lauriii