diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-11 04:13:53 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-11 04:13:53 +0200 |
| commit | e23e33adeaf8ee7439187d4451c856b37816a51f (patch) | |
| tree | a41eef1fba34cdd642d25576395b4ad484a748ad /packages/meshbay-hub/src/meshbay_hub/static/i18n.js | |
| parent | 60c4570e72e36c2a9720593c8baec74ee2ab52d6 (diff) | |
| download | meshbay-e23e33adeaf8ee7439187d4451c856b37816a51f.tar.gz | |
feat: Phase 9 — Web client SPA with WebRTC P2P transport
Complete browser-based client: Preact SPA with login, group file browser,
encrypted download, video playback, group chat, i18n, and dark/light theme.
Browser connects P2P to nodes behind residential NAT via WebRTC DataChannel
(aiortc). Hub handles signaling only — all data flows E2E.
Performance: pipelined downloads (8-chunk sliding window), binary msgpack
wire format (no base64), redundant I/O elimination. Large file downloads
stream to disk via File System Access API (showSaveFilePicker).
Validated on SFR + Orange residential NATs, Chrome + Firefox, IPv4/IPv6.
132 tests passing. Deployed to meshbay.org + Orange node.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/i18n.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/i18n.js | 172 |
1 files changed, 172 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js new file mode 100644 index 0000000..e0242cf --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js @@ -0,0 +1,172 @@ +/** + * MeshBay i18n — lightweight string localization. + * + * Usage: + * import { t, setLocale, getLocale, LOCALES } from './i18n.js'; + * t('nav.logout') // "Logout" + * t('status.files', { n: 42 }) // "42 files" + */ + +const LANG_KEY = 'mb_lang'; + +// ── English strings ───────────────────────────────────────────────────────── + +const en = { + // Nav + 'nav.toggle_menu': 'Toggle menu', + 'nav.light_mode': 'Light mode', + 'nav.dark_mode': 'Dark mode', + 'nav.logout': 'Logout', + 'nav.login': 'Login', + + // Sidebar + 'sidebar.my_groups': 'My Groups', + 'sidebar.no_groups': 'No groups yet', + 'sidebar.discover': 'Discover', + 'sidebar.public_groups': 'Public groups', + + // Login + 'login.title': 'Login', + 'login.username': 'Username', + 'login.password': 'Password', + 'login.submit': 'Login', + 'login.loading': 'Logging in...', + 'login.no_account': 'No account?', + 'login.register_link': 'Register', + + // Register + 'register.title': 'Register', + 'register.username': 'Username', + 'register.email': 'Email', + 'register.password': 'Password (min 8 chars)', + 'register.confirm': 'Confirm password', + 'register.submit': 'Register', + 'register.loading': 'Creating account...', + 'register.has_account': 'Already have an account?', + 'register.login_link': 'Login', + 'register.success_title': 'Account created', + 'register.success_msg': 'You can now log in with your credentials.', + 'register.go_login': 'Go to login', + 'register.err_mismatch': 'Passwords do not match', + 'register.err_min_len': 'Password must be at least 8 characters', + + // Home + 'home.welcome': 'Welcome to MeshBay', + 'home.no_groups': 'You are not a member of any group yet.', + 'home.browse_prefix': 'Browse ', + 'home.browse_link': 'public groups', + 'home.browse_suffix': ' or ask a group admin to invite you.', + 'home.my_groups': 'My Groups', + + // Explore + 'explore.title': 'Public Groups', + 'explore.loading': 'Loading...', + 'explore.empty': 'No public groups available.', + + // Group page + 'group.default_name': 'Group', + 'group.tab_files': 'Files', + 'group.tab_chat': 'Chat', + 'group.filter': 'Filter files...', + 'group.col_name': 'Name', + 'group.col_size': 'Size', + 'group.col_type': 'Type', + 'group.col_date': 'Date', + 'group.empty_filter': 'No files match the filter', + 'group.empty_dir': 'This directory is empty', + 'group.download': 'Download', + 'group.play': 'Play', + 'group.dl_failed': 'Download failed: {err}', + 'group.offline_title': 'No nodes are currently online for this group.', + 'group.offline_hint': 'Files will appear when a node hosting this group connects.', + 'group.err_transport': 'Transport module not loaded', + + // Status + 'status.idle': 'Idle', + 'status.discovering': 'Finding nodes...', + 'status.connecting': 'Connecting via WebRTC...', + 'status.fetching': 'Fetching index...', + 'status.files': '{n} files', + 'status.offline': 'No nodes online', + 'status.error': 'Connection failed', + + // Chat + 'chat.empty': 'No messages yet. Start the conversation!', + 'chat.placeholder': 'Type a message...', + 'chat.send': 'Send', + + // Video player + 'video.loading': 'Loading {name}...', + 'video.close': 'Close (Esc)', + 'video.err_transport': 'Transport not connected', + + // Settings + 'settings.title': 'Settings', + 'settings.coming_soon': 'Coming soon.', + 'settings.profile': 'Profile', + 'settings.username': 'Username', + 'settings.appearance': 'Appearance', + 'settings.theme': 'Theme', + 'settings.theme_light': 'Light', + 'settings.theme_dark': 'Dark', + 'settings.theme_system': 'System', + 'settings.language': 'Language', + 'settings.about': 'About', + 'settings.version': 'Version', + 'settings.protocol': 'Protocol', + + // Sidebar + 'sidebar.settings': 'Settings', +}; + +// ── Locale registry ───────────────────────────────────────────────────────── + +const _strings = { en }; +let _locale = 'en'; + +export const LOCALES = [ + { code: 'en', name: 'English' }, +]; + +export function getLocale() { return _locale; } + +export function setLocale(code) { + if (_strings[code]) { + _locale = code; + localStorage.setItem(LANG_KEY, code); + return true; + } + return false; +} + +export function addLocale(code, strings) { + _strings[code] = strings; +} + +export function t(key, params) { + let s = (_strings[_locale] && _strings[_locale][key]) + || _strings.en[key] + || key; + if (params) { + for (const [k, v] of Object.entries(params)) { + s = s.replace(`{${k}}`, v); + } + } + return s; +} + +// ── Init ──────────────────────────────────────────────────────────────────── + +function _init() { + const stored = localStorage.getItem(LANG_KEY); + if (stored && _strings[stored]) { + _locale = stored; + return; + } + const browserLang = (navigator.language || '').split('-')[0]; + if (_strings[browserLang]) { + _locale = browserLang; + } +} + +_init(); |