Skip to content

Every string the editor shows comes from a catalog a host can replace - #32

Merged
namedgraph merged 6 commits into
masterfrom
localisation
Sep 20, 2026
Merged

namedgraph merged 6 commits into
masterfrom
localisation

Conversation

@namedgraph

Copy link
Copy Markdown
Member

The editor hardcoded its English: 159 sites across eight modules — dialog labels and buttons, the toolbar, the slash menu, the breadcrumb and inspector, confirm() prompts, titles set through ixsl:set-attribute. A host that wants the editor in another language has nowhere to put a translation today; overriding a whole dialog to change one word forks markup it would rather inherit, and editing a vendored copy of src/ is worse, since the next copy silently reverts it.

The seam

  • translations.rdf — 112 keys, English. Plain RDF/XML: rdf:Description keyed by rdf:nodeID, carrying rdfs:label per language. No manifest format of its own, and the shape a host is likely to keep its own strings in already.
  • src/translations.xsl$translations-href, $translations-lang, a resources key, rdfae:translations() and rdfae:label().
  • 159 call sites rewritten to rdfae:label('key'). Keys name the concept, not the English, so a catalog can be written against them without reading the templates.

A host translates the editor by overriding rdfae:translations() to return its own catalog, or by pointing $translations-href elsewhere. Overriding rdfae:label() too replaces the language selection, for a host that resolves it from something richer than a parameter.

Two details worth a look

rdfae:translations() is a use-when pair, not a runtime branch. Under SaxonJS the catalog resolves against the page URI the way $vocab-hrefs does, so doc() hits the preloaded pool; under Saxon there is no page and static-base-uri() reaches the file the headless drivers read. A runtime branch can't span that — ixsl:location() doesn't exist to compile against outside SaxonJS.

The catalog has to be preloaded by the host page, exactly like the vocabularies. index.html and the four tests/fixture*.html pages now do; generate-sef.sh copies it into dist/. This is the one thing an embedder must remember.

Two host-facing fixes travelling with it

  • Dialog labels now carry @for, their controls @id. The labels were bare and pointed at nothing, so clicking one focused nothing.
  • drag-handlerdfa-editor-drag-handle. drag-handle is a token an embedding application is likely to have styled already; when it has, its own rule can hide the editor's handle.

Tests

Headless suite 52 PASS. Browser suites exit 0, 622 assertions true. The two tolerated false results (noId, noMsoClass) are exactly the two produced by master — baselined by stashing this branch, rebuilding the SEF and re-running.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd

namedgraph and others added 6 commits September 11, 2026 15:12
Nine reads reached through ixsl:call/ixsl:get for facts the data model
already exposes, so the module asked JavaScript a question XPath answers.
isSameNode($a, $b) is `$a is $b`; parentNode is `..`; parentElement, which
is null when the parent is not an element, is exactly `parent::*`; and a
Range container's nodeType = 3 test is `instance of text()`. The event
targets that carried two of the identity tests take `as="node()?"` so the
comparison has a node to compare - the same typing relatedTarget already
had in navigate.xsl, and the reason `is` was reachable there and not here.

Two constructions follow: Object() through ixsl:call is ixsl:new('Object',
[]), the extension that exists for it; and typeahead's three-line scroll
options object - allocate, set `block`, pass - is one XDM map literal, the
form navigate.xsl was already passing to the same scrollIntoView on the
line above its own call.

Nothing moves in the DOM layer proper. Building detached subtrees through
rdfae:element and splicing them with insertNode/appendChild/before stays:
xsl:result-document emits at a location and returns nothing to hold, and
the insertion model hands the constructed node onward to be spliced, to
receive the caret and to seed a Range. That is an architecture, not a
workaround, and converting it would be a redesign of the editing core.

make sef compiles both entries; make test passes 52; make test-browser
reports 532 assertions true with the two pre-existing canonicalSource
paste-probe values unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
The chrome hardcoded its English. 159 sites across eight modules - dialog
labels and buttons, the toolbar, the slash menu, the breadcrumb and
inspector, confirm() prompts, the titles set through ixsl:set-attribute -
each one a literal in the template that rendered it. A host wanting the
editor in its own language had nowhere to put a translation: overriding a
whole dialog to change a word forks markup it would rather inherit, and
editing a vendored copy of the source is worse, because the next copy
silently reverts it.

So the strings move to translations.rdf and the templates read them through
rdfae:label(). The catalog is plain RDF/XML - rdf:Description keyed by
rdf:nodeID, carrying rdfs:label per language - which needs no manifest
format of its own and is the shape a host is likely to keep its own strings
in already. A host translates the editor by overriding rdfae:translations()
to return its catalog, or by pointing $translations-href at another
document; overriding rdfae:label() as well replaces the language selection
for a host that resolves it from something richer than $translations-lang.
The keys are named after the concept rather than the English, so a catalog
can be written against them without reading the templates.

rdfae:translations() is two declarations under use-when rather than one with
a branch: in SaxonJS the catalog resolves against the page URI the way the
vocabularies do, so doc() hits the preloaded pool, and under Saxon there is
no page and static-base-uri() reaches the file the test drivers read. A
runtime branch cannot span that, because ixsl:location() does not exist to
compile against outside SaxonJS. Like the vocabularies, the catalog has to
be preloaded by the host page - index.html and the four test fixtures now
do, and generate-sef.sh copies it into dist/.

Two things a host should not have to patch travel with it. Every dialog
label now carries @for and its control an @id, so clicking the label focuses
the field it names - the labels were bare, pointing at nothing. And the
block drag handle's class becomes rdfa-editor-drag-handle: `drag-handle` is
a token a host is likely to have styled already, and when it has, its own
rule hides the editor's handle.

The 52 headless assertions and the browser suites pass unchanged, the two
tolerated false results (noId, noMsoClass) being exactly the two on master.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
The catalog lookup was split under use-when - doc() against ixsl:location()
in the browser, document() against static-base-uri() under Saxon - on the
assumption that a relative href cannot mean the same thing in both. It can,
and the build already says so: the SEF is compiled with -relocate:on, which
resolves relative document() hrefs against the SEF's load location. So a
bare document($translations-href) finds the catalog beside the stylesheet
in either product, and the module drops its ixsl: dependency along with the
second declaration.

The catalog moves to src/, where the stylesheet that reads it lives, and
generate-sef.sh copies it into dist/ as it does the vocabularies. Host pages
pool it under the URI that resolution actually asks for - beside the SEF,
not beside the page, which is where the vocabularies sit. Verified by
pooling it under both URIs, then removing the page-relative one: the browser
suites stay green, so the SEF-relative URI is the one being requested.

Its DTD entity declarations go too. xslt3 does not expand entity references
in a document() target, so the headless path failed with FODC0002 on &rdf;
the moment anything called rdfae:label() - invisible until now only because
the test drivers import the content model and the linters, never the chrome.
The namespaces are written out instead, and a scratch stylesheet under
Saxon-HE now answers table-columns, drag-to-reorder and an unknown key
(which returns the key) from a 112-entry catalog.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
LinkedDataHub vendors src/ and had patched all three in place, where the
next copy would have silently reverted them. None is host-specific.

rdfae:populate-form disclosed the advanced fields inline, so a host whose
overlay groups type, subject and object differently - behind tabs, or a
segmented object switch - had to override the whole prefill to change that
one step. It becomes rdfae:reveal-fields, the single shape-dependent part of
populate-form, overridable on its own while the rest of the prefill stays
inherited.

The annotation handler called populate-form without checking the overlay is
still there. A host that re-renders the page DOM drops it, and populate-form
reaches the form by id(): over an empty sequence it does nothing, silently,
and the dialog opens blank. rdfae:show-overlay rebuilds the overlay too, but
it runs after populate, which is exactly too late. The guard goes in front
of the call; show-overlay keeps its own as the backstop for callers that
arrive without passing through the handler.

And the subject readout is a single line that may be narrower than the IRI
it shows, with nothing offering the rest. @title is now written wherever
textContent is - in show-overlay and on the subject input's own handler - so
the two cannot come to say different things.

52 headless assertions and the browser suites pass unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
$translations-lang defaulted to 'en', so a catalog with Spanish in it still
rendered English until a host told the editor otherwise - and every host
would tell it the same thing, since the preference has one source: the
browser. rdfae:langs() asks navigator.languages directly, reducing to
primary subtags so a catalog tagged es-ES answers a browser asking for
es-419, deduplicating because es-ES,es yields that subtag twice, and
flooring at 'en' for a reader who expressed no preference. It is the same
computation, from the same fact, that a host would otherwise perform on the
editor's behalf.

Two declarations under use-when: navigator exists in the browser only, and
the editor compiles under Saxon for its headless tests, where there is no
reader to ask and 'en' is the answer.

$translations-lang stays, now defaulting from that function rather than from
a literal. A host that resolves the language from something the browser does
not know - a negotiated Accept-Language, a profile setting - still redeclares
it, and one that does not gets the reader's language without configuring
anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
A host with its own design system had one way to restyle an editor button:
copy the template that emits it and change the class. LinkedDataHub carries
four such copies - the link, figure, table and find dialogs - 56 lines whose
only divergence from this source is btn-primary becoming the host's own
button class, and which quietly stop tracking anything that changes here.

Five presentational tokens become parameters instead: the primary, secondary
and danger buttons, the helper text and the checkbox label. Their defaults
are what this editor's CSS already styles, so nothing changes for a
standalone editor, and a host that redeclares them gets its own buttons
throughout the chrome - dialogs, toolbar, overlay, block extension - without
forking a template.

The line is what is presentational and what is load-bearing. rdfa-editor-
content, slash-item, typeahead-input, remove-action and their kind are what
the editor's own ixsl:on* rules match on and its stylesheets select; those
stay literal, because a host redeclaring one would break dispatch rather than
restyle anything. None of the five carries a handler or appears outside the
editor's own CSS - which is what makes them parameterisable and the rest not.

52 headless assertions and the browser suites pass unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
namedgraph added a commit to AtomGraph/LinkedDataHub that referenced this pull request Sep 14, 2026
…ms and a function.

xsl/rdfa-editor/ is a copy of RDFa-Editor/src, and a copy's contract is that
diff -r comes out empty. Seven of its files had drifted - 229 lines - because
the editor hardcoded its English and applied its own button classes, and the
only places to change either were the copy itself, where the next cp reverts
it, or a whole-template override here, which forks markup this platform would
rather inherit. Both had happened: the toolbar and slash menu were localised
in the copy, four dialogs were forked into client/form.xsl for the sake of a
class name, and six lines of editor-generic work - a seam, a guard, two @title
readouts - existed nowhere but the copy.

The editor now carries the mechanisms instead (AtomGraph/RDFa-Editor#32): a
translations.rdf catalog read through rdfae:translations() and rdfae:label(),
rdfae:langs() asking navigator.languages, and presentational classes declared
as parameters. All of it upstream, so all of it arrives by copying.

What is left here is the part that is genuinely this platform's. One function
points the editor's label lookup at LDH's own catalog, which already answered
110 of its 112 keys in both languages - block-ellipsis and
failed-to-load-block-data are the two it did not. Four parameters dress the
editor's buttons and helper text in the design system. The four dialog
templates that used to do that by copying are deleted, and the overlay
redesign, the ldh:Combobox typeahead and the block dialog stay, because those
are behaviour rather than costume.

Measured on the running instance rather than argued: in a Spanish browser the
slash menu reads Párrafo, the find dialog Buscar, the drag handle Arrastre
para reordenar; in an English one, Paragraph, Find, Drag to reorder. The
dialogs arrive from the editor's own templates wearing ac-btn in-primary
ap-solid sz-sm, and the annotation overlay still opens as LDH's redesign -
tabs, and an object that is a literal or a resource but never both - because
upstream's populate-form calls the rdfae:reveal-fields seam this platform
overrides. diff -r against RDFa-Editor/src is empty across all 21 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
@namedgraph
namedgraph merged commit 65ed3b8 into master Sep 20, 2026
3 checks passed
@namedgraph
namedgraph deleted the localisation branch September 20, 2026 17:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant