# 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