Issue #3613327: Admin list links built with toUrl() drop the language prefix, sending a French page back to English
On a site with URL prefixes, an administrator working in French at /fr/admin/booking/orders clicked an order and landed on /admin/booking/orders/12, dropped back into English mid-task, while the operation links on the same row stayed French.
Why
EntityBase::toUrl() pins every relation except collection, add-page and add-form to the language the entity carries. That is the right rule for translatable content with a page per language. It is the wrong rule here: these entities have no page per language, their links are admin screens, and their langcode is only whatever language happened to create the record (the site default, which has no prefix). Operations escaped because they are built from a route, and a route never pins a language.
Reproduced outside the browser, with a /fr request pushed onto the stack:
interface=fr entity=en
toUrl('canonical') /adm/booking/orders/260 <- prefix dropped
toUrl(canonical, language: fr) /fr/adm/booking/orders/260
Url::fromRoute (operations) /fr/adm/booking/orders/260The fix
InterfaceLanguageUrlTrait, used by all 15 Yoyaku entity classes: toUrl() sets the link's language to the current interface language, so a link states the language it is actually for, the one the page around it is being shown in. A caller that means a specific language passes ['language' => $language] and still wins, which keeps this a default rather than a rule.
This is stated positively on purpose. Passing ['language' => NULL] at each call site also works, by leaning on the += in core and on NULL meaning "unspecified", but it expresses the fix as unsetting something rather than as what we want, and it leaves the next admin list free to get it wrong.
Why on the entity rather than at the call sites
The 17 affected toUrl() calls in the admin UI are not the whole bug. Nine list builders inherit EntityListBuilder::getDefaultOperations(), which builds Edit and Delete with $entity->toUrl() inside core, out of reach of a call-site fix. Putting it on the entity covers those, covers every link written from now on, and needs no churn in controllers and forms. toUrl() is the entity's own API for turning a relation into a Url: the UI modules decide where a link points (they own the link templates, added from hook_entity_type_alter()), the entity decides how the Url is built.
Tests
A kernel test pins how the link is built: French keeps the prefix on edit-form and delete-form, English stays unprefixed (it follows the page rather than being pinned the other way), an explicit caller language still wins, and collection is unaffected. A functional test pins what an administrator actually gets, over HTTP, from the resources list in both languages.
Both bite: with the trait removed from one entity, the functional test fails on /admin/booking/resource/1/edit?destination=/fr/admin/booking/resource, which is one of the core list-builder operations described above.
Green: all 19 yoyaku kernel directories, the new functional test, phpcs, phpstan and cspell.
Notes
A page rendering these links varies by interface language and needs the languages:language_interface cache context if it is cached; the design rules now say so.
Three placement config entities are config-translatable, so "these entities are not translatable" is not uniformly true, but the conclusion holds: an administrator editing configuration in French should stay in French, and config translation targets the interface language anyway.
Worth a core issue separately: EntityBase pinning entity language for entities that have no per-language canonical is a general wart, and this trait is the local interim.