Problem/Motivation
This module's central promise is strict language selection: a language the client did not select or accept is never served. Primary data honours it everywhere: an individual read misses with 404/406, a collection excludes items with no acceptable translation, and a /related/ read omits and reports excluded targets (#3612537).
The included member breaks the promise. Core's IncludeResolver resolves every ?include= target through the swapped EntityAccessChecker::getAccessCheckedResourceObject(), and its getEntityTranslation() returned the untranslated entity when the selected language has no translation. The in-code justification ("a harmless no-op") reasons only about individuals, which the controller already resolved, and collection items, which were excluded at query time. It is false for includes, which have no earlier guard: an included resource with no translation in an acceptable language is silently emitted in its default language.
So GET /jsonapi/node/article/{uuid}?include=field_ref&langCode=fr returns the primary node strictly in fr, next to an included resource in en. The same happens under Accept-Language, and for config entities reached through include. The client cannot tell a translated resource from a fallback, and the behaviour is an undocumented divergence from the rest of the module.
Steps to reproduce
- Languages
en(default) andfr. Article A hasenandfrtranslations; article B has onlyen. A references B via an entity-reference fieldfield_ref. GET /jsonapi/node/article/{A}?include=field_ref&langCode=fr. Expected: noenresource anywhere in the response. Actual: primary data isfr, butincludedcarries B inen.- Same URL with header
Accept-Language: frinstead oflangCode: same wrong result.
Proposed resolution
Omit and report, mirroring the related endpoint: an included resource with no translation in any acceptable language is omitted from included and reported under meta.omitted (core's standard omission channel, already used for inaccessible includes), with a link to the resource and a per-item detail: "No translation of the included resource matches the request's language selector. Available translations: ...". The client can distinguish "omitted for language" from "absent" and re-fetch with another selector.
This completes one consistent mental model: a primary to-one miss hard-fails (404/406); list-shaped members (collections, to-many related targets, included) exclude and report.
- The implementation stays in the access checker seam:
getEntityTranslation()signals a strict miss (alangCodethe entity has no translation of, or a failed strictAccept-Languagenegotiation), andgetAccessCheckedResourceObject()converts the miss into the sameEntityAccessDeniedHttpException-shaped omission the related endpoint builds. Core'sIncludeResolverand the normalizers rendermeta.omittedwith no further changes, and any future caller reaching the checker with an unresolvable language gets the strict behaviour by default. - Config entities follow the same rule, with availability defined by their base language plus configured overrides.
Content-Languagekeeps its fallback: it accompanies writes and may target a translation that is being created.- Cacheability: the omission entry carries the omitted entity's cacheability, so the report is invalidated when the entity gains the missing translation, plus the language cache contexts.
- Reads without a language selector keep core's include behaviour unchanged.
- Alternatives rejected: documenting the silent fallback as a divergence (contradicts the module's promise and makes
includedsemantically unreliable), and failing the whole request on any include miss (disproportionate; mirrors neither collections nor related). - The behaviour carries a
DIVERGENCE(core):marker: core #3199697 does not translate includes at all; propose upstream or drop at handoff.
Issue fork jsonapi_multilingual-3613728
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 #3
lauriiiComment #5
lauriii