/** * 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': 'Passphrase (min 12 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': 'Use at least {n} characters', 'register.err_too_weak': 'Too easy to guess. Your passphrase is what protects ' + 'your keys where they are stored — a few unrelated words work well.', 'register.strength': 'Strength: about {bits} bits. This protects the copy of ' + 'your keys kept on the nodes you join, so it is worth getting right.', // 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.search': 'Search groups...', 'explore.loading': 'Loading...', 'explore.empty': 'No public groups available.', 'explore.create_group': 'Create group', 'explore.join': 'Join', 'explore.member': 'Joined', // Group page 'group.default_name': 'Group', 'group.tab_files': 'Files', 'group.tab_chat': 'Chat', 'group.tab_members': 'Members', '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.upload': 'Upload', 'group.uploading': 'Uploading...', 'group.view': 'View', 'group.delete': 'Delete', 'group.delete_confirm': 'Delete {name}?', 'group.delete_group': 'Delete group', 'group.delete_group_confirm': 'Permanently delete "{name}" and remove all members? Files on the node are preserved.', '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', 'chat.attach': 'Attach file', // Video player 'video.loading': 'Loading {name}...', 'video.buffering': 'Buffering...', 'video.close': 'Close (Esc)', 'video.err_transport': 'Transport not connected', 'video.err_mse': 'Codec not supported for streaming: {codec}', // Settings 'settings.title': 'Settings', 'settings.coming_soon': 'Coming soon.', 'settings.profile': 'Profile', 'settings.username': 'Username', 'settings.node_pins': 'Node identities', 'settings.node_pins_hint': "Each node's identity key is remembered the first time you connect. If it changes, the connection is refused — that is expected only when an operator reinstalls a node. Verify with them before clearing.", 'settings.node_pins_count': '{n} pinned', 'settings.node_pins_clear': 'Clear pinned identities', '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', 'settings.role': 'Role', 'settings.groups': 'Group notifications', 'settings.notifications': 'Notifications', 'settings.node_key': 'Link Node', 'settings.node_key_desc': 'If you operate a MeshBay node, paste its Ed25519 public key here to link it to your account. You can find this key in your node\'s admin dashboard.', 'settings.node_key_placeholder': 'Paste node Ed25519 public key (base64)', 'settings.node_key_submit': 'Link Node', 'settings.node_key_success': 'Node linked successfully', 'settings.node_key_current': 'Linked node key', // Sidebar 'sidebar.create_group': 'Create group', 'sidebar.settings': 'Settings', 'sidebar.admin': 'Admin', // User menu 'usermenu.language': 'Language', 'usermenu.settings': 'Settings', 'usermenu.theme_light': 'Light mode', 'usermenu.theme_dark': 'Dark mode', 'usermenu.logout': 'Logout', // Admin panel 'admin.title': 'Administration', 'admin.tab_stats': 'Stats', 'admin.tab_users': 'Users', 'admin.tab_groups': 'Groups', 'admin.tab_logs': 'Logs', 'admin.tab_blocklist': 'Blocklist', // Admin stats 'admin.stat_users': 'Users', 'admin.stat_groups': 'Groups', 'admin.stat_nodes': 'Nodes', 'admin.stat_online': 'Online Nodes', // Admin users 'admin.users_search': 'Search users...', 'admin.col_username': 'Username', 'admin.col_role': 'Role', 'admin.col_status': 'Status', 'admin.col_created': 'Created', 'admin.col_actions': 'Actions', 'admin.col_email': 'Email', 'admin.col_groups': 'Groups', 'admin.no_users': 'No users found', 'admin.btn_suspend': 'Suspend', 'admin.btn_unsuspend': 'Unsuspend', 'admin.btn_details': 'Details', 'admin.user_detail': 'User details', 'admin.btn_close': 'Close', 'admin.self_note': '(you)', // Admin groups 'admin.col_name': 'Name', 'admin.col_visibility': 'Visibility', 'admin.col_members': 'Members', 'admin.no_groups': 'No groups found', // Admin logs 'admin.col_time': 'Time', 'admin.col_event': 'Event', 'admin.col_user': 'User', 'admin.col_ip': 'IP', 'admin.col_detail': 'Detail', 'admin.filter_all': 'All events', 'admin.no_logs': 'No logs found', 'admin.btn_load_more': 'Load more', // Admin blocklist 'admin.col_hash': 'Hash', 'admin.col_reason': 'Reason', 'admin.col_date': 'Date', 'admin.col_added_by': 'Added by', 'admin.no_blocked': 'No blocked hashes', 'admin.add_hash': 'Add hash to blocklist', 'admin.hash_placeholder': 'blake3 hash (64 hex chars)', 'admin.reason_placeholder': 'Reason', 'admin.btn_block': 'Block', 'admin.btn_unblock': 'Unblock', // Notifications // Create group 'create_group.title': 'Create Group', 'create_group.name': 'Group name', 'create_group.visibility': 'Visibility', 'create_group.private': 'Private', 'create_group.public': 'Public', 'create_group.join_policy': 'Join policy', 'create_group.invite': 'Invite only', 'create_group.open': 'Open (anyone can join)', 'create_group.description': 'Description', 'create_group.description_hint': 'What is this group about? (optional)', 'create_group.submit': 'Create', 'create_group.hint': 'A group needs a node to host files. You can create the group now and connect a node later.', 'create_group.creating': 'Creating...', // Members 'members.col_role': 'Role', 'members.group_role': 'Group role', 'members.admin': 'Admin', 'members.group_admin': 'Group admin', 'members.owner': 'Owner', 'members.member': 'Member', 'members.invite_title': 'Invite member', 'members.username_placeholder': 'Username', 'members.invite_btn': 'Invite', 'members.pair_title': 'Pair this browser with your node', 'members.pair_hint': 'Your node only accepts operator actions — invites, file ' + 'deletion — from a browser it has been paired with. Run ' + '`meshbay-node operator pair` on the node and type the code here. The code ' + 'never passes through the hub, which is what stops the hub from claiming to ' + 'be you.', 'members.pair_btn': 'Pair', 'members.pair_success': 'This browser is now paired with the node.', 'members.invite_code_ready': 'Invitation code for {user} — send it to them the way ' + 'you normally talk. It works once, and it never passes through the hub.', 'members.invite_code_hint': 'They enter it the first time they open this group. ' + 'You do not need to be online then.', 'group.join_code_title': 'This node needs to recognise you', 'group.join_code_hint': 'Ask whoever invited you for the one-time code, and enter ' + 'it here. After that this browser is recognised and you will not be asked again.', 'group.join_code_btn': 'Join', 'notif.title': 'Notifications', 'notif.empty': 'No notifications', 'notif.mark_all_read': 'Mark all read', // Sidebar 'sidebar.search': 'Search files', // Status 'status.cached': '{n} cached files', // Search 'search.title': 'Search Files', 'search.placeholder': 'Search across all groups...', 'search.no_results': 'No files match your search.', 'search.col_group': 'Group', 'search.result_count': '{n} files found', 'search.hint': 'Search file names across all cached group indexes.', }; // ── Locale registry ───────────────────────────────────────────────────────── const _strings = { en }; let _locale = 'en'; export const LOCALES = [ { code: 'en', name: 'English', flag: '\u{1F1EC}\u{1F1E7}' }, ]; 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();