Wording and localization
Every string a widget renders is a key with a default. You override the ones you want; the rest keep working. A drop-in element that needs a translation file before it can show a screen is not drop-in, so the widgets ship their own.
What you get without writing anything
Each widget ships an English default set and a French pack. A French app writes nothing at all: set the language on the page and the widgets follow.
<html lang="fr">
The language is resolved per element, in this order:
- the nearest ancestor with a
langattribute (the element itself counts); <html lang>;navigator.language.
Only the subtag is used, lowercased - fr-CA and FR both select fr. A
language with no pack falls through to English.
That last rule makes a mixed page work: a French app embedding one English
section can put lang="en" on that section, and the widgets inside it follow.
Overriding a key
el.labels = {
'connect.title': 'Where should your notes live?',
'connect.file': 'A file on this computer'
};
The map is partial and merged, never a replacement. Override three keys and you get those three plus translated copy for everything else.
Resolution runs in this order, first hit wins:
- your
labelsmap; - the shipped pack for the page’s language;
- the English defaults;
- the key itself.
The last step matters when you are debugging: a screen showing
connect.password.title instead of a sentence means the key does not exist -
usually a typo in your map, since a real key always has an English default
behind it.
An existing app that passes a full map keeps exactly the copy it had. Adopting a newer widget never silently changes wording you already chose.
Removing a heading
Assign an empty string to remove a heading whose job your page already does:
el.labels = { 'share.title': '' }; // the page's own <h2> already says it
This works for the *.title keys. It is not a hack around a missing knob - it
is the documented way to drop a heading without forking the widget.
Placeholders
Copy can carry {placeholders}, filled from the widget’s state:
el.labels = {
'status.saved': 'Sauvegarde dans {place}'
};
A status that cannot name where it saved has to be read twice - the state on one line, the destination on another. Interpolation lets a pack write one sentence, and lets each language put the place where its own grammar wants it, which a fixed “state, then label” layout cannot.
Two rules that come from this design:
- A key built around a place has a
.placelesstwin, used when a destination is attached but has no name to give.status.savedhasstatus.saved.placeless. The twin exists only where it is needed - a key that never mentions a place already answers on its own. - An unfilled placeholder is left visible, not blanked. A sentence with a
hole reads as a bug nobody can name;
{place}on screen names it.
Error copy
Flow errors map to their own keys, so you can reword a failure without touching the happy path:
| Key | When |
|---|---|
error.generic |
Any failure with no more specific key |
error.targetUnavailable |
The destination did not answer |
error.authExpired |
The authorization expired and must be redone |
error.decryptFailed |
The password did not open the backup |
error.badFormat |
The file is not a selfstore backup |
Not every widget carries every key - each element’s reference page lists its own. The wording differs per widget on purpose: “the destination did not answer” and “the share service did not answer” are the same error in two different places, and only the widget knows which one it is.
Translating to a third language
The packs live in the library, so a new language is a contribution to selfstore rather than a file in your app. Until it lands, pass a full map:
const ES = {
'connect.title': 'Donde guardamos tus datos?',
'connect.file': 'Un archivo en este dispositivo'
// ...
};
for (const el of document.querySelectorAll('selfstore-connect')) {
el.labels = ES;
}
Each element’s reference page lists every key it owns, so a full map is a finite, checkable job rather than a hunt through the rendered UI.
The timing rule
labels is read when the widget builds its view. Assign it before store,
which is what wires the flow and triggers the first render.
The gate is stricter: while it is open, its frame stands and only cosmetics are
pushed into the child, so labels assigned to an already-open gate reach the
connect journey inside but not the frame’s own title. Set them before the gate
opens - in practice, right after you create the element and before you assign
store.