This issue has been created
 
 
XWiki Platform / cid:jira-generated-image-avatar-52138bb8-e538-42cc-927d-9686ce03e4d5 XWIKI-25022 Open

Let a page carry the translations its client-side code needs, instead of fetching them over REST

 
View issue   ยท   Add comment
 

Issue created

 
cid:jira-generated-image-avatar-610fee12-8301-478e-8de1-96f8c518a656 Manuel Leduc created this issue on 18/Sep/26 14:37
 
Summary: Let a page carry the translations its client-side code needs, instead of fetching them over REST
Issue Type: cid:jira-generated-image-avatar-52138bb8-e538-42cc-927d-9686ce03e4d5 Improvement
Affects Versions: 17.10.0-rc-1
Assignee: Unassigned
Components: Localization
Created: 18/Sep/26 14:37
Priority: cid:jira-generated-image-static-major-f8d31559-9aad-4509-9314-9c94ea58fa13 Major
Reporter: Manuel Leduc
Description:

Context

The front-end resolves its translation keys over {{GET /rest/wikis/

{wiki}

/localization/translations}}, one round trip after the HTML. On the standard flavor's Sandbox.WebHome that is 7 requests and ~3 KB for 35 keys as a guest, on the critical path of the components that need the strings.

XWIKI-25008 reduces the number of those requests and XWIKI-25009 makes a repeat request cheap (an ETag and a 304). Neither removes the first visit: the keys are still fetched after the page has loaded, although the server knew those strings while it rendered the page.

Proposal

Let a page carry the translations its first-paint JavaScript needs, as an opt-in mechanism. A key that is not pre-rendered keeps working exactly as today, by falling back to the REST resolver, so nothing becomes mandatory and UI loaded asynchronously is not preloaded.

Declaring keys

An extension declares named key bundles in its pom.xml, the way import maps are declared:

<xwiki.extension.localization.keys>
{
  "xwiki-requiredrights-messages": [
    "security.requiredrights.ui.modal.label",
    "security.requiredrights.ui.modal.close"
  ]
}
</xwiki.extension.localization.keys>

Full keys, no prefix: a prefix only ever saved bytes on the wire, which this mechanism does not spend. No packaging change is needed, since AbstractModelConverter already copies every xwiki.extension.* pom property into the extension descriptor; only a consumer is new, modelled on JavascriptImportmapResolver (merge the declarations of the installed and core extensions, cache per wiki, invalidate on extension events).

The pom is the single source of truth for a bundle that opts in: its AMD key module is generated from the declaration, so the keys exist once. Existing hand-written key modules keep working untouched.

Collecting and emitting

A request-scoped collector gathers what the page pulls, in the spirit of $xwiki.jsx.use(...) and RequiredSkinExtensionsRecorder:

  • $services.localization.preload('xwiki-requiredrights-messages') for a declared bundle;
  • $services.localization.preload(['myapp.a', 'myapp.b']) for a wiki page that has no pom to declare keys in.

The collected keys are written as one block at the end of the page (the head is evaluated before the content, so a block in the head would need the legacy endParsing hook to see keys declared while rendering content):

<script type="application/json" id="xwiki-translations" data-locale="en">
{"security.requiredrights.ui.modal.label": "Required Rights", "myapp.missing.key": null}
</script>

The values are the raw sources, with parameters unresolved, exactly as the REST resource returns them. A key with no translation is present with a null value, like the REST answer: the client keys off presence, not value, so an inlined null means "no translation, do not ask again" while an absent key means "ask REST". $jsontool escapes <, so the block cannot be broken out of.

Reading it

A new translator is wired before the REST one in the localization webjar's initialize(...). The chaining contract of localization-api already says a translator answers what it can and passes the rest on, so the fallback is today's behaviour and no xwiki-l10n caller changes. The translator reads the blocks on demand at each resolution, so a block that appears later is picked up.

Later phases

  • An asynchronously rendered HTML fragment carries its own block inside its markup; the on-demand read finds it when the fragment is inserted, with no header and no lifecycle to coordinate.
  • A Vue component loaded through a dynamic import() has no server-rendered markup. Either it keeps using REST (one batched, revalidated request), or it gets a versioned per-module, per-locale resource served with the webjar handler's long cache lifetime. Left open on purpose: the gain over a revalidated batch is small, and versioned URLs sit badly with translations that are editable in the wiki. Bundling translations into the chunk at build time is rejected, since it multiplies build outputs per locale and breaks wiki-side overrides.

Measured on a prototype

A working prototype (a template emitting the block, plus the extra translator in the webjar) on a page carrying a Live Data macro, so 105 keys are requested: 36 by the page itself and 69 by Live Data, which is loaded as a lazily imported component. Guest, 100 ms of added round-trip time, with XWIKI-25008 and XWIKI-25009 in place in every column:

visit today the page's 36 keys inlined all 105 inlined
1st 7 requests / 11.4 KB 2 requests / 6.4 KB 0 requests
2nd 2 requests / 9.3 KB 2 requests / 0.7 KB 0 requests
3rd and later 2 requests / 0.7 KB 2 requests / 0.7 KB 0 requests
  • The first visit loses its localization round trip, which is the point: the strings are there when the HTML is parsed.
  • Resolving and serialising the keys adds no measurable render time; the cost is bytes: +2.5 KB of HTML per view for 36 keys, +6.7 KB for 105.
  • Counting that HTML on every view, pre-rendering is ahead for about the first five visits of a browser cache lifetime with 36 keys, and only three with 105. Hence the documented rule: pre-render what first paint needs, nothing else. Live Data's 69 keys alone are 5.6 KB of the 6.7 KB, to save a request for a component that is not on the critical path.
  • Pre-rendering the page's keys also makes the remaining REST batch deterministic across visits, which is what revalidation needs.

Notes

This is design work: it is sequenced after XWIKI-25008 and XWIKI-25009, and should be re-measured once they are in, since they change the baseline.