Mark the language of every value a page renders - #91
Merged
Conversation
…r language. A representation negotiated by language needs Accept-Language in its Vary and in its entity tag, or a shared cache hands one reader the other's rendering and a conditional request answers 304 across languages. Both fixes are in the Core snapshot; the release this depended on predates them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…hat was asked for. Asking for a language does not make the page that language. The root lang came from the reader's top preference with no reference to the content, so Accept-Language: de produced lang="de" over a page whose every word was English, and es produced lang="es" over the same page. That is a WCAG 3.1.1 failure generated from a request header. So the two facts are separated. $ac:langs stays the reader's ordered preference and keeps driving collation and label selection; $ac:contentLang is what the representation was composed in, and only that reaches <html lang>. A writer supplies it as the highest-ranked accepted language whose translations it actually ships. It defaults to en rather than to $ac:lang. The rule is "the first accepted language the UI provides, else en", and Web-Client's own strings are English only, so for a client supplying no value the negotiation can only ever answer en. Defaulting to $ac:lang would return a value the rule cannot produce, and would leave the defect as the out-of-the-box behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…ge it is in. A cell rendered the value matching the reader's language, or the first in document order when none matched, and discarded the rest. So a reader whose language the data lacks was shown an arbitrary one, every other reader was shown a single value with no sign the others existed, and neither could tell which language they were looking at. The header fixes the column count, so the values share one cell rather than each claiming a column: the first value of a property opens it and its siblings are folded in, ordered by ac:lang-rank so the reader's language leads and the rest follow instead of disappearing. Each carries its own lang, since a cell now holds several at once and the document's language is wrong for all but one of them. An untagged literal gets lang="", HTML's spelling of the absent RDF tag; a typed value is not prose and inherits, so a number or a date is read out in whatever language the reader is in. Object links get the same treatment from the other end: the anchor already applies the label mode itself, so it can read the tag off the literal that mode chose without any caller passing it one. A label computed from a URI has no literal behind it and inherits, which is what you want for a fragment or a local name. The ldt:lang templates go with them. They were the same suppression, commented out, over a parameter that was never declared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…owser answer it differently without declaring it twice. $ac:langs was a parameter with a default in Web-Client and an override in LinkedDataHub's client.xsl, because the browser's list can only be read where ixsl is allowed. Two declarations of one value is the shape that lets them drift, and they had: the client override expressed a normalisation the server default did not, so the same page ranked languages differently depending on which engine drew it. ac:langs() replaces it, with the same use-when split the browser-dependent functions already use - except that the two bodies live on opposite sides of the repository boundary, since Web-Client carries no ixsl references. This body reads the parameter the writer supplies and exists only under SAXON; the SaxonJS one lives in LinkedDataHub. Exactly one is present in any compilation, which is also what lets this one keep reading a parameter that does not exist in the client-side stylesheet at all. Normalising inside the function is what makes the two engines agree: primary subtags, because fn:lang() prefix-matches and 'es' therefore reaches a label tagged es-ES while 'es-ES' would not reach one tagged es; deduped, because a browser sending es-ES,es yields that subtag twice; and 'en' when nothing was expressed, which is the floor the server-side negotiation already applied. $ac:lang goes with it. Nothing ever supplied it - there is no vocabulary term and no writer passes one - so it was a derived alias for the first accepted language, declared once per engine and carrying the same drift risk. Its 36 call sites say ac:langs()[1] now, which is what the value always was: the collation sites read as "sort in the reader's top preference", and the date formatting keeps the language argument it needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…ry other label already does. The anchor for an object resource wrapped its label in xsl:value-of, which atomizes the literal to a string and throws the language tag away before anything downstream can read it. So a Lithuanian reader saw "Gatvių tipai" on a page composed in English with nothing saying which language it was in, and a German reader - whose language the data does not have - saw a fallback label in a third language, equally unmarked. Binding the label as nodes keeps the literal, and its @xml:lang goes on the anchor. A label built as a computed string, from a fragment identifier or a decoded path segment, has no literal behind it and inherits, which is what you want for those. This is the same idiom the subject anchors in xhtml:Anchor mode already use. They were marked earlier and this template was missed: it renders the links inside a property value rather than the subject heading, so it never went through that mode at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
Folding every value of a property into the one cell its header allots stopped the cell from hiding values, but left them wrapped in bare spans with nothing between them: eight order items in an OrderedItem cell read as one unbroken run of UUIDs, wrapping mid-token, with no way to tell where one link ended and the next began. So the cell lays them out rather than merely emitting them. Each value is a block of its own, and the blocks live in a container the cell holds, because a td cannot bound its own height and a property with fifty values must not set the row height for the whole table - the skin caps the container and the surplus scrolls, with every value still in the DOM. The layout is a mode, xhtml:TableDataCellValue, so a skin overrides a cell's values the way it already overrides a property list's. It matches the statement, not the nodes beneath it: an XHTML literal is one value however many elements it is written with, and the xsl:choose over the value's language becomes two templates, which is where that choice belongs. The language badge was written inline in the property list and would have been written a second time here. It moves to an ac:lang-tag mode over @xml:lang, applied from wherever values are laid out, so the two places cannot drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YxhcqMPK1kZR1pxxJqoge
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #89/#90. Those made label lookup negotiate over the reader's ordered
language preferences; this branch makes the rendered page say which language it
ended up in, and stops the negotiation from silently discarding the values it did
not pick.
What changes
$ac:langsis a function, not a parameter.ac:langs()is declared once infunctions.xsland answered differently by Saxon and Saxon-JS viause-when, sothe server reads
Accept-Languageand the browser readsnavigator.languageswithout either side re-declaring the state.
$ldt:langand itsLDT.javaterm gowith it — the last of the language state that lived in two places.
The page declares the language it is written in, rather than the one that was
asked for, and publishes the shortest tag the bundle justifies rather than the key
it happens to be stored under.
Every rendered value carries its own language. A label chosen from one of
several languages is marked with the tag it was chosen in — on the anchor and the
span that emit it, read off the literal
ac:labelmode picked, with no callerpassing it in. A label computed from a URI has no literal behind it and inherits.
An untagged literal gets
lang="", HTML's spelling of RDF's absent tag; a typedvalue is not prose and inherits, so a number or a date is read out in whatever
language the reader is in. This is WCAG 3.1.2, and it keeps the extracted RDFa
faithful.
A table cell holds every value its property has. A cell used to render the one
value matching the reader's language, or the first in document order when none
matched, and drop the rest — so a reader whose language the data lacks saw an
arbitrary value and everyone else saw one value with no sign the others existed.
The header fixes the column count, so the values now share the one cell the first
one opens, ordered by
ac:lang-rank().…and lays them out instead of merely emitting them. The first cut wrapped each
value in a bare span with nothing between them, which reads fine for a title in
three languages and not at all for eight object references: they ran together into
one unbroken line, wrapping mid-token. Each value is now a block of its own inside
a container the cell holds — a
tdcannot bound its own height, so the containeris what a skin caps, and a property with fifty values scrolls rather than setting
the row height for the whole table. Nothing is dropped from the DOM.
The layout is a mode,
xhtml:TableDataCellValue, so a skin overrides a cell'svalues the way it already overrides a property list's. It matches the statement
rather than the nodes beneath it, so an XHTML literal stays one value however many
elements it is written with.
The language badge is written once. It existed inline in the property list and
was about to be copied into the table cell; it moves to an
ac:lang-tagmode over@xml:lang, applied from wherever values are laid out, so the two cannot drift.The generic layer answers it with nothing, the Bootstrap layer with its label, and
a downstream skin with whatever it uses.
Verification
No automated coverage for rendering; checked by eye against LinkedDataHub's
Northwind Traders demo — a container table with a high-arity object property, a
resource description with multi-language titles, under both the server render and
the Saxon-JS one, which share these stylesheets.
🤖 Generated with Claude Code
https://claude.ai/code/session_016YxhcqMPK1kZR1pxxJqoge