diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-16 00:21:07 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-16 00:21:07 +0200 |
| commit | 3b2d03a919e56d14f8097844b97b844ddf526cd6 (patch) | |
| tree | 31b222fedf6d608809eb47c45c5f431d7001d87a /packages/meshbay-hub/src/meshbay_hub/static/app.js | |
| parent | 75065cc1ed5d9cd733871d478dc4f915663d795c (diff) | |
| download | meshbay-3b2d03a919e56d14f8097844b97b844ddf526cd6.tar.gz | |
feat(hub): translate the web client into nine more languages
French, Spanish, Brazilian Portuguese, Simplified Chinese, Japanese,
German, Italian, Dutch and Polish, all in the formal register. `hub`,
`node` and `GEK` stay in English: they name the CLI, node.toml and the
docs, and translating them would cut the interface off from everything an
operator reads and types.
Catalogues move out of i18n.js into locales/, one file per language,
fetched with a dynamic import. A visitor downloads their language plus
English as a fallback — about 36 KB rather than the ~180 KB that ten
inlined catalogues would have cost everyone. i18n.js keeps only the
loader, so the first render now waits for initLocale().
Three things the old code got wrong, none of them visible until there was
a second language:
- Resolution trimmed a tag to its base before matching, so a browser
reporting pt-BR looked for a `pt` catalogue that does not exist and fell
back to English. Matching is now exact first, then by base language.
- Counted strings were single strings, so Polish could not express
1 plik / 2 pliki / 5 plików at all. t() selects through
Intl.PluralRules; en.js gains the same treatment, which incidentally
fixes "1 files".
- Interpolation used String.replace, which reads `$&` in the replacement.
A file named rap$&sody.mp3 rendered corrupted in its own delete dialog.
Five strings were still hardcoded in app.js — the group name placeholder
and the four visibility/join-policy descriptions — and are now keyed.
test_locales.py holds the nine translations to the shape of en.js: same
keys, a counted string stays counted everywhere, every plural entry covers
each category Intl actually produces for that language, and the
{placeholders} survive translation. Verified failing first, against a
catalogue with a key removed, a placeholder dropped and the Polish `few`
form deleted.
The language menu also grew from one entry to ten, which overran a short
viewport inside a dropdown that clipped instead of scrolling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 24 |
1 files changed, 17 insertions, 7 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index cf33d72..6a5a13f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -2,7 +2,7 @@ import { html, render, useState, useEffect, useCallback, useRef, createContext, useContext, } from './vendor/htm-preact.js'; -import { t, getLocale, setLocale, LOCALES } from './i18n.js'; +import { t, getLocale, setLocale, initLocale, LOCALES } from './i18n.js'; import { ZipStream, entriesUnder } from './zipstream.js'; import { transfers, formatSpeed } from './transfers.js'; import * as downloads from './downloads.js'; @@ -852,7 +852,7 @@ function CreateGroupPage({ token, onCreated }) { <form onSubmit=${onSubmit}> <div class="form-field"> <label class="form-label">${t('create_group.name')}</label> - <input type="text" placeholder="e.g. Family Photos, Project Files..." + <input type="text" placeholder="${t('create_group.name_placeholder')}" value=${name} onInput=${e => setName(e.target.value)} required autofocus /> </div> @@ -872,13 +872,13 @@ function CreateGroupPage({ token, onCreated }) { onClick=${() => setVisibility('private')}> <${Icon} name="lock" cls="option-icon" /> <span class="option-title">${t('create_group.private')}</span> - <span class="option-desc">Only invited members can see and access files</span> + <span class="option-desc">${t('create_group.private_desc')}</span> </button> <button type="button" class="option-card ${visibility === 'public' ? 'selected' : ''}" onClick=${() => setVisibility('public')}> <${Icon} name="globe" cls="option-icon" /> <span class="option-title">${t('create_group.public')}</span> - <span class="option-desc">Anyone can discover and browse this group</span> + <span class="option-desc">${t('create_group.public_desc')}</span> </button> </div> </div> @@ -890,13 +890,13 @@ function CreateGroupPage({ token, onCreated }) { onClick=${() => setJoinPolicy('invite')}> <${Icon} name="envelope" cls="option-icon" /> <span class="option-title">${t('create_group.invite')}</span> - <span class="option-desc">Members must be invited by an admin</span> + <span class="option-desc">${t('create_group.invite_desc')}</span> </button> <button type="button" class="option-card ${joinPolicy === 'open' ? 'selected' : ''}" onClick=${() => setJoinPolicy('open')}> <${Icon} name="door" cls="option-icon" /> <span class="option-title">${t('create_group.open')}</span> - <span class="option-desc">Anyone can join without approval</span> + <span class="option-desc">${t('create_group.open_desc')}</span> </button> </div> </div> @@ -3643,4 +3643,14 @@ function App() { // ── Boot ───────────────────────────────────────────────────────────────────── -render(html`<${App} />`, document.getElementById('app')); +// Catalogues are fetched, so the first render waits for one: mounting earlier +// would paint the interface in English and then swap every string. initLocale() +// falls back to English rather than rejecting, so this cannot strand the page. +const mount = () => render(html`<${App} />`, document.getElementById('app')); +initLocale().then(mount, (err) => { + // Nothing in initLocale() is supposed to reject. If something does, an + // English interface is still an interface; an unhandled rejection here is a + // blank page. + console.error('[MeshBay] locale init failed, continuing in English:', err); + mount(); +}); |