# Wording and localization
Source: https://selfstore.dev/docs/widgets-localization
How selfstore's widgets resolve their copy - shipped English and French packs, the page's lang attribute, per-key overrides, placeholder interpolation, and the empty-string trick that removes a heading.
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
```
The language is resolved per element, in this order:
1. the nearest ancestor with a `lang` attribute (the element itself counts);
2. ``;
3. `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
```ts
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:
1. your `labels` map;
2. the shipped pack for the page's language;
3. the English defaults;
4. 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:
```ts
el.labels = { 'share.title': '' }; // the page's own
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:
```ts
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 `.placeless` twin**, used when a
destination is attached but has no name to give. `status.saved` has
`status.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:
```ts
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`.
Map of this site for a model: https://selfstore.dev/llms.txt
Every page in one file: https://selfstore.dev/llms-full.txt