Translations of referenced entities.

Created on 18 September 2023, over 1 year ago
Updated 6 November 2023, about 1 year ago

Problem/Motivation

Entities referenced from Entity Reference fields are always in default language.

Steps to reproduce

Have translation enabled for your content type.
Have Entity Reference field.
Query a node translation and include entity reference field in field list.

Expected result: query returns translations of the same language as parent node for referenced nodes.
Actual result: referenced nodes are in default language.

My use case example:

route(path: "/it/node/1", langcode: "it") {
    ... on RouteInternal {
      __typename
      entity {
        ... on NodeGlossary {
          id
          title
          langcode {
            id
          }
          crossReferences {
            ... on NodeGlossary {
              id
              title
              langcode {
                id
              }
            }
          }
        }
      }
    }
  }

Result:

{
  "data": {
    "route": {
      "__typename": "RouteInternal",
      "entity": {
        "id": "4c7536d9-74ff-4686-bed3-d2cf150fe1e0",
        "title": "glos1 IT",
        "langcode": {
          "id": "it"
        },
        "crossReferences": [
          {
            "id": "8116608b-426a-420f-90ea-6b35f94c486d",
            "title": "glos2",
            "langcode": {
              "id": "de"
            }
          },
          {
            "id": "69739b9c-c9d5-41cb-a65e-dedd4215752a",
            "title": "glos3",
            "langcode": {
              "id": "de"
            }
          }
        ]
      }
    }
  }
}

Expected result:

{
  "data": {
    "route": {
      "__typename": "RouteInternal",
      "entity": {
        "id": "4c7536d9-74ff-4686-bed3-d2cf150fe1e0",
        "title": "glos1 IT",
        "langcode": {
          "id": "it"
        },
        "crossReferences": [
          {
            "id": "8116608b-426a-420f-90ea-6b35f94c486d",
            "title": "glos2 IT",
            "langcode": {
              "id": "it"
            }
          },
          {
            "id": "69739b9c-c9d5-41cb-a65e-dedd4215752a",
            "title": "glos3 IT",
            "langcode": {
              "id": "it"
            }
          }
        ]
      }
    }
  }
}
πŸ› Bug report
Status

Closed: works as designed

Version

2.0

Component

Code

Created by

πŸ‡§πŸ‡ΎBelarus Yury N

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @Yury N
  • πŸ‡§πŸ‡ΎBelarus Yury N

    Not sure if it is correct way to do it. But it worked for out case.

  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    Thanks heaps for this! Get the ball rolling.

    I'll review and test this over the coming days.

    I'd also like to explore alternatives with `referencedEntities` instead of using `field values[]->entity`
    And also look into `EntityRepository::getTranslationFromContext` before committing.

    This thread seems to have all the ideas nutted out. https://www.drupal.org/project/drupal/issues/2915972 β†’

  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    Looking into this gave me some good restructuring opportunity.

    Entity references were loading in a foreach, which isn't great for the database, but also they didn't have a GraphQL module entity buffer, which isn't great if re-using entities across large queries.

    So, this commit will be a bit gnarly, but heres what I landed on:

    Replace the data producer for entity references with:

          $builder->produce('entity_reference')
            ->map('field', $builder->fromValue($this->getFieldName()))
            ->map('entity', $builder->fromParent())
            ->map('language', $builder->callback(
              fn (EntityInterface $entity) => ($entity instanceof TranslatableInterface)
                ? $entity->language()->getId()
                : NULL
            )),
    

    The Drupal\graphql\Plugin\GraphQL\DataProducer\Field\EntityReferenceTrait used in the entity_reference data producer has:

          $entities = $this->getTranslated($entities, $language);
    

    And that in turn runs

        return array_map(function (EntityInterface $entity) use ($language) {
          if ($language !== $entity->language()->getId() && $entity instanceof TranslatableInterface && $entity->hasTranslation($language)) {
            $entity = $entity->getTranslation($language);
          }
          $entity->addCacheContexts(["static:language:{$language}"]);
          return $entity;
        }, $entities);
    

    Which shooouuuulllllld match up to what we technically want in your patch.
    And it should also give us some added benefits of cache, buffer loading and access controls.

    And now I don't have to maintain it, win win.

    I'll run some tests and post back here once its in dev.

  • πŸ‡§πŸ‡ΎBelarus Yury N

    Glad to hear that you found different solution. I have realized that my patch creates situations when entity referenced from required field does not have translation, which results in "Cannot return null for non-nullable field" errors

  • Status changed to Needs review over 1 year ago
  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    Changes on dev

    https://git.drupalcode.org/project/graphql_compose/-/commit/bfec317fefee...

    It's in there... somewhere.
    Mostly around the Entity Reference field types.

    Let me know how you go with your testing please :)

  • πŸ‡§πŸ‡ΎBelarus Yury N

    Did not had time for deeper testing, but it seems to work well. Thank you for your work!
    Just one thing: now it returns original translation if requested language translation does not exist. It would be nice to have some control over this behavior, like return only requested translations or return translations and fallback to original language if no translation.

  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    Mmm i think at that point if you wanted to control exact same translation, and remove it if its not a match, you’re changing the data in the field.

    The way its working now is how routes and edges should work.

    Perhaps thats more of a hook on the results to array filter to your applications needs?

    What does drupal core do?

  • Status changed to Closed: works as designed about 1 year ago
  • πŸ‡¦πŸ‡ΊAustralia almunnings Melbourne, πŸ‡¦πŸ‡Ί

    Outcomes pushed to 2.0.0
    Thanks!

Production build 0.71.5 2024