Issue #3616524: Translate the resource name, the slot title and the tariff label a booker reads

Makes the three entity types carrying a label a booker reads translatable, reads those labels in the visitor's language on every surface that shows them, and gives a house somewhere to author the translations.

What is translatable now

yoyaku_resource, yoyaku_slot and yoyaku_resource_tariff gain a data table and translatable: TRUE, and their label field alone is marked translatable. Everything else on those rows stays language-neutral: the times, the capacities and quotas, the machine keys, the weights, the references, the two flags a slot is published by. A house editing a quota in French means it to hold in English too.

yoyaku_slot_tariff is deliberately not among them, though the issue summary listed it. It stores no authored prose at all: its fields are the slot, the tariff, the allotment and the quota. A data table on it would be a row per language of values nothing reads back in one, on the hottest read path there is. What a booker reads on a slot tariff is the resource tariff it provisions, and that is translatable.

Translatable was only half of it

Two things turned up that the issue summary did not know, and both had to be part of this or the change would deliver nothing a booker can see.

Nothing read these labels in the visitor's language. Only four call sites in the whole module went through entity.repository, all of them in the placement map. So the types that were already translatable had the same defect: a translated section, grade, tariff class or allotment label never reached the cart, the ticket or the offer card. Those are fixed in the same pass.

No operator could author a translation of any yoyaku entity. Core hangs its translation overview, its add and edit forms and its Translate tab off an entity type's canonical link template, and the yoyaku entities are headless and have none. So translatable: TRUE was data-model-only for all seven types that carried it. A canonical link is now added on the edit form, which is the screen these entities do have, the translation screens are pointed under it, and the route provider is swapped for one that emits the canonical route. Core's menu_link_content, the one entity type of its own in the same position, does exactly this. Nothing happens unless content_translation is installed.

What breaks when a type gains a data table, and what does not

The base table keeps only id, uuid and langcode; every other column moves, translatable or not. Two consequences, one of them not the one the summary expected.

Counts do not double. Core marks any query over a type with a data table non-simple, so ->count(), ->range() and ->pager() all get a GROUP BY base_table.id and a plain fetch is keyed by id. The availability-count risk the summary named is not the mechanism.

Raw SQL naming a moved column is a hard failure. yoyaku_manager scoped its access-query alters with base_table.resource, which is an SQL error on every access-checked slot and tariff query: the slot overview, the resource-slots tab, bulk slot update, the tariff list and the tariff-order screen. It now resolves the column's real table. ResourceAccessTest catches it, and was seen to fail with no such column: base_table.resource before the fix.

Conditions and sorts quietly mean "any translation". An unqualified join to the data table carries no langcode, so a slot published in one language reads as published and a list sorted on a label is ordered by whichever row the database met first while the screen shows another. Every entity query over the three types now adds ->condition('default_langcode', 1); the display language is applied where the entity is rendered, never in the query.

Caches

The month availability feed cached the slot titles and the tariff labels under a key with no language in it, so the first visitor's language was served to the next. Fixed, and pinned by a test. The placement map's two halves were already keyed per language. The picker page bakes the slot title into drupalSettings and now declares the content-language cache context. Nothing denormalizes a label onto a booking, a transaction or a frozen fact, so there is no stored-string migration.

What it costs

Measured on the same machine minutes apart, canonical 1.x against this branch, and written into docs/performance.md under its own dated heading.

Every figure in the register is unchanged, all twenty-six of them, including the pricing scenarios the summary expected to move. A query that reads one of these types now joins or re-reads a data table, and a join is still one query.

What moved is the cost of loading one of these entities cold: a resource 3 to 4 reads, a slot 1 to 2, a tariff 2 to 3. One extra read each, being core's second read of the data table, paid once per distinct entity rather than once per line or per order. The sweep of twenty orders sharing one slot went from 32.00 to 32.05 queries per order, which is that one read spread over the fleet, and its ceiling carries it.

That reads as nothing in the register only because the register measures a warm static cache: every one of those tests builds its fixture in the same request. A real booking request loads the slot and the tariff cold, so two extra queries per hold is what this costs where it is actually paid. What the register does answer is whether the cost grows with the party, the lines or the places, and it does not: the extra reads are per entity, so a party of twelve pays what a party of one pays. The load run over HTTP, the other half of the agreed gate, has not been done: it needs the branch deployed to the local site and its entity schemas reinstalled, which would purge the demo data there.

Tests

Each new assertion was run against the unfixed code and seen to fail there first.

  • EntityLabelTest — which types are translatable and which is not, a translated label leaving the rest of the row alone, the composed slot title naming its resource in the slot's own language, and a second slot at the same resource and start still refused once the first has a translation.
  • TranslationScreensTest — the whole authoring chain: core offers the types on the content language screen, every translation route exists, and the edit screen carries the Translate tab beside an Edit tab. Every link in that chain fails silently on its own.
  • TransactionSummaryTest — the summary names the event and the tariff in the language asked for, which is what a notification needs: mail is built from the queue that sweeps the order, whose request language is the sweep's and not the booker's.
  • AvailabilityFeedTest — a month is held once per language and warming one does not overwrite the other.
  • ResourceAccessTest — the manager scoping over the three types plus the slot tariff, with translations present, and a count that counts entities rather than translation rows.
  • AllotmentStorageInstallTest — installing the module creates the three data tables.

The whole kernel suite (217 classes) is green locally on SQLite and the classes touching raw SQL are green on MySQL.

Docs and translations

A new docs/translation.md says what a house translates and what it does not, how to switch it on, how a translation reaches a booker, and how to write a query over a translated type. docs/concepts.md says it per entity, and the InterfaceLanguageUrlTrait rule in docs/architecture.md argued from a premise that has moved, so it is reworded. French is in the same commit.

Pre-1.0, so no update hooks: a site carrying data from before this reinstalls, and the migration note says so.

Edited by Frank Mably

Merge request reports

Loading
Loading