diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub')
13 files changed, 4155 insertions, 394 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(); +}); diff --git a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js index affa7c8..48408d9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js @@ -1,422 +1,159 @@ /** * MeshBay i18n — lightweight string localization. * + * Catalogues live one per language in `locales/` and are fetched on demand, so + * a visitor downloads the language they read plus English as a fallback, never + * the other eight. + * * Usage: - * import { t, setLocale, getLocale, LOCALES } from './i18n.js'; + * import { t, initLocale, setLocale, getLocale, LOCALES } from './i18n.js'; + * await initLocale(); // once, before the first render * t('nav.logout') // "Logout" * t('status.files', { n: 42 }) // "42 files" + * + * A catalogue value is a string, or an object keyed by Intl.PluralRules + * category when it is rendered with a count: + * 'status.files': { one: '{n} file', other: '{n} files' } + * Polish needs `one`/`few`/`many`/`other` where English needs two, which is why + * the plural category is chosen here rather than baked into the string. */ const LANG_KEY = 'mb_lang'; +const FALLBACK = 'en'; -// ── 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.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.mkdir': 'New folder', - 'group.mkdir_prompt': 'Name of the new folder:', - '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)', - 'preview.pdf_fallback': 'This browser will not display the PDF inline. Download it instead — it was decrypted here either way.', - 'group.upload_indexing': 'indexing…', - '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.downloads': 'Downloads', - 'settings.dl_auto': 'Save automatically', - 'settings.dl_auto_hint': 'Files go straight into the folder you choose below, ' - + 'with no dialog. Downloading twenty files puts twenty files there.', - 'settings.dl_ask': 'Ask every time', - 'settings.dl_ask_hint': 'A Save As dialog for each file — one per file, ' - + 'including when you download a selection.', - 'settings.dl_folder': 'Folder: {name}', - 'settings.dl_no_folder': 'No folder chosen — downloads go wherever your browser ' - + 'puts them', - 'settings.dl_choose': 'Choose folder', - 'settings.dl_change': 'Change', - 'settings.dl_forget': 'Forget', - 'settings.dl_path_note': 'A web page cannot be given a path, so there is nothing ' - + 'to type here: your browser grants access to the folder you pick, and MeshBay ' - + 'only ever writes inside it. You may be asked to confirm once per session.', - 'settings.dl_unsupported': 'This browser cannot write into a folder of your ' - + 'choosing (no File System Access API), so downloads go to its own download ' - + 'folder. Chrome and Edge support choosing one.', - '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.danger': 'Delete account', - 'settings.delete_hint': 'Removes your account, your group memberships and your ' - + 'notifications from the hub, and frees your username. It cannot reach what ' - + 'lives on nodes: files you uploaded stay where their operator keeps them, ' - + 'and each node keeps the identity it pinned for you until its operator ' - + 'unpins it. Connection logs are kept for a year, as the law requires.', - 'settings.delete_account': 'Delete my account', - 'settings.delete_confirm': 'Enter your passphrase to confirm. This cannot be undone.', - 'settings.delete_confirm_btn': 'Delete for good', - 'settings.cancel': 'Cancel', - '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_nodes': 'Nodes', - 'admin.tab_logs': 'Logs', - 'admin.col_observed_ip': 'Seen from', - 'admin.col_hint': 'Announced hint', - 'admin.col_last_seen': 'Last announce', - 'admin.node_online': 'online', - 'admin.node_offline': 'offline', - 'admin.no_nodes': 'No nodes registered', - 'admin.nodes_hint': '"Seen from" is the address the announcement arrived from, ' - + 'on a request signed with the node key — that is the one to answer a ' - + 'question with. The hint is what the node believes its own address to be, ' - + 'learned from a STUN server and sent to us; it is useful for reaching the ' - + 'node and is not evidence of anything.', - '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.btn_delete': 'Delete', - 'admin.delete_confirm': 'Delete the account "{user}"? This cannot be undone. ' - + 'Files they uploaded stay on the nodes that host them, and each node keeps ' - + 'the identity it pinned until its operator unpins it. Suspending is the ' - + 'reversible option.', - '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', +export const LOCALES = [ + { code: 'en', name: 'English', flag: '\u{1F1EC}\u{1F1E7}' }, + { code: 'fr', name: 'Français', flag: '\u{1F1EB}\u{1F1F7}' }, + { code: 'es', name: 'Español', flag: '\u{1F1EA}\u{1F1F8}' }, + { code: 'pt-BR', name: 'Português (Brasil)', flag: '\u{1F1E7}\u{1F1F7}' }, + { code: 'zh-CN', name: '简体中文', flag: '\u{1F1E8}\u{1F1F3}' }, + { code: 'ja', name: '日本語', flag: '\u{1F1EF}\u{1F1F5}' }, + { code: 'de', name: 'Deutsch', flag: '\u{1F1E9}\u{1F1EA}' }, + { code: 'it', name: 'Italiano', flag: '\u{1F1EE}\u{1F1F9}' }, + { code: 'nl', name: 'Nederlands', flag: '\u{1F1F3}\u{1F1F1}' }, + { code: 'pl', name: 'Polski', flag: '\u{1F1F5}\u{1F1F1}' }, +]; - // 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...', +const _codes = LOCALES.map(l => l.code); +const _strings = {}; +const _plurals = {}; +let _locale = FALLBACK; - // 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.invite_needs_pairing': 'Invitations are signed with the key this node ' - + 'pinned for your browser, so pair it below before inviting anyone.', - 'members.invite_ask_operator': 'Only the operator of the node hosting this group can ' - + 'invite, from a browser paired with it.', - 'members.remove': 'Remove', - 'members.remove_confirm': 'Remove {user} from this group? Their account, their ' - + 'other groups and the files they uploaded are not affected.', - 'members.remove_hint': 'Removing someone takes away their membership and stops ' - + 'the node serving them the group key. It does not delete their account, and ' - + 'it does not take back the key they already hold — rotate it on the node ' - + '(meshbay-node gek-init) if that matters.', - '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.', - '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.', - 'transfers.title': 'Transfers', - 'transfers.clear': 'Clear finished', - 'transfers.cancel': 'Cancel', - 'transfers.open': 'Open', - 'transfers.done': 'Finished', - 'transfers.cancelled': 'Cancelled', - 'transfers.failed': 'Failed', - 'group.select': 'Select', - 'group.select_done': 'Done', - 'group.actions': 'Actions ({n})', - 'group.download_n': 'Download ({n})', - 'group.download_zip_n': 'Download folders as zip ({n})', - 'group.delete_n': 'Delete ({n})', - 'group.delete_n_confirm': 'Delete {n} item(s)? {names}', - 'group.download_zip': 'Download as zip ({n} files)', - 'group.zip_empty': 'That folder has nothing in it to download.', - 'group.zip_no_stream': 'This browser cannot write a download straight to disk, ' - + 'so {name} ({size}) has to be built in memory first. On a large archive ' - + 'that may fail. Continue?', - 'group.rmdir': 'Delete folder', - 'group.rmdir_confirm': 'Delete the folder "{name}"? It must be empty.', - 'group.rmdir_not_empty': 'That folder is not empty. Delete what is in it first — ' - + 'nothing here removes files you cannot see.', - 'group.desc_edit': 'Edit description', - 'group.desc_add': 'Add a description', - 'group.desc_save': 'Save', - 'group.desc_cancel': 'Cancel', - 'group.desc_placeholder': 'What this group is for — members see it on their home page.', - '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', +// ── Resolution ────────────────────────────────────────────────────────────── - 'notif.purge': 'Clear all', - 'notif.title': 'Notifications', - 'notif.empty': 'No notifications', - 'notif.mark_all_read': 'Mark all read', +/** + * Match a requested tag against the registry, region-tolerantly. + * + * `navigator.language` is `pt-BR` or `zh-CN` on the machines those catalogues + * are for, and `fr-CA` or `de-AT` on machines the base catalogue serves fine. + * Trimming to the base first — which this used to do — sent a Brazilian browser + * looking for a `pt` catalogue that does not exist. + */ +function _match(tag) { + if (!tag) return null; + const want = String(tag).replace('_', '-'); + const exact = _codes.find(c => c.toLowerCase() === want.toLowerCase()); + if (exact) return exact; + const base = want.split('-')[0].toLowerCase(); + return _codes.find(c => c.split('-')[0].toLowerCase() === base) || null; +} - // Sidebar - 'sidebar.search': 'Search files', +/** + * Storage access throws outright in some privacy modes rather than returning + * null. A language preference is not worth failing the boot over, so both + * directions swallow it and the browser's own languages decide instead. + */ +function _stored(value) { + try { + if (value === undefined) return localStorage.getItem(LANG_KEY); + localStorage.setItem(LANG_KEY, value); + } catch { + return null; + } + return value; +} - // Status +function _preferred() { + const stored = _match(_stored()); + if (stored) return stored; + for (const tag of navigator.languages || [navigator.language]) { + const hit = _match(tag); + if (hit) return hit; + } + return FALLBACK; +} - // Search - 'search.title': 'Search Files', - 'search.placeholder': 'Search across all groups...', - 'search.no_results': 'No files match your search.', - 'search.synced': 'synced {ago}', - 'search.cache_note': 'Search reads what each group looked like the last time you ' - + 'opened it. Open a group to bring its files up to date here.', - 'search.col_group': 'Group', - 'search.result_count': '{n} files found', - 'search.hint': 'Search file names across all cached group indexes.', -}; +// ── Loading ───────────────────────────────────────────────────────────────── -// ── Locale registry ───────────────────────────────────────────────────────── +async function _load(code) { + if (_strings[code]) return true; + try { + const mod = await import(`./locales/${code}.js`); + _strings[code] = mod.default; + return true; + } catch (err) { + console.warn(`[MeshBay] locale ${code} failed to load:`, err); + return false; + } +} -const _strings = { en }; -let _locale = 'en'; +/** + * Load the active catalogue and the English fallback, then fix the locale. + * Resolves even when a catalogue is missing — an untranslated interface beats + * a blank page, so a failed load degrades to English rather than throwing. + */ +export async function initLocale() { + const want = _preferred(); + const loads = [_load(want)]; + if (want !== FALLBACK) loads.push(_load(FALLBACK)); + const [ok] = await Promise.all(loads); + _locale = ok ? want : FALLBACK; + if (!_strings[FALLBACK]) await _load(FALLBACK); + document.documentElement.lang = _locale; + return _locale; +} -export const LOCALES = [ - { code: 'en', name: 'English', flag: '\u{1F1EC}\u{1F1E7}' }, -]; +// ── Lookup ────────────────────────────────────────────────────────────────── export function getLocale() { return _locale; } export function setLocale(code) { - if (_strings[code]) { - _locale = code; - localStorage.setItem(LANG_KEY, code); - return true; - } - return false; + const hit = _match(code); + if (!hit) return false; + _stored(hit); + return true; } export function addLocale(code, strings) { _strings[code] = strings; } +function _pluralRules(locale) { + if (!_plurals[locale]) _plurals[locale] = new Intl.PluralRules(locale); + return _plurals[locale]; +} + +/** A catalogue entry is a plain string, or plural categories to choose from. */ +function _select(entry, locale, params) { + if (typeof entry === 'string') return entry; + if (!entry || typeof entry !== 'object') return null; + const n = Number(params && params.n); + const cat = _pluralRules(locale).select(Number.isFinite(n) ? n : 0); + return entry[cat] ?? entry.other ?? null; +} + export function t(key, params) { - let s = (_strings[_locale] && _strings[_locale][key]) - || _strings.en[key] - || key; + let s = _select(_strings[_locale] && _strings[_locale][key], _locale, params); + if (s == null) { + s = _select(_strings[FALLBACK] && _strings[FALLBACK][key], FALLBACK, params); + } + if (s == null) return key; if (params) { for (const [k, v] of Object.entries(params)) { - s = s.replace(`{${k}}`, v); + // split/join rather than replace: a value is often a filename, and + // String.replace reads `$&` and friends in the replacement. + s = s.split(`{${k}}`).join(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(); diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js new file mode 100644 index 0000000..1ff4687 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -0,0 +1,407 @@ +/** + * German — translated from locales/en.js. + * + * Register: formal (Siezen). + * `hub`, `node` and `GEK` are left in English: they name the CLI + * (`meshbay-node`), node.toml and the docs, which are English throughout. + * Grammatical gender chosen here: der Hub, der Node, der GEK (der Schlüssel). + */ + +export default { + // Nav + 'nav.toggle_menu': 'Menü anzeigen', + 'nav.light_mode': 'Helles Design', + 'nav.dark_mode': 'Dunkles Design', + 'nav.logout': 'Abmelden', + 'nav.login': 'Anmelden', + + // Sidebar + 'sidebar.my_groups': 'Meine Gruppen', + 'sidebar.no_groups': 'Noch keine Gruppen', + 'sidebar.discover': 'Entdecken', + 'sidebar.public_groups': 'Öffentliche Gruppen', + + // Login + 'login.title': 'Anmelden', + 'login.username': 'Benutzername', + 'login.password': 'Passwort', + 'login.submit': 'Anmelden', + 'login.loading': 'Anmeldung läuft …', + 'login.no_account': 'Noch kein Konto?', + 'login.register_link': 'Konto erstellen', + + // Register + 'register.title': 'Konto erstellen', + 'register.username': 'Benutzername', + 'register.email': 'E-Mail-Adresse', + 'register.password': 'Passphrase (mindestens 12 Zeichen)', + 'register.confirm': 'Passwort bestätigen', + 'register.submit': 'Konto erstellen', + 'register.loading': 'Konto wird erstellt …', + 'register.has_account': 'Sie haben bereits ein Konto?', + 'register.login_link': 'Anmelden', + 'register.success_title': 'Konto erstellt', + 'register.success_msg': 'Sie können sich jetzt mit Ihren Zugangsdaten anmelden.', + 'register.go_login': 'Zur Anmeldung', + 'register.err_mismatch': 'Die Passwörter stimmen nicht überein', + 'register.err_min_len': { + one: 'Verwenden Sie mindestens {n} Zeichen', + other: 'Verwenden Sie mindestens {n} Zeichen', + }, + 'register.err_too_weak': 'Zu leicht zu erraten. Ihre Passphrase ist das, was Ihre ' + + 'Schlüssel dort schützt, wo sie gespeichert sind — ein paar zusammenhanglose ' + + 'Wörter funktionieren gut.', + 'register.strength': 'Stärke: etwa {bits} Bit. Sie schützt die Kopie Ihrer ' + + 'Schlüssel, die auf den Nodes liegt, denen Sie beitreten — es lohnt sich also, ' + + 'sie gut zu wählen.', + + // Home + 'home.welcome': 'Willkommen bei MeshBay', + 'home.no_groups': 'Sie sind noch in keiner Gruppe Mitglied.', + 'home.browse_prefix': 'Sehen Sie sich die ', + 'home.browse_link': 'öffentlichen Gruppen', + 'home.browse_suffix': ' an oder bitten Sie einen Gruppenadministrator um eine Einladung.', + 'home.my_groups': 'Meine Gruppen', + + // Explore + 'explore.title': 'Öffentliche Gruppen', + 'explore.search': 'Gruppen suchen …', + 'explore.loading': 'Wird geladen …', + 'explore.empty': 'Keine öffentlichen Gruppen verfügbar.', + 'explore.create_group': 'Gruppe erstellen', + 'explore.join': 'Beitreten', + 'explore.member': 'Beigetreten', + + // Group page + 'group.default_name': 'Gruppe', + 'group.tab_files': 'Dateien', + 'group.tab_chat': 'Chat', + 'group.tab_members': 'Mitglieder', + 'group.filter': 'Dateien filtern …', + 'group.col_name': 'Name', + 'group.col_size': 'Größe', + 'group.col_type': 'Typ', + 'group.col_date': 'Datum', + 'group.empty_filter': 'Keine Datei entspricht dem Filter', + 'group.empty_dir': 'Dieser Ordner ist leer', + 'group.download': 'Herunterladen', + 'group.play': 'Abspielen', + 'group.offline_title': 'Für diese Gruppe ist derzeit kein Node online.', + 'group.offline_hint': 'Die Dateien erscheinen, sobald sich ein Node verbindet, der ' + + 'diese Gruppe hostet.', + 'group.upload': 'Hochladen', + 'group.mkdir': 'Neuer Ordner', + 'group.mkdir_prompt': 'Name des neuen Ordners:', + 'group.view': 'Ansehen', + 'group.delete': 'Löschen', + 'group.delete_confirm': '{name} löschen?', + 'group.delete_group': 'Gruppe löschen', + 'group.delete_group_confirm': '„{name}“ endgültig löschen und alle Mitglieder ' + + 'entfernen? Die Dateien auf dem Node bleiben erhalten.', + 'group.err_transport': 'Transportmodul nicht geladen', + + // Status + 'status.idle': 'Inaktiv', + 'status.discovering': 'Nodes werden gesucht …', + 'status.connecting': 'Verbindung über WebRTC …', + 'status.fetching': 'Index wird abgerufen …', + 'status.files': { + one: '{n} Datei', + other: '{n} Dateien', + }, + 'status.offline': 'Kein Node online', + 'status.error': 'Verbindung fehlgeschlagen', + + // Chat + 'chat.empty': 'Noch keine Nachrichten. Beginnen Sie das Gespräch!', + 'chat.placeholder': 'Nachricht schreiben …', + 'chat.send': 'Senden', + 'chat.attach': 'Datei anhängen', + + // Video player + 'video.loading': '{name} wird geladen …', + 'video.buffering': 'Wird gepuffert …', + 'video.close': 'Schließen (Esc)', + 'preview.pdf_fallback': 'Dieser Browser zeigt das PDF nicht direkt an. Laden Sie es ' + + 'stattdessen herunter — entschlüsselt wurde es ohnehin hier.', + 'group.upload_indexing': 'wird indiziert …', + 'video.err_transport': 'Transport nicht verbunden', + 'video.err_mse': 'Codec wird für das Streaming nicht unterstützt: {codec}', + + // Settings + 'settings.title': 'Einstellungen', + 'settings.coming_soon': 'Demnächst verfügbar.', + 'settings.downloads': 'Downloads', + 'settings.dl_auto': 'Automatisch speichern', + 'settings.dl_auto_hint': 'Dateien landen ohne Dialog direkt in dem Ordner, den Sie ' + + 'unten auswählen. Zwanzig Downloads legen dort zwanzig Dateien ab.', + 'settings.dl_ask': 'Jedes Mal fragen', + 'settings.dl_ask_hint': 'Ein „Speichern unter“-Dialog pro Datei — einer je Datei, ' + + 'auch wenn Sie eine Auswahl herunterladen.', + 'settings.dl_folder': 'Ordner: {name}', + 'settings.dl_no_folder': 'Kein Ordner ausgewählt — Downloads landen dort, wo Ihr ' + + 'Browser sie ablegt', + 'settings.dl_choose': 'Ordner auswählen', + 'settings.dl_change': 'Ändern', + 'settings.dl_forget': 'Verwerfen', + 'settings.dl_path_note': 'Einer Webseite lässt sich kein Pfad übergeben, hier ist ' + + 'also nichts einzutippen: Ihr Browser gewährt Zugriff auf den Ordner, den Sie ' + + 'auswählen, und MeshBay schreibt ausschließlich darin. Einmal pro Sitzung kann ' + + 'eine Bestätigung verlangt werden.', + 'settings.dl_unsupported': 'Dieser Browser kann nicht in einen Ordner Ihrer Wahl ' + + 'schreiben (keine File System Access API), daher landen Downloads in seinem ' + + 'eigenen Download-Ordner. Chrome und Edge lassen die Wahl zu.', + 'settings.profile': 'Profil', + 'settings.username': 'Benutzername', + 'settings.node_pins': 'Node-Identitäten', + 'settings.node_pins_hint': 'Der Identitätsschlüssel jedes Nodes wird bei der ersten ' + + 'Verbindung gemerkt. Ändert er sich, wird die Verbindung abgelehnt — das ist nur ' + + 'zu erwarten, wenn ein Betreiber einen Node neu installiert. Fragen Sie dort ' + + 'nach, bevor Sie die Einträge löschen.', + 'settings.node_pins_count': { + one: '{n} gemerkt', + other: '{n} gemerkt', + }, + 'settings.node_pins_clear': 'Gemerkte Identitäten löschen', + 'settings.danger': 'Konto löschen', + 'settings.delete_hint': 'Entfernt Ihr Konto, Ihre Gruppenmitgliedschaften und Ihre ' + + 'Benachrichtigungen vom Hub und gibt Ihren Benutzernamen frei. Was auf den Nodes ' + + 'liegt, erreicht das nicht: hochgeladene Dateien bleiben dort, wo ihr Betreiber ' + + 'sie aufbewahrt, und jeder Node behält die für Sie gemerkte Identität, bis sein ' + + 'Betreiber sie entfernt. Verbindungsprotokolle werden ein Jahr lang aufbewahrt, ' + + 'wie es das Gesetz verlangt.', + 'settings.delete_account': 'Mein Konto löschen', + 'settings.delete_confirm': 'Geben Sie zur Bestätigung Ihre Passphrase ein. ' + + 'Das lässt sich nicht rückgängig machen.', + 'settings.delete_confirm_btn': 'Endgültig löschen', + 'settings.cancel': 'Abbrechen', + 'settings.appearance': 'Darstellung', + 'settings.theme': 'Design', + 'settings.theme_light': 'Hell', + 'settings.theme_dark': 'Dunkel', + 'settings.theme_system': 'System', + 'settings.language': 'Sprache', + 'settings.about': 'Über', + 'settings.version': 'Version', + 'settings.protocol': 'Protokoll', + 'settings.role': 'Rolle', + 'settings.groups': 'Gruppenbenachrichtigungen', + 'settings.notifications': 'Benachrichtigungen', + 'settings.node_key': 'Node verknüpfen', + 'settings.node_key_desc': 'Wenn Sie einen MeshBay-Node betreiben, fügen Sie hier ' + + 'dessen öffentlichen Ed25519-Schlüssel ein, um ihn mit Ihrem Konto zu ' + + 'verknüpfen. Sie finden diesen Schlüssel im Administrations-Dashboard Ihres Nodes.', + 'settings.node_key_placeholder': 'Öffentlichen Ed25519-Schlüssel des Nodes einfügen (base64)', + 'settings.node_key_submit': 'Node verknüpfen', + 'settings.node_key_success': 'Node erfolgreich verknüpft', + 'settings.node_key_current': 'Schlüssel des verknüpften Nodes', + + // Sidebar + 'sidebar.create_group': 'Gruppe erstellen', + 'sidebar.settings': 'Einstellungen', + 'sidebar.admin': 'Verwaltung', + + // User menu + 'usermenu.language': 'Sprache', + 'usermenu.settings': 'Einstellungen', + 'usermenu.theme_light': 'Helles Design', + 'usermenu.theme_dark': 'Dunkles Design', + 'usermenu.logout': 'Abmelden', + + // Admin panel + 'admin.title': 'Verwaltung', + 'admin.tab_stats': 'Statistik', + 'admin.tab_users': 'Benutzer', + 'admin.tab_groups': 'Gruppen', + 'admin.tab_nodes': 'Nodes', + 'admin.tab_logs': 'Protokolle', + 'admin.col_observed_ip': 'Gesehen von', + 'admin.col_hint': 'Gemeldete Adresse', + 'admin.col_last_seen': 'Letzte Meldung', + 'admin.node_online': 'online', + 'admin.node_offline': 'offline', + 'admin.no_nodes': 'Keine Nodes registriert', + 'admin.nodes_hint': '„Gesehen von“ ist die Adresse, von der die Meldung tatsächlich ' + + 'eintraf, in einer mit dem Node-Schlüssel signierten Anfrage — auf diese sollten ' + + 'Sie sich bei einer Auskunft stützen. Die gemeldete Adresse ist die, für die der ' + + 'Node sich selbst hält; er hat sie von einem STUN-Server erfahren und uns ' + + 'geschickt. Sie hilft, den Node zu erreichen, und beweist nichts.', + 'admin.tab_blocklist': 'Sperrliste', + + // Admin stats + 'admin.stat_users': 'Benutzer', + 'admin.stat_groups': 'Gruppen', + 'admin.stat_nodes': 'Nodes', + 'admin.stat_online': 'Nodes online', + + // Admin users + 'admin.users_search': 'Benutzer suchen …', + 'admin.col_username': 'Benutzername', + 'admin.col_role': 'Rolle', + 'admin.col_status': 'Status', + 'admin.col_created': 'Erstellt', + 'admin.col_actions': 'Aktionen', + 'admin.col_email': 'E-Mail', + 'admin.col_groups': 'Gruppen', + 'admin.no_users': 'Keine Benutzer gefunden', + 'admin.btn_suspend': 'Sperren', + 'admin.btn_unsuspend': 'Entsperren', + 'admin.btn_details': 'Details', + 'admin.btn_delete': 'Löschen', + 'admin.delete_confirm': 'Das Konto „{user}“ löschen? Das lässt sich nicht ' + + 'rückgängig machen. Die hochgeladenen Dateien bleiben auf den Nodes, die sie ' + + 'hosten, und jeder Node behält die gemerkte Identität, bis sein Betreiber sie ' + + 'entfernt. Sperren ist die umkehrbare Option.', + 'admin.user_detail': 'Benutzerdetails', + 'admin.btn_close': 'Schließen', + 'admin.self_note': '(Sie)', + + // Admin groups + 'admin.col_name': 'Name', + 'admin.col_visibility': 'Sichtbarkeit', + 'admin.col_members': 'Mitglieder', + 'admin.no_groups': 'Keine Gruppen gefunden', + + // Admin logs + 'admin.col_time': 'Zeit', + 'admin.col_event': 'Ereignis', + 'admin.col_user': 'Benutzer', + 'admin.col_ip': 'IP', + 'admin.col_detail': 'Detail', + 'admin.filter_all': 'Alle Ereignisse', + 'admin.no_logs': 'Keine Protokolle gefunden', + 'admin.btn_load_more': 'Mehr laden', + + // Admin blocklist + 'admin.col_hash': 'Hash', + 'admin.col_reason': 'Grund', + 'admin.col_date': 'Datum', + 'admin.col_added_by': 'Hinzugefügt von', + 'admin.no_blocked': 'Keine gesperrten Hashes', + 'admin.add_hash': 'Hash zur Sperrliste hinzufügen', + 'admin.hash_placeholder': 'blake3-Hash (64 Hexadezimalzeichen)', + 'admin.reason_placeholder': 'Grund', + 'admin.btn_block': 'Sperren', + 'admin.btn_unblock': 'Entsperren', + + // Notifications + // Create group + 'create_group.title': 'Gruppe erstellen', + 'create_group.name': 'Gruppenname', + 'create_group.name_placeholder': 'z. B. Familienfotos, Projektunterlagen …', + 'create_group.visibility': 'Sichtbarkeit', + 'create_group.private': 'Privat', + 'create_group.private_desc': 'Nur eingeladene Mitglieder sehen die Dateien und greifen darauf zu', + 'create_group.public': 'Öffentlich', + 'create_group.public_desc': 'Alle können diese Gruppe finden und durchsehen', + 'create_group.join_policy': 'Beitrittsregel', + 'create_group.invite': 'Nur mit Einladung', + 'create_group.invite_desc': 'Mitglieder müssen von einem Administrator eingeladen werden', + 'create_group.open': 'Offen (alle können beitreten)', + 'create_group.open_desc': 'Alle können ohne Freigabe beitreten', + 'create_group.description': 'Beschreibung', + 'create_group.description_hint': 'Worum geht es in dieser Gruppe? (optional)', + 'create_group.submit': 'Erstellen', + 'create_group.hint': 'Eine Gruppe braucht einen Node, der die Dateien hostet. Sie ' + + 'können die Gruppe jetzt erstellen und später einen Node verbinden.', + 'create_group.creating': 'Wird erstellt …', + + // Members + 'members.col_role': 'Rolle', + 'members.group_role': 'Rolle in der Gruppe', + 'members.admin': 'Administrator', + 'members.group_admin': 'Gruppenadministrator', + 'members.owner': 'Eigentümer', + 'members.member': 'Mitglied', + 'members.invite_title': 'Mitglied einladen', + 'members.username_placeholder': 'Benutzername', + 'members.invite_btn': 'Einladen', + 'members.invite_needs_pairing': 'Einladungen werden mit dem Schlüssel signiert, den ' + + 'dieser Node für Ihren Browser gemerkt hat. Koppeln Sie ihn daher unten, bevor ' + + 'Sie jemanden einladen.', + 'members.invite_ask_operator': 'Nur der Betreiber des Nodes, der diese Gruppe ' + + 'hostet, kann einladen — aus einem mit ihm gekoppelten Browser.', + 'members.remove': 'Entfernen', + 'members.remove_confirm': '{user} aus dieser Gruppe entfernen? Das Konto, die ' + + 'übrigen Gruppen und die hochgeladenen Dateien bleiben unberührt.', + 'members.remove_hint': 'Wer entfernt wird, verliert die Mitgliedschaft, und der Node ' + + 'gibt ihm den Gruppenschlüssel nicht mehr heraus. Das Konto wird dadurch nicht ' + + 'gelöscht, und der bereits erhaltene Schlüssel lässt sich nicht zurückholen — ' + + 'wechseln Sie ihn auf dem Node (meshbay-node gek-init), wenn das wichtig ist.', + 'members.pair_title': 'Diesen Browser mit Ihrem Node koppeln', + 'members.pair_hint': 'Ihr Node akzeptiert Betreiberaktionen — Einladungen, Löschen ' + + 'von Dateien — nur aus einem Browser, mit dem er gekoppelt wurde. Führen Sie auf ' + + 'dem Node `meshbay-node operator pair` aus und geben Sie den Code hier ein. Der ' + + 'Code läuft nie über den Hub.', + 'members.pair_btn': 'Koppeln', + 'members.pair_success': 'Dieser Browser ist jetzt mit dem Node gekoppelt.', + 'members.invite_code_ready': 'Einladungscode für {user} — schicken Sie ihn auf dem ' + + 'Weg, den Sie ohnehin nutzen. Er funktioniert einmal und läuft nie über den Hub.', + 'members.invite_code_hint': 'Die eingeladene Person gibt ihn ein, wenn sie diese ' + + 'Gruppe zum ersten Mal öffnet. Sie müssen dann nicht online sein.', + 'transfers.title': 'Übertragungen', + 'transfers.clear': 'Abgeschlossene entfernen', + 'transfers.cancel': 'Abbrechen', + 'transfers.open': 'Öffnen', + 'transfers.done': 'Abgeschlossen', + 'transfers.cancelled': 'Abgebrochen', + 'transfers.failed': 'Fehlgeschlagen', + 'group.select': 'Auswählen', + 'group.select_done': 'Fertig', + 'group.actions': 'Aktionen ({n})', + 'group.download_n': 'Herunterladen ({n})', + 'group.download_zip_n': 'Ordner als ZIP herunterladen ({n})', + 'group.delete_n': 'Löschen ({n})', + 'group.delete_n_confirm': { + one: '{n} Element löschen? {names}', + other: '{n} Elemente löschen? {names}', + }, + 'group.download_zip': { + one: 'Als ZIP herunterladen ({n} Datei)', + other: 'Als ZIP herunterladen ({n} Dateien)', + }, + 'group.zip_empty': 'In diesem Ordner gibt es nichts herunterzuladen.', + 'group.zip_no_stream': 'Dieser Browser kann einen Download nicht direkt auf die ' + + 'Festplatte schreiben, daher muss {name} ({size}) zuerst im Arbeitsspeicher ' + + 'aufgebaut werden. Bei einem großen Archiv kann das fehlschlagen. Fortfahren?', + 'group.rmdir': 'Ordner löschen', + 'group.rmdir_confirm': 'Den Ordner „{name}“ löschen? Er muss leer sein.', + 'group.rmdir_not_empty': 'Dieser Ordner ist nicht leer. Löschen Sie zuerst seinen ' + + 'Inhalt — nichts hier entfernt Dateien, die Sie nicht sehen.', + 'group.desc_edit': 'Beschreibung bearbeiten', + 'group.desc_add': 'Beschreibung hinzufügen', + 'group.desc_save': 'Speichern', + 'group.desc_cancel': 'Abbrechen', + 'group.desc_placeholder': 'Wofür diese Gruppe da ist — Mitglieder sehen es auf ihrer ' + + 'Startseite.', + 'group.join_code_title': 'Dieser Node muss Sie erkennen', + 'group.join_code_hint': 'Fragen Sie die Person, die Sie eingeladen hat, nach dem ' + + 'Einmalcode und geben Sie ihn hier ein. Danach ist dieser Browser bekannt und Sie ' + + 'werden nicht erneut gefragt.', + 'group.join_code_btn': 'Beitreten', + + 'notif.purge': 'Alle löschen', + 'notif.title': 'Benachrichtigungen', + 'notif.empty': 'Keine Benachrichtigungen', + 'notif.mark_all_read': 'Alle als gelesen markieren', + + // Sidebar + 'sidebar.search': 'Dateien suchen', + + // Status + + // Search + 'search.title': 'Dateien suchen', + 'search.placeholder': 'In allen Gruppen suchen …', + 'search.no_results': 'Keine Datei entspricht Ihrer Suche.', + 'search.synced': 'synchronisiert {ago}', + 'search.cache_note': 'Die Suche stützt sich auf den Stand, den jede Gruppe beim ' + + 'letzten Öffnen hatte. Öffnen Sie eine Gruppe, um ihre Dateien hier zu ' + + 'aktualisieren.', + 'search.col_group': 'Gruppe', + 'search.result_count': { + one: '{n} Datei gefunden', + other: '{n} Dateien gefunden', + }, + 'search.hint': 'Durchsucht Dateinamen in allen zwischengespeicherten Gruppenindexen.', +}; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js new file mode 100644 index 0000000..0e4427d --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -0,0 +1,392 @@ +/** + * English — the source catalogue. + * + * Every other locale is a translation of this file and carries the same keys. + * A value may be a string, or an object of Intl.PluralRules categories + * (`one`, `few`, `many`, `other`) when it is rendered with a count. + * + * Product vocabulary stays in English everywhere: `hub`, `node` and `GEK` name + * the CLI (`meshbay-node`), node.toml and the docs, so translating them would + * cut the interface off from everything an operator reads and types. + */ + +export default { + // 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': { + one: 'Use at least {n} character', + other: '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.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.mkdir': 'New folder', + 'group.mkdir_prompt': 'Name of the new folder:', + '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': { + one: '{n} file', + other: '{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)', + 'preview.pdf_fallback': 'This browser will not display the PDF inline. Download it instead — it was decrypted here either way.', + 'group.upload_indexing': 'indexing…', + '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.downloads': 'Downloads', + 'settings.dl_auto': 'Save automatically', + 'settings.dl_auto_hint': 'Files go straight into the folder you choose below, ' + + 'with no dialog. Downloading twenty files puts twenty files there.', + 'settings.dl_ask': 'Ask every time', + 'settings.dl_ask_hint': 'A Save As dialog for each file — one per file, ' + + 'including when you download a selection.', + 'settings.dl_folder': 'Folder: {name}', + 'settings.dl_no_folder': 'No folder chosen — downloads go wherever your browser ' + + 'puts them', + 'settings.dl_choose': 'Choose folder', + 'settings.dl_change': 'Change', + 'settings.dl_forget': 'Forget', + 'settings.dl_path_note': 'A web page cannot be given a path, so there is nothing ' + + 'to type here: your browser grants access to the folder you pick, and MeshBay ' + + 'only ever writes inside it. You may be asked to confirm once per session.', + 'settings.dl_unsupported': 'This browser cannot write into a folder of your ' + + 'choosing (no File System Access API), so downloads go to its own download ' + + 'folder. Chrome and Edge support choosing one.', + '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': { + one: '{n} pinned', + other: '{n} pinned', + }, + 'settings.node_pins_clear': 'Clear pinned identities', + 'settings.danger': 'Delete account', + 'settings.delete_hint': 'Removes your account, your group memberships and your ' + + 'notifications from the hub, and frees your username. It cannot reach what ' + + 'lives on nodes: files you uploaded stay where their operator keeps them, ' + + 'and each node keeps the identity it pinned for you until its operator ' + + 'unpins it. Connection logs are kept for a year, as the law requires.', + 'settings.delete_account': 'Delete my account', + 'settings.delete_confirm': 'Enter your passphrase to confirm. This cannot be undone.', + 'settings.delete_confirm_btn': 'Delete for good', + 'settings.cancel': 'Cancel', + '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_nodes': 'Nodes', + 'admin.tab_logs': 'Logs', + 'admin.col_observed_ip': 'Seen from', + 'admin.col_hint': 'Announced hint', + 'admin.col_last_seen': 'Last announce', + 'admin.node_online': 'online', + 'admin.node_offline': 'offline', + 'admin.no_nodes': 'No nodes registered', + 'admin.nodes_hint': '"Seen from" is the address the announcement arrived from, ' + + 'on a request signed with the node key — that is the one to answer a ' + + 'question with. The hint is what the node believes its own address to be, ' + + 'learned from a STUN server and sent to us; it is useful for reaching the ' + + 'node and is not evidence of anything.', + '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.btn_delete': 'Delete', + 'admin.delete_confirm': 'Delete the account "{user}"? This cannot be undone. ' + + 'Files they uploaded stay on the nodes that host them, and each node keeps ' + + 'the identity it pinned until its operator unpins it. Suspending is the ' + + 'reversible option.', + '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.name_placeholder': 'e.g. Family Photos, Project Files...', + 'create_group.visibility': 'Visibility', + 'create_group.private': 'Private', + 'create_group.private_desc': 'Only invited members can see and access files', + 'create_group.public': 'Public', + 'create_group.public_desc': 'Anyone can discover and browse this group', + 'create_group.join_policy': 'Join policy', + 'create_group.invite': 'Invite only', + 'create_group.invite_desc': 'Members must be invited by an admin', + 'create_group.open': 'Open (anyone can join)', + 'create_group.open_desc': 'Anyone can join without approval', + '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.invite_needs_pairing': 'Invitations are signed with the key this node ' + + 'pinned for your browser, so pair it below before inviting anyone.', + 'members.invite_ask_operator': 'Only the operator of the node hosting this group can ' + + 'invite, from a browser paired with it.', + 'members.remove': 'Remove', + 'members.remove_confirm': 'Remove {user} from this group? Their account, their ' + + 'other groups and the files they uploaded are not affected.', + 'members.remove_hint': 'Removing someone takes away their membership and stops ' + + 'the node serving them the group key. It does not delete their account, and ' + + 'it does not take back the key they already hold — rotate it on the node ' + + '(meshbay-node gek-init) if that matters.', + '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.', + '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.', + 'transfers.title': 'Transfers', + 'transfers.clear': 'Clear finished', + 'transfers.cancel': 'Cancel', + 'transfers.open': 'Open', + 'transfers.done': 'Finished', + 'transfers.cancelled': 'Cancelled', + 'transfers.failed': 'Failed', + 'group.select': 'Select', + 'group.select_done': 'Done', + 'group.actions': 'Actions ({n})', + 'group.download_n': 'Download ({n})', + 'group.download_zip_n': 'Download folders as zip ({n})', + 'group.delete_n': 'Delete ({n})', + 'group.delete_n_confirm': { + one: 'Delete {n} item? {names}', + other: 'Delete {n} items? {names}', + }, + 'group.download_zip': { + one: 'Download as zip ({n} file)', + other: 'Download as zip ({n} files)', + }, + 'group.zip_empty': 'That folder has nothing in it to download.', + 'group.zip_no_stream': 'This browser cannot write a download straight to disk, ' + + 'so {name} ({size}) has to be built in memory first. On a large archive ' + + 'that may fail. Continue?', + 'group.rmdir': 'Delete folder', + 'group.rmdir_confirm': 'Delete the folder "{name}"? It must be empty.', + 'group.rmdir_not_empty': 'That folder is not empty. Delete what is in it first — ' + + 'nothing here removes files you cannot see.', + 'group.desc_edit': 'Edit description', + 'group.desc_add': 'Add a description', + 'group.desc_save': 'Save', + 'group.desc_cancel': 'Cancel', + 'group.desc_placeholder': 'What this group is for — members see it on their home page.', + '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.purge': 'Clear all', + 'notif.title': 'Notifications', + 'notif.empty': 'No notifications', + 'notif.mark_all_read': 'Mark all read', + + // Sidebar + 'sidebar.search': 'Search files', + + // Status + + // Search + 'search.title': 'Search Files', + 'search.placeholder': 'Search across all groups...', + 'search.no_results': 'No files match your search.', + 'search.synced': 'synced {ago}', + 'search.cache_note': 'Search reads what each group looked like the last time you ' + + 'opened it. Open a group to bring its files up to date here.', + 'search.col_group': 'Group', + 'search.result_count': { + one: '{n} file found', + other: '{n} files found', + }, + 'search.hint': 'Search file names across all cached group indexes.', +}; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js new file mode 100644 index 0000000..c09175b --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -0,0 +1,402 @@ +/** + * Spanish — translated from locales/en.js. + * + * Register: formal (usted). + * `hub`, `node` and `GEK` are left in English: they name the CLI + * (`meshbay-node`), node.toml and the docs, which are English throughout. + */ + +export default { + // Nav + 'nav.toggle_menu': 'Mostrar el menú', + 'nav.light_mode': 'Tema claro', + 'nav.dark_mode': 'Tema oscuro', + 'nav.logout': 'Cerrar sesión', + 'nav.login': 'Iniciar sesión', + + // Sidebar + 'sidebar.my_groups': 'Mis grupos', + 'sidebar.no_groups': 'Aún no hay grupos', + 'sidebar.discover': 'Descubrir', + 'sidebar.public_groups': 'Grupos públicos', + + // Login + 'login.title': 'Iniciar sesión', + 'login.username': 'Nombre de usuario', + 'login.password': 'Contraseña', + 'login.submit': 'Iniciar sesión', + 'login.loading': 'Iniciando sesión...', + 'login.no_account': '¿No tiene cuenta?', + 'login.register_link': 'Crear una cuenta', + + // Register + 'register.title': 'Crear una cuenta', + 'register.username': 'Nombre de usuario', + 'register.email': 'Correo electrónico', + 'register.password': 'Frase de contraseña (mínimo 12 caracteres)', + 'register.confirm': 'Confirmar contraseña', + 'register.submit': 'Crear la cuenta', + 'register.loading': 'Creando la cuenta...', + 'register.has_account': '¿Ya tiene una cuenta?', + 'register.login_link': 'Iniciar sesión', + 'register.success_title': 'Cuenta creada', + 'register.success_msg': 'Ya puede iniciar sesión con sus credenciales.', + 'register.go_login': 'Ir al inicio de sesión', + 'register.err_mismatch': 'Las contraseñas no coinciden', + 'register.err_min_len': { + one: 'Use al menos {n} carácter', + other: 'Use al menos {n} caracteres', + }, + 'register.err_too_weak': 'Demasiado fácil de adivinar. Su frase de contraseña es lo ' + + 'que protege sus claves allí donde están almacenadas — unas cuantas palabras sin ' + + 'relación entre sí funcionan muy bien.', + 'register.strength': 'Solidez: unos {bits} bits. Protege la copia de sus claves que ' + + 'se guarda en los nodes a los que se une, así que conviene acertar.', + + // Home + 'home.welcome': 'Bienvenido a MeshBay', + 'home.no_groups': 'Todavía no es miembro de ningún grupo.', + 'home.browse_prefix': 'Explore los ', + 'home.browse_link': 'grupos públicos', + 'home.browse_suffix': ' o pida a un administrador de grupo que le invite.', + 'home.my_groups': 'Mis grupos', + + // Explore + 'explore.title': 'Grupos públicos', + 'explore.search': 'Buscar grupos...', + 'explore.loading': 'Cargando...', + 'explore.empty': 'No hay grupos públicos disponibles.', + 'explore.create_group': 'Crear un grupo', + 'explore.join': 'Unirse', + 'explore.member': 'Miembro', + + // Group page + 'group.default_name': 'Grupo', + 'group.tab_files': 'Archivos', + 'group.tab_chat': 'Chat', + 'group.tab_members': 'Miembros', + 'group.filter': 'Filtrar archivos...', + 'group.col_name': 'Nombre', + 'group.col_size': 'Tamaño', + 'group.col_type': 'Tipo', + 'group.col_date': 'Fecha', + 'group.empty_filter': 'Ningún archivo coincide con el filtro', + 'group.empty_dir': 'Esta carpeta está vacía', + 'group.download': 'Descargar', + 'group.play': 'Reproducir', + 'group.offline_title': 'Ningún node está en línea para este grupo en este momento.', + 'group.offline_hint': 'Los archivos aparecerán cuando se conecte un node que aloje ' + + 'este grupo.', + 'group.upload': 'Subir', + 'group.mkdir': 'Nueva carpeta', + 'group.mkdir_prompt': 'Nombre de la nueva carpeta:', + 'group.view': 'Ver', + 'group.delete': 'Eliminar', + 'group.delete_confirm': '¿Eliminar {name}?', + 'group.delete_group': 'Eliminar el grupo', + 'group.delete_group_confirm': '¿Eliminar «{name}» de forma permanente y expulsar a ' + + 'todos los miembros? Los archivos que están en el node se conservan.', + 'group.err_transport': 'Módulo de transporte no cargado', + + // Status + 'status.idle': 'Inactivo', + 'status.discovering': 'Buscando nodes...', + 'status.connecting': 'Conectando por WebRTC...', + 'status.fetching': 'Obteniendo el índice...', + 'status.files': { + one: '{n} archivo', + other: '{n} archivos', + }, + 'status.offline': 'Ningún node en línea', + 'status.error': 'Error de conexión', + + // Chat + 'chat.empty': 'Todavía no hay mensajes. ¡Empiece la conversación!', + 'chat.placeholder': 'Escriba un mensaje...', + 'chat.send': 'Enviar', + 'chat.attach': 'Adjuntar archivo', + + // Video player + 'video.loading': 'Cargando {name}...', + 'video.buffering': 'Almacenando en búfer...', + 'video.close': 'Cerrar (Esc)', + 'preview.pdf_fallback': 'Este navegador no mostrará el PDF integrado. Descárguelo ' + + 'en su lugar — en cualquier caso se descifró aquí.', + 'group.upload_indexing': 'indexando…', + 'video.err_transport': 'Transporte no conectado', + 'video.err_mse': 'Códec no compatible con la reproducción en continuo: {codec}', + + // Settings + 'settings.title': 'Ajustes', + 'settings.coming_soon': 'Próximamente.', + 'settings.downloads': 'Descargas', + 'settings.dl_auto': 'Guardar automáticamente', + 'settings.dl_auto_hint': 'Los archivos van directamente a la carpeta que elija más ' + + 'abajo, sin ningún diálogo. Descargar veinte archivos deja allí veinte archivos.', + 'settings.dl_ask': 'Preguntar cada vez', + 'settings.dl_ask_hint': 'Un diálogo «Guardar como» por archivo — uno por archivo, ' + + 'también cuando descarga una selección.', + 'settings.dl_folder': 'Carpeta: {name}', + 'settings.dl_no_folder': 'Ninguna carpeta elegida — las descargas van adonde las ' + + 'ponga su navegador', + 'settings.dl_choose': 'Elegir carpeta', + 'settings.dl_change': 'Cambiar', + 'settings.dl_forget': 'Olvidar', + 'settings.dl_path_note': 'A una página web no se le puede indicar una ruta, así que ' + + 'aquí no hay nada que escribir: su navegador concede acceso a la carpeta que ' + + 'usted señale, y MeshBay solo escribe dentro de ella. Es posible que se le pida ' + + 'confirmación una vez por sesión.', + 'settings.dl_unsupported': 'Este navegador no puede escribir en una carpeta de su ' + + 'elección (no tiene File System Access API), de modo que las descargas van a su ' + + 'propia carpeta de descargas. Chrome y Edge sí permiten elegir una.', + 'settings.profile': 'Perfil', + 'settings.username': 'Nombre de usuario', + 'settings.node_pins': 'Identidades de los nodes', + 'settings.node_pins_hint': 'La clave de identidad de cada node se memoriza la ' + + 'primera vez que se conecta. Si cambia, la conexión se rechaza — algo esperable ' + + 'solo cuando un operador reinstala un node. Confírmelo con él antes de borrarla.', + 'settings.node_pins_count': { + one: '{n} fijada', + other: '{n} fijadas', + }, + 'settings.node_pins_clear': 'Borrar las identidades fijadas', + 'settings.danger': 'Eliminar la cuenta', + 'settings.delete_hint': 'Elimina su cuenta, sus pertenencias a grupos y sus ' + + 'notificaciones del hub, y libera su nombre de usuario. No alcanza lo que vive ' + + 'en los nodes: los archivos que subió se quedan donde los guarda su operador, y ' + + 'cada node conserva la identidad que fijó para usted hasta que su operador la ' + + 'retire. Los registros de conexión se guardan un año, como exige la ley.', + 'settings.delete_account': 'Eliminar mi cuenta', + 'settings.delete_confirm': 'Escriba su frase de contraseña para confirmar. ' + + 'Esta acción no se puede deshacer.', + 'settings.delete_confirm_btn': 'Eliminar definitivamente', + 'settings.cancel': 'Cancelar', + 'settings.appearance': 'Apariencia', + 'settings.theme': 'Tema', + 'settings.theme_light': 'Claro', + 'settings.theme_dark': 'Oscuro', + 'settings.theme_system': 'Sistema', + 'settings.language': 'Idioma', + 'settings.about': 'Acerca de', + 'settings.version': 'Versión', + 'settings.protocol': 'Protocolo', + 'settings.role': 'Rol', + 'settings.groups': 'Notificaciones de grupo', + 'settings.notifications': 'Notificaciones', + 'settings.node_key': 'Vincular un node', + 'settings.node_key_desc': 'Si administra un node MeshBay, pegue aquí su clave ' + + 'pública Ed25519 para vincularlo a su cuenta. Encontrará esa clave en el panel ' + + 'de administración de su node.', + 'settings.node_key_placeholder': 'Pegue la clave pública Ed25519 del node (base64)', + 'settings.node_key_submit': 'Vincular el node', + 'settings.node_key_success': 'Node vinculado correctamente', + 'settings.node_key_current': 'Clave del node vinculado', + + // Sidebar + 'sidebar.create_group': 'Crear un grupo', + 'sidebar.settings': 'Ajustes', + 'sidebar.admin': 'Administración', + + // User menu + 'usermenu.language': 'Idioma', + 'usermenu.settings': 'Ajustes', + 'usermenu.theme_light': 'Tema claro', + 'usermenu.theme_dark': 'Tema oscuro', + 'usermenu.logout': 'Cerrar sesión', + + // Admin panel + 'admin.title': 'Administración', + 'admin.tab_stats': 'Estadísticas', + 'admin.tab_users': 'Usuarios', + 'admin.tab_groups': 'Grupos', + 'admin.tab_nodes': 'Nodes', + 'admin.tab_logs': 'Registros', + 'admin.col_observed_ip': 'Visto desde', + 'admin.col_hint': 'Dirección anunciada', + 'admin.col_last_seen': 'Último anuncio', + 'admin.node_online': 'en línea', + 'admin.node_offline': 'desconectado', + 'admin.no_nodes': 'Ningún node registrado', + 'admin.nodes_hint': '«Visto desde» es la dirección desde la que llegó el anuncio, ' + + 'en una petición firmada con la clave del node — esa es la que sirve para ' + + 'responder a un requerimiento. La dirección anunciada es la que el node cree que ' + + 'es la suya, aprendida de un servidor STUN y enviada aquí; sirve para alcanzar el ' + + 'node y no prueba nada.', + 'admin.tab_blocklist': 'Lista de bloqueo', + + // Admin stats + 'admin.stat_users': 'Usuarios', + 'admin.stat_groups': 'Grupos', + 'admin.stat_nodes': 'Nodes', + 'admin.stat_online': 'Nodes en línea', + + // Admin users + 'admin.users_search': 'Buscar usuarios...', + 'admin.col_username': 'Nombre de usuario', + 'admin.col_role': 'Rol', + 'admin.col_status': 'Estado', + 'admin.col_created': 'Creación', + 'admin.col_actions': 'Acciones', + 'admin.col_email': 'Correo electrónico', + 'admin.col_groups': 'Grupos', + 'admin.no_users': 'No se ha encontrado ningún usuario', + 'admin.btn_suspend': 'Suspender', + 'admin.btn_unsuspend': 'Reactivar', + 'admin.btn_details': 'Detalles', + 'admin.btn_delete': 'Eliminar', + 'admin.delete_confirm': '¿Eliminar la cuenta «{user}»? Esta acción no se puede ' + + 'deshacer. Los archivos que subió permanecen en los nodes que los alojan, y cada ' + + 'node conserva la identidad que fijó hasta que su operador la retire. La ' + + 'suspensión es la opción reversible.', + 'admin.user_detail': 'Detalles del usuario', + 'admin.btn_close': 'Cerrar', + 'admin.self_note': '(usted)', + + // Admin groups + 'admin.col_name': 'Nombre', + 'admin.col_visibility': 'Visibilidad', + 'admin.col_members': 'Miembros', + 'admin.no_groups': 'No se ha encontrado ningún grupo', + + // Admin logs + 'admin.col_time': 'Hora', + 'admin.col_event': 'Evento', + 'admin.col_user': 'Usuario', + 'admin.col_ip': 'IP', + 'admin.col_detail': 'Detalle', + 'admin.filter_all': 'Todos los eventos', + 'admin.no_logs': 'No se ha encontrado ningún registro', + 'admin.btn_load_more': 'Cargar más', + + // Admin blocklist + 'admin.col_hash': 'Huella', + 'admin.col_reason': 'Motivo', + 'admin.col_date': 'Fecha', + 'admin.col_added_by': 'Añadida por', + 'admin.no_blocked': 'Ninguna huella bloqueada', + 'admin.add_hash': 'Añadir una huella a la lista de bloqueo', + 'admin.hash_placeholder': 'huella blake3 (64 caracteres hexadecimales)', + 'admin.reason_placeholder': 'Motivo', + 'admin.btn_block': 'Bloquear', + 'admin.btn_unblock': 'Desbloquear', + + // Notifications + // Create group + 'create_group.title': 'Crear un grupo', + 'create_group.name': 'Nombre del grupo', + 'create_group.name_placeholder': 'p. ej. Fotos de familia, Archivos del proyecto...', + 'create_group.visibility': 'Visibilidad', + 'create_group.private': 'Privado', + 'create_group.private_desc': 'Solo los miembros invitados ven los archivos y acceden a ellos', + 'create_group.public': 'Público', + 'create_group.public_desc': 'Cualquiera puede descubrir y explorar este grupo', + 'create_group.join_policy': 'Política de acceso', + 'create_group.invite': 'Solo por invitación', + 'create_group.invite_desc': 'Los miembros deben ser invitados por un administrador', + 'create_group.open': 'Abierto (cualquiera puede unirse)', + 'create_group.open_desc': 'Cualquiera puede unirse sin aprobación', + 'create_group.description': 'Descripción', + 'create_group.description_hint': '¿De qué trata este grupo? (opcional)', + 'create_group.submit': 'Crear', + 'create_group.hint': 'Un grupo necesita un node para alojar los archivos. Puede ' + + 'crear el grupo ahora y conectar un node más adelante.', + 'create_group.creating': 'Creando...', + + // Members + 'members.col_role': 'Rol', + 'members.group_role': 'Rol en el grupo', + 'members.admin': 'Administrador', + 'members.group_admin': 'Administrador del grupo', + 'members.owner': 'Propietario', + 'members.member': 'Miembro', + 'members.invite_title': 'Invitar a un miembro', + 'members.username_placeholder': 'Nombre de usuario', + 'members.invite_btn': 'Invitar', + 'members.invite_needs_pairing': 'Las invitaciones se firman con la clave que este ' + + 'node fijó para su navegador, así que empareje el navegador abajo antes de ' + + 'invitar a nadie.', + 'members.invite_ask_operator': 'Solo el operador del node que aloja este grupo puede ' + + 'invitar, desde un navegador emparejado con él.', + 'members.remove': 'Expulsar', + 'members.remove_confirm': '¿Expulsar a {user} de este grupo? Su cuenta, sus otros ' + + 'grupos y los archivos que subió no se ven afectados.', + 'members.remove_hint': 'Expulsar a alguien le quita la pertenencia al grupo e impide ' + + 'que el node le entregue la clave del grupo. No elimina su cuenta, y no recupera ' + + 'la clave que ya tiene en su poder — rótela en el node (meshbay-node gek-init) si ' + + 'eso importa.', + 'members.pair_title': 'Emparejar este navegador con su node', + 'members.pair_hint': 'Su node solo acepta acciones de operador — invitaciones, ' + + 'eliminación de archivos — desde un navegador con el que se haya emparejado. ' + + 'Ejecute `meshbay-node operator pair` en el node y escriba aquí el código. El ' + + 'código nunca pasa por el hub.', + 'members.pair_btn': 'Emparejar', + 'members.pair_success': 'Este navegador ya está emparejado con el node.', + 'members.invite_code_ready': 'Código de invitación para {user} — envíeselo por el ' + + 'medio que usen habitualmente. Funciona una sola vez y nunca pasa por el hub.', + 'members.invite_code_hint': 'Lo introduce la primera vez que abre este grupo. No ' + + 'necesita que usted esté en línea en ese momento.', + 'transfers.title': 'Transferencias', + 'transfers.clear': 'Borrar las terminadas', + 'transfers.cancel': 'Cancelar', + 'transfers.open': 'Abrir', + 'transfers.done': 'Terminada', + 'transfers.cancelled': 'Cancelada', + 'transfers.failed': 'Fallida', + 'group.select': 'Seleccionar', + 'group.select_done': 'Listo', + 'group.actions': 'Acciones ({n})', + 'group.download_n': 'Descargar ({n})', + 'group.download_zip_n': 'Descargar las carpetas en zip ({n})', + 'group.delete_n': 'Eliminar ({n})', + 'group.delete_n_confirm': { + one: '¿Eliminar {n} elemento? {names}', + other: '¿Eliminar {n} elementos? {names}', + }, + 'group.download_zip': { + one: 'Descargar en zip ({n} archivo)', + other: 'Descargar en zip ({n} archivos)', + }, + 'group.zip_empty': 'Esa carpeta no contiene nada que descargar.', + 'group.zip_no_stream': 'Este navegador no puede escribir una descarga directamente ' + + 'en el disco, así que {name} ({size}) tiene que construirse antes en memoria. Con ' + + 'un archivo comprimido grande eso puede fallar. ¿Continuar?', + 'group.rmdir': 'Eliminar la carpeta', + 'group.rmdir_confirm': '¿Eliminar la carpeta «{name}»? Debe estar vacía.', + 'group.rmdir_not_empty': 'Esa carpeta no está vacía. Elimine antes lo que contiene — ' + + 'aquí nada borra archivos que usted no ve.', + 'group.desc_edit': 'Editar la descripción', + 'group.desc_add': 'Añadir una descripción', + 'group.desc_save': 'Guardar', + 'group.desc_cancel': 'Cancelar', + 'group.desc_placeholder': 'Para qué sirve este grupo — los miembros lo ven en su ' + + 'página de inicio.', + 'group.join_code_title': 'Este node necesita reconocerle', + 'group.join_code_hint': 'Pida el código de un solo uso a quien le invitó e ' + + 'introdúzcalo aquí. Después, este navegador queda reconocido y no se le volverá a ' + + 'preguntar.', + 'group.join_code_btn': 'Unirse', + + 'notif.purge': 'Borrar todo', + 'notif.title': 'Notificaciones', + 'notif.empty': 'Ninguna notificación', + 'notif.mark_all_read': 'Marcar todo como leído', + + // Sidebar + 'sidebar.search': 'Buscar archivos', + + // Status + + // Search + 'search.title': 'Buscar archivos', + 'search.placeholder': 'Buscar en todos los grupos...', + 'search.no_results': 'Ningún archivo coincide con su búsqueda.', + 'search.synced': 'sincronizado {ago}', + 'search.cache_note': 'La búsqueda usa el estado que tenía cada grupo la última vez ' + + 'que lo abrió. Abra un grupo para poner sus archivos al día aquí.', + 'search.col_group': 'Grupo', + 'search.result_count': { + one: '{n} archivo encontrado', + other: '{n} archivos encontrados', + }, + 'search.hint': 'Busca nombres de archivo en todos los índices de grupo en caché.', +}; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js new file mode 100644 index 0000000..845e7ce --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -0,0 +1,407 @@ +/** + * French — translated from locales/en.js. + * + * Register: formal (vouvoiement). + * `hub`, `node` and `GEK` are left in English: they name the CLI + * (`meshbay-node`), node.toml and the docs, which are English throughout. + */ + +export default { + // Nav + 'nav.toggle_menu': 'Afficher le menu', + 'nav.light_mode': 'Thème clair', + 'nav.dark_mode': 'Thème sombre', + 'nav.logout': 'Déconnexion', + 'nav.login': 'Connexion', + + // Sidebar + 'sidebar.my_groups': 'Mes groupes', + 'sidebar.no_groups': 'Aucun groupe pour le moment', + 'sidebar.discover': 'Découvrir', + 'sidebar.public_groups': 'Groupes publics', + + // Login + 'login.title': 'Connexion', + 'login.username': "Nom d'utilisateur", + 'login.password': 'Mot de passe', + 'login.submit': 'Se connecter', + 'login.loading': 'Connexion en cours...', + 'login.no_account': 'Pas encore de compte ?', + 'login.register_link': 'Créer un compte', + + // Register + 'register.title': 'Créer un compte', + 'register.username': "Nom d'utilisateur", + 'register.email': 'Adresse e-mail', + 'register.password': 'Phrase secrète (12 caractères minimum)', + 'register.confirm': 'Confirmer le mot de passe', + 'register.submit': 'Créer le compte', + 'register.loading': 'Création du compte...', + 'register.has_account': 'Vous avez déjà un compte ?', + 'register.login_link': 'Se connecter', + 'register.success_title': 'Compte créé', + 'register.success_msg': 'Vous pouvez maintenant vous connecter avec vos identifiants.', + 'register.go_login': 'Aller à la connexion', + 'register.err_mismatch': 'Les mots de passe ne correspondent pas', + 'register.err_min_len': { + one: 'Utilisez au moins {n} caractère', + other: 'Utilisez au moins {n} caractères', + }, + 'register.err_too_weak': 'Trop facile à deviner. Votre phrase secrète est ce qui ' + + 'protège vos clés là où elles sont stockées — quelques mots sans rapport entre ' + + 'eux font très bien l’affaire.', + 'register.strength': 'Robustesse : environ {bits} bits. Elle protège la copie de ' + + 'vos clés conservée sur les nodes que vous rejoignez, cela vaut donc la peine ' + + 'de la soigner.', + + // Home + 'home.welcome': 'Bienvenue sur MeshBay', + 'home.no_groups': "Vous n'êtes membre d'aucun groupe pour le moment.", + 'home.browse_prefix': 'Parcourez les ', + 'home.browse_link': 'groupes publics', + 'home.browse_suffix': " ou demandez à l'administrateur d'un groupe de vous inviter.", + 'home.my_groups': 'Mes groupes', + + // Explore + 'explore.title': 'Groupes publics', + 'explore.search': 'Rechercher un groupe...', + 'explore.loading': 'Chargement...', + 'explore.empty': 'Aucun groupe public disponible.', + 'explore.create_group': 'Créer un groupe', + 'explore.join': 'Rejoindre', + 'explore.member': 'Membre', + + // Group page + 'group.default_name': 'Groupe', + 'group.tab_files': 'Fichiers', + 'group.tab_chat': 'Discussion', + 'group.tab_members': 'Membres', + 'group.filter': 'Filtrer les fichiers...', + 'group.col_name': 'Nom', + 'group.col_size': 'Taille', + 'group.col_type': 'Type', + 'group.col_date': 'Date', + 'group.empty_filter': 'Aucun fichier ne correspond au filtre', + 'group.empty_dir': 'Ce dossier est vide', + 'group.download': 'Télécharger', + 'group.play': 'Lire', + 'group.offline_title': "Aucun node n'est actuellement en ligne pour ce groupe.", + 'group.offline_hint': 'Les fichiers apparaîtront dès qu’un node hébergeant ce ' + + 'groupe se connectera.', + 'group.upload': 'Envoyer', + 'group.mkdir': 'Nouveau dossier', + 'group.mkdir_prompt': 'Nom du nouveau dossier :', + 'group.view': 'Afficher', + 'group.delete': 'Supprimer', + 'group.delete_confirm': 'Supprimer {name} ?', + 'group.delete_group': 'Supprimer le groupe', + 'group.delete_group_confirm': 'Supprimer définitivement « {name} » et en retirer ' + + 'tous les membres ? Les fichiers présents sur le node sont conservés.', + 'group.err_transport': 'Module de transport non chargé', + + // Status + 'status.idle': 'Inactif', + 'status.discovering': 'Recherche de nodes...', + 'status.connecting': 'Connexion via WebRTC...', + 'status.fetching': "Récupération de l'index...", + 'status.files': { + one: '{n} fichier', + other: '{n} fichiers', + }, + 'status.offline': 'Aucun node en ligne', + 'status.error': 'Échec de la connexion', + + // Chat + 'chat.empty': 'Aucun message pour le moment. Lancez la conversation !', + 'chat.placeholder': 'Écrivez un message...', + 'chat.send': 'Envoyer', + 'chat.attach': 'Joindre un fichier', + + // Video player + 'video.loading': 'Chargement de {name}...', + 'video.buffering': 'Mise en mémoire tampon...', + 'video.close': 'Fermer (Échap)', + 'preview.pdf_fallback': 'Ce navigateur n’affichera pas le PDF directement. ' + + 'Téléchargez-le plutôt — il a été déchiffré ici dans les deux cas.', + 'group.upload_indexing': 'indexation…', + 'video.err_transport': 'Transport non connecté', + 'video.err_mse': 'Codec non pris en charge pour la diffusion : {codec}', + + // Settings + 'settings.title': 'Paramètres', + 'settings.coming_soon': 'Bientôt disponible.', + 'settings.downloads': 'Téléchargements', + 'settings.dl_auto': 'Enregistrer automatiquement', + 'settings.dl_auto_hint': 'Les fichiers vont directement dans le dossier choisi ' + + 'ci-dessous, sans boîte de dialogue. Télécharger vingt fichiers en dépose vingt.', + 'settings.dl_ask': 'Demander à chaque fois', + 'settings.dl_ask_hint': 'Une boîte de dialogue « Enregistrer sous » par fichier — ' + + 'un par fichier, y compris lorsque vous téléchargez une sélection.', + 'settings.dl_folder': 'Dossier : {name}', + 'settings.dl_no_folder': 'Aucun dossier choisi — les téléchargements vont là où ' + + 'votre navigateur les place', + 'settings.dl_choose': 'Choisir un dossier', + 'settings.dl_change': 'Changer', + 'settings.dl_forget': 'Oublier', + 'settings.dl_path_note': 'On ne peut pas indiquer un chemin à une page web, il n’y ' + + 'a donc rien à saisir ici : votre navigateur donne accès au dossier que vous ' + + 'désignez, et MeshBay n’écrit jamais ailleurs qu’à l’intérieur. Une ' + + 'confirmation peut vous être demandée une fois par session.', + 'settings.dl_unsupported': 'Ce navigateur ne sait pas écrire dans un dossier de ' + + 'votre choix (pas de File System Access API) ; les téléchargements vont donc ' + + 'dans son propre dossier de téléchargement. Chrome et Edge permettent d’en ' + + 'choisir un.', + 'settings.profile': 'Profil', + 'settings.username': "Nom d'utilisateur", + 'settings.node_pins': 'Identités des nodes', + 'settings.node_pins_hint': "La clé d'identité de chaque node est mémorisée lors de " + + 'la première connexion. Si elle change, la connexion est refusée — ce qui n’est ' + + 'normal que lorsqu’un opérateur réinstalle un node. Vérifiez auprès de lui ' + + 'avant d’effacer.', + 'settings.node_pins_count': { + one: '{n} épinglée', + other: '{n} épinglées', + }, + 'settings.node_pins_clear': 'Effacer les identités épinglées', + 'settings.danger': 'Supprimer le compte', + 'settings.delete_hint': 'Supprime votre compte, vos adhésions aux groupes et vos ' + + 'notifications du hub, et libère votre nom d’utilisateur. Cela n’atteint pas ' + + 'ce qui se trouve sur les nodes : les fichiers que vous avez envoyés restent là ' + + 'où leur opérateur les conserve, et chaque node garde l’identité qu’il a ' + + 'épinglée pour vous jusqu’à ce que son opérateur la retire. Les journaux de ' + + 'connexion sont conservés un an, comme la loi l’exige.', + 'settings.delete_account': 'Supprimer mon compte', + 'settings.delete_confirm': 'Saisissez votre phrase secrète pour confirmer. ' + + 'Cette action est irréversible.', + 'settings.delete_confirm_btn': 'Supprimer définitivement', + 'settings.cancel': 'Annuler', + 'settings.appearance': 'Apparence', + 'settings.theme': 'Thème', + 'settings.theme_light': 'Clair', + 'settings.theme_dark': 'Sombre', + 'settings.theme_system': 'Système', + 'settings.language': 'Langue', + 'settings.about': 'À propos', + 'settings.version': 'Version', + 'settings.protocol': 'Protocole', + 'settings.role': 'Rôle', + 'settings.groups': 'Notifications de groupe', + 'settings.notifications': 'Notifications', + 'settings.node_key': 'Associer un node', + 'settings.node_key_desc': 'Si vous exploitez un node MeshBay, collez ici sa clé ' + + 'publique Ed25519 pour l’associer à votre compte. Vous trouverez cette clé ' + + 'dans le tableau de bord d’administration de votre node.', + 'settings.node_key_placeholder': 'Collez la clé publique Ed25519 du node (base64)', + 'settings.node_key_submit': 'Associer le node', + 'settings.node_key_success': 'Node associé avec succès', + 'settings.node_key_current': 'Clé du node associé', + + // Sidebar + 'sidebar.create_group': 'Créer un groupe', + 'sidebar.settings': 'Paramètres', + 'sidebar.admin': 'Administration', + + // User menu + 'usermenu.language': 'Langue', + 'usermenu.settings': 'Paramètres', + 'usermenu.theme_light': 'Thème clair', + 'usermenu.theme_dark': 'Thème sombre', + 'usermenu.logout': 'Déconnexion', + + // Admin panel + 'admin.title': 'Administration', + 'admin.tab_stats': 'Statistiques', + 'admin.tab_users': 'Utilisateurs', + 'admin.tab_groups': 'Groupes', + 'admin.tab_nodes': 'Nodes', + 'admin.tab_logs': 'Journaux', + 'admin.col_observed_ip': 'Vu depuis', + 'admin.col_hint': 'Adresse annoncée', + 'admin.col_last_seen': 'Dernière annonce', + 'admin.node_online': 'en ligne', + 'admin.node_offline': 'hors ligne', + 'admin.no_nodes': 'Aucun node enregistré', + 'admin.nodes_hint': '« Vu depuis » est l’adresse d’où l’annonce nous est ' + + 'parvenue, sur une requête signée avec la clé du node — c’est celle sur ' + + 'laquelle s’appuyer pour répondre à une réquisition. L’adresse annoncée est ' + + 'celle que le node croit être la sienne, apprise d’un serveur STUN puis ' + + 'transmise ; elle sert à joindre le node et ne prouve rien.', + 'admin.tab_blocklist': 'Liste de blocage', + + // Admin stats + 'admin.stat_users': 'Utilisateurs', + 'admin.stat_groups': 'Groupes', + 'admin.stat_nodes': 'Nodes', + 'admin.stat_online': 'Nodes en ligne', + + // Admin users + 'admin.users_search': 'Rechercher un utilisateur...', + 'admin.col_username': "Nom d'utilisateur", + 'admin.col_role': 'Rôle', + 'admin.col_status': 'Statut', + 'admin.col_created': 'Création', + 'admin.col_actions': 'Actions', + 'admin.col_email': 'E-mail', + 'admin.col_groups': 'Groupes', + 'admin.no_users': 'Aucun utilisateur trouvé', + 'admin.btn_suspend': 'Suspendre', + 'admin.btn_unsuspend': 'Réactiver', + 'admin.btn_details': 'Détails', + 'admin.btn_delete': 'Supprimer', + 'admin.delete_confirm': 'Supprimer le compte « {user} » ? Cette action est ' + + 'irréversible. Les fichiers qu’il a envoyés restent sur les nodes qui les ' + + 'hébergent, et chaque node conserve l’identité qu’il a épinglée jusqu’à ce ' + + 'que son opérateur la retire. La suspension est l’option réversible.', + 'admin.user_detail': "Détails de l'utilisateur", + 'admin.btn_close': 'Fermer', + 'admin.self_note': '(vous)', + + // Admin groups + 'admin.col_name': 'Nom', + 'admin.col_visibility': 'Visibilité', + 'admin.col_members': 'Membres', + 'admin.no_groups': 'Aucun groupe trouvé', + + // Admin logs + 'admin.col_time': 'Horodatage', + 'admin.col_event': 'Événement', + 'admin.col_user': 'Utilisateur', + 'admin.col_ip': 'IP', + 'admin.col_detail': 'Détail', + 'admin.filter_all': 'Tous les événements', + 'admin.no_logs': 'Aucun journal trouvé', + 'admin.btn_load_more': 'Charger plus', + + // Admin blocklist + 'admin.col_hash': 'Empreinte', + 'admin.col_reason': 'Motif', + 'admin.col_date': 'Date', + 'admin.col_added_by': 'Ajoutée par', + 'admin.no_blocked': 'Aucune empreinte bloquée', + 'admin.add_hash': 'Ajouter une empreinte à la liste de blocage', + 'admin.hash_placeholder': 'empreinte blake3 (64 caractères hexadécimaux)', + 'admin.reason_placeholder': 'Motif', + 'admin.btn_block': 'Bloquer', + 'admin.btn_unblock': 'Débloquer', + + // Notifications + // Create group + 'create_group.title': 'Créer un groupe', + 'create_group.name': 'Nom du groupe', + 'create_group.name_placeholder': 'ex. Photos de famille, Documents du projet...', + 'create_group.visibility': 'Visibilité', + 'create_group.private': 'Privé', + 'create_group.private_desc': 'Seuls les membres invités voient les fichiers et y accèdent', + 'create_group.public': 'Public', + 'create_group.public_desc': 'Tout le monde peut découvrir et parcourir ce groupe', + 'create_group.join_policy': "Politique d'adhésion", + 'create_group.invite': 'Sur invitation', + 'create_group.invite_desc': 'Les membres doivent être invités par un administrateur', + 'create_group.open': 'Ouvert (tout le monde peut rejoindre)', + 'create_group.open_desc': 'Tout le monde peut rejoindre sans validation', + 'create_group.description': 'Description', + 'create_group.description_hint': 'De quoi parle ce groupe ? (facultatif)', + 'create_group.submit': 'Créer', + 'create_group.hint': 'Un groupe a besoin d’un node pour héberger les fichiers. ' + + 'Vous pouvez créer le groupe maintenant et connecter un node plus tard.', + 'create_group.creating': 'Création...', + + // Members + 'members.col_role': 'Rôle', + 'members.group_role': 'Rôle dans le groupe', + 'members.admin': 'Administrateur', + 'members.group_admin': 'Administrateur du groupe', + 'members.owner': 'Propriétaire', + 'members.member': 'Membre', + 'members.invite_title': 'Inviter un membre', + 'members.username_placeholder': "Nom d'utilisateur", + 'members.invite_btn': 'Inviter', + 'members.invite_needs_pairing': 'Les invitations sont signées avec la clé que ce ' + + 'node a épinglée pour votre navigateur : appairez-le ci-dessous avant ' + + 'd’inviter qui que ce soit.', + 'members.invite_ask_operator': 'Seul l’opérateur du node qui héberge ce groupe ' + + 'peut inviter, depuis un navigateur appairé avec lui.', + 'members.remove': 'Retirer', + 'members.remove_confirm': 'Retirer {user} de ce groupe ? Son compte, ses autres ' + + 'groupes et les fichiers qu’il a envoyés ne sont pas affectés.', + 'members.remove_hint': 'Retirer quelqu’un lui ôte son adhésion et empêche le node ' + + 'de lui remettre la clé du groupe. Cela ne supprime pas son compte, et cela ne ' + + 'reprend pas la clé qu’il détient déjà — faites-la tourner sur le node ' + + '(meshbay-node gek-init) si cela compte.', + 'members.pair_title': 'Appairer ce navigateur avec votre node', + 'members.pair_hint': 'Votre node n’accepte les actions d’opérateur — invitations, ' + + 'suppression de fichiers — que depuis un navigateur avec lequel il a été ' + + 'appairé. Lancez `meshbay-node operator pair` sur le node et saisissez le code ' + + 'ici. Le code ne transite jamais par le hub.', + 'members.pair_btn': 'Appairer', + 'members.pair_success': 'Ce navigateur est désormais appairé avec le node.', + 'members.invite_code_ready': 'Code d’invitation pour {user} — transmettez-le-lui ' + + 'par le moyen dont vous avez l’habitude. Il ne fonctionne qu’une fois, et ne ' + + 'transite jamais par le hub.', + 'members.invite_code_hint': 'La personne le saisit la première fois qu’elle ouvre ' + + 'ce groupe. Vous n’avez pas besoin d’être en ligne à ce moment-là.', + 'transfers.title': 'Transferts', + 'transfers.clear': 'Effacer les terminés', + 'transfers.cancel': 'Annuler', + 'transfers.open': 'Ouvrir', + 'transfers.done': 'Terminé', + 'transfers.cancelled': 'Annulé', + 'transfers.failed': 'Échec', + 'group.select': 'Sélectionner', + 'group.select_done': 'Terminé', + 'group.actions': 'Actions ({n})', + 'group.download_n': 'Télécharger ({n})', + 'group.download_zip_n': 'Télécharger les dossiers en zip ({n})', + 'group.delete_n': 'Supprimer ({n})', + 'group.delete_n_confirm': { + one: 'Supprimer {n} élément ? {names}', + other: 'Supprimer {n} éléments ? {names}', + }, + 'group.download_zip': { + one: 'Télécharger en zip ({n} fichier)', + other: 'Télécharger en zip ({n} fichiers)', + }, + 'group.zip_empty': 'Ce dossier ne contient rien à télécharger.', + 'group.zip_no_stream': 'Ce navigateur ne sait pas écrire un téléchargement ' + + 'directement sur le disque : {name} ({size}) doit donc être assemblé en mémoire ' + + 'au préalable. Sur une archive volumineuse, cela peut échouer. Continuer ?', + 'group.rmdir': 'Supprimer le dossier', + 'group.rmdir_confirm': 'Supprimer le dossier « {name} » ? Il doit être vide.', + 'group.rmdir_not_empty': 'Ce dossier n’est pas vide. Supprimez d’abord ce qu’il ' + + 'contient — rien ici ne supprime des fichiers que vous ne voyez pas.', + 'group.desc_edit': 'Modifier la description', + 'group.desc_add': 'Ajouter une description', + 'group.desc_save': 'Enregistrer', + 'group.desc_cancel': 'Annuler', + 'group.desc_placeholder': 'À quoi sert ce groupe — les membres le voient sur leur ' + + 'page d’accueil.', + 'group.join_code_title': 'Ce node doit vous reconnaître', + 'group.join_code_hint': 'Demandez son code à usage unique à la personne qui vous a ' + + 'invité, et saisissez-le ici. Ensuite, ce navigateur est reconnu et la question ' + + 'ne vous sera plus posée.', + 'group.join_code_btn': 'Rejoindre', + + 'notif.purge': 'Tout effacer', + 'notif.title': 'Notifications', + 'notif.empty': 'Aucune notification', + 'notif.mark_all_read': 'Tout marquer comme lu', + + // Sidebar + 'sidebar.search': 'Rechercher des fichiers', + + // Status + + // Search + 'search.title': 'Rechercher des fichiers', + 'search.placeholder': 'Rechercher dans tous les groupes...', + 'search.no_results': 'Aucun fichier ne correspond à votre recherche.', + 'search.synced': 'synchronisé {ago}', + 'search.cache_note': 'La recherche s’appuie sur l’état de chaque groupe lors de ' + + 'votre dernière visite. Ouvrez un groupe pour mettre ses fichiers à jour ici.', + 'search.col_group': 'Groupe', + 'search.result_count': { + one: '{n} fichier trouvé', + other: '{n} fichiers trouvés', + }, + 'search.hint': 'Recherche les noms de fichiers dans tous les index de groupe en cache.', +}; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js new file mode 100644 index 0000000..e3c92ef --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -0,0 +1,404 @@ +/** + * Italian — translated from locales/en.js. + * + * Register: formal (forma di cortesia, "Lei"). + * `hub`, `node` and `GEK` are left in English: they name the CLI + * (`meshbay-node`), node.toml and the docs, which are English throughout. + * Grammatical gender chosen here: l'hub (m.), il node (m.), la GEK → "la chiave". + */ + +export default { + // Nav + 'nav.toggle_menu': 'Mostra il menu', + 'nav.light_mode': 'Tema chiaro', + 'nav.dark_mode': 'Tema scuro', + 'nav.logout': 'Esci', + 'nav.login': 'Accedi', + + // Sidebar + 'sidebar.my_groups': 'I miei gruppi', + 'sidebar.no_groups': 'Ancora nessun gruppo', + 'sidebar.discover': 'Esplora', + 'sidebar.public_groups': 'Gruppi pubblici', + + // Login + 'login.title': 'Accedi', + 'login.username': 'Nome utente', + 'login.password': 'Password', + 'login.submit': 'Accedi', + 'login.loading': 'Accesso in corso...', + 'login.no_account': 'Non ha ancora un account?', + 'login.register_link': 'Crea un account', + + // Register + 'register.title': 'Crea un account', + 'register.username': 'Nome utente', + 'register.email': 'Indirizzo e-mail', + 'register.password': 'Passphrase (almeno 12 caratteri)', + 'register.confirm': 'Conferma la password', + 'register.submit': "Crea l'account", + 'register.loading': "Creazione dell'account...", + 'register.has_account': 'Ha già un account?', + 'register.login_link': 'Accedi', + 'register.success_title': 'Account creato', + 'register.success_msg': 'Ora può accedere con le sue credenziali.', + 'register.go_login': "Vai all'accesso", + 'register.err_mismatch': 'Le password non coincidono', + 'register.err_min_len': { + one: 'Usi almeno {n} carattere', + other: 'Usi almeno {n} caratteri', + }, + 'register.err_too_weak': 'Troppo facile da indovinare. La passphrase è ciò che ' + + 'protegge le sue chiavi là dove sono conservate — qualche parola senza alcun ' + + 'legame tra loro funziona bene.', + 'register.strength': 'Robustezza: circa {bits} bit. Protegge la copia delle sue ' + + 'chiavi conservata sui node a cui aderisce, quindi vale la pena sceglierla con cura.', + + // Home + 'home.welcome': 'Benvenuto in MeshBay', + 'home.no_groups': 'Non fa ancora parte di nessun gruppo.', + 'home.browse_prefix': 'Dia un’occhiata ai ', + 'home.browse_link': 'gruppi pubblici', + 'home.browse_suffix': " oppure chieda a un amministratore di gruppo di invitarla.", + 'home.my_groups': 'I miei gruppi', + + // Explore + 'explore.title': 'Gruppi pubblici', + 'explore.search': 'Cerca gruppi...', + 'explore.loading': 'Caricamento...', + 'explore.empty': 'Nessun gruppo pubblico disponibile.', + 'explore.create_group': 'Crea un gruppo', + 'explore.join': 'Partecipa', + 'explore.member': 'Iscritto', + + // Group page + 'group.default_name': 'Gruppo', + 'group.tab_files': 'File', + 'group.tab_chat': 'Chat', + 'group.tab_members': 'Membri', + 'group.filter': 'Filtra i file...', + 'group.col_name': 'Nome', + 'group.col_size': 'Dimensione', + 'group.col_type': 'Tipo', + 'group.col_date': 'Data', + 'group.empty_filter': 'Nessun file corrisponde al filtro', + 'group.empty_dir': 'Questa cartella è vuota', + 'group.download': 'Scarica', + 'group.play': 'Riproduci', + 'group.offline_title': 'Al momento nessun node è online per questo gruppo.', + 'group.offline_hint': 'I file compariranno quando si collegherà un node che ospita ' + + 'questo gruppo.', + 'group.upload': 'Carica', + 'group.mkdir': 'Nuova cartella', + 'group.mkdir_prompt': 'Nome della nuova cartella:', + 'group.view': 'Visualizza', + 'group.delete': 'Elimina', + 'group.delete_confirm': 'Eliminare {name}?', + 'group.delete_group': 'Elimina il gruppo', + 'group.delete_group_confirm': 'Eliminare definitivamente «{name}» e rimuovere tutti ' + + 'i membri? I file presenti sul node vengono conservati.', + 'group.err_transport': 'Modulo di trasporto non caricato', + + // Status + 'status.idle': 'Inattivo', + 'status.discovering': 'Ricerca dei node...', + 'status.connecting': 'Connessione tramite WebRTC...', + 'status.fetching': "Recupero dell'indice...", + 'status.files': { + one: '{n} file', + other: '{n} file', + }, + 'status.offline': 'Nessun node online', + 'status.error': 'Connessione non riuscita', + + // Chat + 'chat.empty': 'Ancora nessun messaggio. Inizi la conversazione!', + 'chat.placeholder': 'Scriva un messaggio...', + 'chat.send': 'Invia', + 'chat.attach': 'Allega un file', + + // Video player + 'video.loading': 'Caricamento di {name}...', + 'video.buffering': 'Buffering in corso...', + 'video.close': 'Chiudi (Esc)', + 'preview.pdf_fallback': 'Questo browser non mostrerà il PDF nella pagina. Lo scarichi ' + + 'invece — in ogni caso è stato decifrato qui.', + 'group.upload_indexing': 'indicizzazione…', + 'video.err_transport': 'Trasporto non connesso', + 'video.err_mse': 'Codec non supportato per lo streaming: {codec}', + + // Settings + 'settings.title': 'Impostazioni', + 'settings.coming_soon': 'Prossimamente.', + 'settings.downloads': 'Download', + 'settings.dl_auto': 'Salva automaticamente', + 'settings.dl_auto_hint': 'I file finiscono direttamente nella cartella che sceglie ' + + 'qui sotto, senza alcuna finestra. Scaricare venti file ne deposita venti.', + 'settings.dl_ask': 'Chiedi ogni volta', + 'settings.dl_ask_hint': 'Una finestra «Salva con nome» per ciascun file — una per ' + + 'file, anche quando scarica una selezione.', + 'settings.dl_folder': 'Cartella: {name}', + 'settings.dl_no_folder': 'Nessuna cartella scelta — i download finiscono dove li ' + + 'colloca il browser', + 'settings.dl_choose': 'Scegli una cartella', + 'settings.dl_change': 'Cambia', + 'settings.dl_forget': 'Dimentica', + 'settings.dl_path_note': 'A una pagina web non si può indicare un percorso, quindi ' + + 'qui non c’è nulla da digitare: è il browser a concedere l’accesso alla cartella ' + + 'che indica, e MeshBay scrive soltanto al suo interno. Potrebbe esserle chiesta ' + + 'una conferma una volta per sessione.', + 'settings.dl_unsupported': 'Questo browser non sa scrivere in una cartella a sua ' + + 'scelta (manca la File System Access API), perciò i download finiscono nella sua ' + + 'cartella di download. Chrome ed Edge permettono di sceglierne una.', + 'settings.profile': 'Profilo', + 'settings.username': 'Nome utente', + 'settings.node_pins': 'Identità dei node', + 'settings.node_pins_hint': "La chiave d'identità di ogni node viene memorizzata alla " + + 'prima connessione. Se cambia, la connessione viene rifiutata — cosa che ci si ' + + 'aspetta solo quando un operatore reinstalla un node. Verifichi con lui prima di ' + + 'cancellarla.', + 'settings.node_pins_count': { + one: '{n} fissata', + other: '{n} fissate', + }, + 'settings.node_pins_clear': 'Cancella le identità fissate', + 'settings.danger': "Elimina l'account", + 'settings.delete_hint': "Rimuove il suo account, le sue adesioni ai gruppi e le sue " + + "notifiche dall'hub, e libera il suo nome utente. Non arriva a ciò che si trova " + + 'sui node: i file che ha caricato restano dove il loro operatore li conserva, e ' + + 'ogni node mantiene l’identità che ha fissato per lei finché il suo operatore non ' + + 'la rimuove. I registri di connessione sono conservati per un anno, come impone ' + + 'la legge.', + 'settings.delete_account': 'Elimina il mio account', + 'settings.delete_confirm': 'Inserisca la sua passphrase per confermare. ' + + "L'operazione non può essere annullata.", + 'settings.delete_confirm_btn': 'Elimina definitivamente', + 'settings.cancel': 'Annulla', + 'settings.appearance': 'Aspetto', + 'settings.theme': 'Tema', + 'settings.theme_light': 'Chiaro', + 'settings.theme_dark': 'Scuro', + 'settings.theme_system': 'Sistema', + 'settings.language': 'Lingua', + 'settings.about': 'Informazioni', + 'settings.version': 'Versione', + 'settings.protocol': 'Protocollo', + 'settings.role': 'Ruolo', + 'settings.groups': 'Notifiche dei gruppi', + 'settings.notifications': 'Notifiche', + 'settings.node_key': 'Collega un node', + 'settings.node_key_desc': 'Se gestisce un node MeshBay, incolli qui la sua chiave ' + + 'pubblica Ed25519 per collegarlo al suo account. Trova questa chiave nel ' + + 'pannello di amministrazione del suo node.', + 'settings.node_key_placeholder': 'Incolli la chiave pubblica Ed25519 del node (base64)', + 'settings.node_key_submit': 'Collega il node', + 'settings.node_key_success': 'Node collegato correttamente', + 'settings.node_key_current': 'Chiave del node collegato', + + // Sidebar + 'sidebar.create_group': 'Crea un gruppo', + 'sidebar.settings': 'Impostazioni', + 'sidebar.admin': 'Amministrazione', + + // User menu + 'usermenu.language': 'Lingua', + 'usermenu.settings': 'Impostazioni', + 'usermenu.theme_light': 'Tema chiaro', + 'usermenu.theme_dark': 'Tema scuro', + 'usermenu.logout': 'Esci', + + // Admin panel + 'admin.title': 'Amministrazione', + 'admin.tab_stats': 'Statistiche', + 'admin.tab_users': 'Utenti', + 'admin.tab_groups': 'Gruppi', + 'admin.tab_nodes': 'Node', + 'admin.tab_logs': 'Registri', + 'admin.col_observed_ip': 'Visto da', + 'admin.col_hint': 'Indirizzo dichiarato', + 'admin.col_last_seen': 'Ultimo annuncio', + 'admin.node_online': 'online', + 'admin.node_offline': 'offline', + 'admin.no_nodes': 'Nessun node registrato', + 'admin.nodes_hint': '«Visto da» è l’indirizzo dal quale l’annuncio è effettivamente ' + + 'arrivato, in una richiesta firmata con la chiave del node — è quello su cui ' + + 'basarsi per rispondere a una richiesta formale. L’indirizzo dichiarato è quello ' + + 'che il node crede essere il proprio, appreso da un server STUN e inviato a noi; ' + + 'serve a raggiungere il node e non prova nulla.', + 'admin.tab_blocklist': 'Elenco dei blocchi', + + // Admin stats + 'admin.stat_users': 'Utenti', + 'admin.stat_groups': 'Gruppi', + 'admin.stat_nodes': 'Node', + 'admin.stat_online': 'Node online', + + // Admin users + 'admin.users_search': 'Cerca utenti...', + 'admin.col_username': 'Nome utente', + 'admin.col_role': 'Ruolo', + 'admin.col_status': 'Stato', + 'admin.col_created': 'Creazione', + 'admin.col_actions': 'Azioni', + 'admin.col_email': 'E-mail', + 'admin.col_groups': 'Gruppi', + 'admin.no_users': 'Nessun utente trovato', + 'admin.btn_suspend': 'Sospendi', + 'admin.btn_unsuspend': 'Riattiva', + 'admin.btn_details': 'Dettagli', + 'admin.btn_delete': 'Elimina', + 'admin.delete_confirm': 'Eliminare l’account «{user}»? L’operazione non può essere ' + + 'annullata. I file che ha caricato restano sui node che li ospitano, e ogni node ' + + 'mantiene l’identità che ha fissato finché il suo operatore non la rimuove. La ' + + 'sospensione è l’opzione reversibile.', + 'admin.user_detail': "Dettagli dell'utente", + 'admin.btn_close': 'Chiudi', + 'admin.self_note': '(lei)', + + // Admin groups + 'admin.col_name': 'Nome', + 'admin.col_visibility': 'Visibilità', + 'admin.col_members': 'Membri', + 'admin.no_groups': 'Nessun gruppo trovato', + + // Admin logs + 'admin.col_time': 'Ora', + 'admin.col_event': 'Evento', + 'admin.col_user': 'Utente', + 'admin.col_ip': 'IP', + 'admin.col_detail': 'Dettaglio', + 'admin.filter_all': 'Tutti gli eventi', + 'admin.no_logs': 'Nessun registro trovato', + 'admin.btn_load_more': 'Carica altri', + + // Admin blocklist + 'admin.col_hash': 'Hash', + 'admin.col_reason': 'Motivo', + 'admin.col_date': 'Data', + 'admin.col_added_by': 'Aggiunto da', + 'admin.no_blocked': 'Nessun hash bloccato', + 'admin.add_hash': "Aggiungi un hash all'elenco dei blocchi", + 'admin.hash_placeholder': 'hash blake3 (64 caratteri esadecimali)', + 'admin.reason_placeholder': 'Motivo', + 'admin.btn_block': 'Blocca', + 'admin.btn_unblock': 'Sblocca', + + // Notifications + // Create group + 'create_group.title': 'Crea un gruppo', + 'create_group.name': 'Nome del gruppo', + 'create_group.name_placeholder': 'es. Foto di famiglia, File del progetto...', + 'create_group.visibility': 'Visibilità', + 'create_group.private': 'Privato', + 'create_group.private_desc': 'Solo i membri invitati vedono i file e vi accedono', + 'create_group.public': 'Pubblico', + 'create_group.public_desc': 'Chiunque può scoprire e sfogliare questo gruppo', + 'create_group.join_policy': 'Modalità di adesione', + 'create_group.invite': 'Solo su invito', + 'create_group.invite_desc': 'I membri devono essere invitati da un amministratore', + 'create_group.open': 'Aperto (chiunque può partecipare)', + 'create_group.open_desc': 'Chiunque può partecipare senza approvazione', + 'create_group.description': 'Descrizione', + 'create_group.description_hint': 'Di cosa si occupa questo gruppo? (facoltativo)', + 'create_group.submit': 'Crea', + 'create_group.hint': 'Un gruppo ha bisogno di un node che ospiti i file. Può creare ' + + 'il gruppo ora e collegare un node più avanti.', + 'create_group.creating': 'Creazione in corso...', + + // Members + 'members.col_role': 'Ruolo', + 'members.group_role': 'Ruolo nel gruppo', + 'members.admin': 'Amministratore', + 'members.group_admin': 'Amministratore del gruppo', + 'members.owner': 'Proprietario', + 'members.member': 'Membro', + 'members.invite_title': 'Invita un membro', + 'members.username_placeholder': 'Nome utente', + 'members.invite_btn': 'Invita', + 'members.invite_needs_pairing': 'Gli inviti sono firmati con la chiave che questo ' + + 'node ha fissato per il suo browser: lo associ qui sotto prima di invitare ' + + 'chiunque.', + 'members.invite_ask_operator': 'Solo l’operatore del node che ospita questo gruppo ' + + 'può invitare, da un browser associato a esso.', + 'members.remove': 'Rimuovi', + 'members.remove_confirm': 'Rimuovere {user} da questo gruppo? Il suo account, gli ' + + 'altri gruppi e i file che ha caricato non vengono toccati.', + 'members.remove_hint': 'Rimuovere qualcuno gli toglie l’adesione e impedisce al node ' + + 'di consegnargli la chiave del gruppo. Non elimina il suo account e non recupera ' + + 'la chiave che già possiede — la ruoti sul node (meshbay-node gek-init) se la ' + + 'cosa è importante.', + 'members.pair_title': 'Associ questo browser al suo node', + 'members.pair_hint': 'Il suo node accetta le azioni da operatore — inviti, ' + + 'eliminazione di file — solo da un browser a cui è stato associato. Esegua ' + + '`meshbay-node operator pair` sul node e digiti qui il codice. Il codice non ' + + 'passa mai per l’hub.', + 'members.pair_btn': 'Associa', + 'members.pair_success': 'Questo browser è ora associato al node.', + 'members.invite_code_ready': 'Codice di invito per {user} — glielo mandi con il ' + + 'mezzo che usate di solito. Funziona una volta sola e non passa mai per l’hub.', + 'members.invite_code_hint': 'Lo inserirà la prima volta che aprirà questo gruppo. ' + + 'Non è necessario che lei sia online in quel momento.', + 'transfers.title': 'Trasferimenti', + 'transfers.clear': 'Rimuovi i completati', + 'transfers.cancel': 'Annulla', + 'transfers.open': 'Apri', + 'transfers.done': 'Completato', + 'transfers.cancelled': 'Annullato', + 'transfers.failed': 'Non riuscito', + 'group.select': 'Seleziona', + 'group.select_done': 'Fine', + 'group.actions': 'Azioni ({n})', + 'group.download_n': 'Scarica ({n})', + 'group.download_zip_n': 'Scarica le cartelle in zip ({n})', + 'group.delete_n': 'Elimina ({n})', + 'group.delete_n_confirm': { + one: 'Eliminare {n} elemento? {names}', + other: 'Eliminare {n} elementi? {names}', + }, + 'group.download_zip': { + one: 'Scarica in zip ({n} file)', + other: 'Scarica in zip ({n} file)', + }, + 'group.zip_empty': 'Quella cartella non contiene nulla da scaricare.', + 'group.zip_no_stream': 'Questo browser non sa scrivere un download direttamente su ' + + 'disco, quindi {name} ({size}) deve prima essere costruito in memoria. Con un ' + + 'archivio grande l’operazione può fallire. Continuare?', + 'group.rmdir': 'Elimina la cartella', + 'group.rmdir_confirm': 'Eliminare la cartella «{name}»? Deve essere vuota.', + 'group.rmdir_not_empty': 'Quella cartella non è vuota. Elimini prima ciò che ' + + 'contiene — qui nulla cancella file che lei non vede.', + 'group.desc_edit': 'Modifica la descrizione', + 'group.desc_add': 'Aggiungi una descrizione', + 'group.desc_save': 'Salva', + 'group.desc_cancel': 'Annulla', + 'group.desc_placeholder': 'A cosa serve questo gruppo — i membri lo vedono sulla ' + + 'loro pagina iniziale.', + 'group.join_code_title': 'Questo node deve riconoscerla', + 'group.join_code_hint': 'Chieda il codice monouso a chi l’ha invitata e lo inserisca ' + + 'qui. Dopodiché questo browser è riconosciuto e non le verrà più chiesto.', + 'group.join_code_btn': 'Partecipa', + + 'notif.purge': 'Cancella tutto', + 'notif.title': 'Notifiche', + 'notif.empty': 'Nessuna notifica', + 'notif.mark_all_read': 'Segna tutto come letto', + + // Sidebar + 'sidebar.search': 'Cerca file', + + // Status + + // Search + 'search.title': 'Cerca file', + 'search.placeholder': 'Cerca in tutti i gruppi...', + 'search.no_results': 'Nessun file corrisponde alla sua ricerca.', + 'search.synced': 'sincronizzato {ago}', + 'search.cache_note': 'La ricerca si basa su com’era ogni gruppo l’ultima volta che ' + + 'lo ha aperto. Apra un gruppo per aggiornarne qui i file.', + 'search.col_group': 'Gruppo', + 'search.result_count': { + one: '{n} file trovato', + other: '{n} file trovati', + }, + 'search.hint': 'Cerca i nomi dei file in tutti gli indici di gruppo in cache.', +}; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js new file mode 100644 index 0000000..f9fd5ac --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -0,0 +1,393 @@ +/** + * Japanese — translated from locales/en.js. + * + * Register: formal — です・ます throughout, with 丁寧語 for instructions. + * `hub`, `node` and `GEK` are left in English: they name the CLI + * (`meshbay-node`), node.toml and the docs, which are English throughout. + * Japanese has a single plural category, so counted strings need only `other`. + */ + +export default { + // Nav + 'nav.toggle_menu': 'メニューを表示', + 'nav.light_mode': 'ライトテーマ', + 'nav.dark_mode': 'ダークテーマ', + 'nav.logout': 'ログアウト', + 'nav.login': 'ログイン', + + // Sidebar + 'sidebar.my_groups': 'マイグループ', + 'sidebar.no_groups': 'グループはまだありません', + 'sidebar.discover': '見つける', + 'sidebar.public_groups': '公開グループ', + + // Login + 'login.title': 'ログイン', + 'login.username': 'ユーザー名', + 'login.password': 'パスワード', + 'login.submit': 'ログイン', + 'login.loading': 'ログインしています…', + 'login.no_account': 'アカウントをお持ちでないですか?', + 'login.register_link': 'アカウントを作成', + + // Register + 'register.title': 'アカウントを作成', + 'register.username': 'ユーザー名', + 'register.email': 'メールアドレス', + 'register.password': 'パスフレーズ(12 文字以上)', + 'register.confirm': 'パスワードの確認', + 'register.submit': 'アカウントを作成', + 'register.loading': 'アカウントを作成しています…', + 'register.has_account': 'すでにアカウントをお持ちですか?', + 'register.login_link': 'ログイン', + 'register.success_title': 'アカウントを作成しました', + 'register.success_msg': 'ご自身の資格情報でログインできるようになりました。', + 'register.go_login': 'ログインへ進む', + 'register.err_mismatch': 'パスワードが一致しません', + 'register.err_min_len': { + other: '{n} 文字以上でご入力ください', + }, + 'register.err_too_weak': '推測されやすすぎます。パスフレーズは、鍵が保管されている場所で' + + 'その鍵を守るものです。互いに関係のない単語をいくつか並べると効果的です。', + 'register.strength': '強度:約 {bits} ビット。参加している各 node に保管される鍵の' + + 'コピーを守るものですので、しっかり決めておく価値があります。', + + // Home + 'home.welcome': 'MeshBay へようこそ', + 'home.no_groups': 'まだどのグループにも参加していません。', + 'home.browse_prefix': '', + 'home.browse_link': '公開グループ', + 'home.browse_suffix': 'を見てみるか、グループ管理者に招待を依頼してください。', + 'home.my_groups': 'マイグループ', + + // Explore + 'explore.title': '公開グループ', + 'explore.search': 'グループを検索…', + 'explore.loading': '読み込んでいます…', + 'explore.empty': '利用できる公開グループはありません。', + 'explore.create_group': 'グループを作成', + 'explore.join': '参加', + 'explore.member': '参加済み', + + // Group page + 'group.default_name': 'グループ', + 'group.tab_files': 'ファイル', + 'group.tab_chat': 'チャット', + 'group.tab_members': 'メンバー', + 'group.filter': 'ファイルを絞り込み…', + 'group.col_name': '名前', + 'group.col_size': 'サイズ', + 'group.col_type': '種類', + 'group.col_date': '日付', + 'group.empty_filter': '条件に一致するファイルはありません', + 'group.empty_dir': 'このフォルダーは空です', + 'group.download': 'ダウンロード', + 'group.play': '再生', + 'group.offline_title': 'このグループのために稼働している node は現在ありません。', + 'group.offline_hint': 'このグループをホストしている node が接続すると、' + + 'ファイルが表示されます。', + 'group.upload': 'アップロード', + 'group.mkdir': '新しいフォルダー', + 'group.mkdir_prompt': '新しいフォルダーの名前:', + 'group.view': '表示', + 'group.delete': '削除', + 'group.delete_confirm': '{name} を削除しますか?', + 'group.delete_group': 'グループを削除', + 'group.delete_group_confirm': '「{name}」を完全に削除し、すべてのメンバーを' + + '解除しますか?node 上のファイルは保持されます。', + 'group.err_transport': 'トランスポートモジュールが読み込まれていません', + + // Status + 'status.idle': '待機中', + 'status.discovering': 'node を探しています…', + 'status.connecting': 'WebRTC で接続しています…', + 'status.fetching': 'インデックスを取得しています…', + 'status.files': { + other: '{n} 個のファイル', + }, + 'status.offline': 'オンラインの node はありません', + 'status.error': '接続に失敗しました', + + // Chat + 'chat.empty': 'まだメッセージはありません。会話を始めましょう。', + 'chat.placeholder': 'メッセージを入力…', + 'chat.send': '送信', + 'chat.attach': 'ファイルを添付', + + // Video player + 'video.loading': '{name} を読み込んでいます…', + 'video.buffering': 'バッファリング中…', + 'video.close': '閉じる(Esc)', + 'preview.pdf_fallback': 'このブラウザーはページ内に PDF を表示しません。' + + 'ダウンロードしてご覧ください。いずれにせよ復号はここで行われています。', + 'group.upload_indexing': 'インデックスを作成中…', + 'video.err_transport': 'トランスポートが接続されていません', + 'video.err_mse': 'ストリーミング再生に対応していないコーデックです:{codec}', + + // Settings + 'settings.title': '設定', + 'settings.coming_soon': '近日公開。', + 'settings.downloads': 'ダウンロード', + 'settings.dl_auto': '自動的に保存する', + 'settings.dl_auto_hint': 'ファイルはダイアログなしで、下で選んだフォルダーに' + + 'そのまま保存されます。20 個ダウンロードすれば、そこに 20 個並びます。', + 'settings.dl_ask': '毎回確認する', + 'settings.dl_ask_hint': 'ファイルごとに「名前を付けて保存」ダイアログが開きます。' + + 'まとめてダウンロードする場合も 1 ファイルにつき 1 回です。', + 'settings.dl_folder': 'フォルダー:{name}', + 'settings.dl_no_folder': 'フォルダーが選ばれていません。ダウンロードは' + + 'ブラウザーが決めた場所に保存されます', + 'settings.dl_choose': 'フォルダーを選択', + 'settings.dl_change': '変更', + 'settings.dl_forget': '解除', + 'settings.dl_path_note': 'ウェブページにパスを指定することはできないため、' + + 'ここに入力するものはありません。お選びになったフォルダーへのアクセスは' + + 'ブラウザーが許可し、MeshBay はその中にしか書き込みません。' + + 'セッションごとに 1 回、確認を求められることがあります。', + 'settings.dl_unsupported': 'このブラウザーはお好きなフォルダーへの書き込みに' + + '対応していないため(File System Access API がありません)、' + + 'ダウンロードはブラウザー自身のダウンロードフォルダーに保存されます。' + + 'Chrome と Edge では選択できます。', + 'settings.profile': 'プロフィール', + 'settings.username': 'ユーザー名', + 'settings.node_pins': 'node の識別情報', + 'settings.node_pins_hint': '各 node の識別鍵は、最初に接続したときに記憶されます。' + + 'それが変わった場合、接続は拒否されます。これが起こるのは、運営者が node を' + + '再インストールしたときだけのはずです。消去する前に運営者にご確認ください。', + 'settings.node_pins_count': { + other: '{n} 件を固定中', + }, + 'settings.node_pins_clear': '固定した識別情報を消去', + 'settings.danger': 'アカウントを削除', + 'settings.delete_hint': 'hub から、お客様のアカウント、グループへの参加、通知を' + + '削除し、ユーザー名を解放します。node 上にあるものには及びません。' + + 'アップロードしたファイルは、その運営者が保管する場所に残り、' + + '各 node はお客様のために固定した識別情報を、運営者が解除するまで保持します。' + + '接続ログは法令に従い 1 年間保管されます。', + 'settings.delete_account': 'アカウントを削除する', + 'settings.delete_confirm': '確認のためパスフレーズをご入力ください。' + + 'この操作は取り消せません。', + 'settings.delete_confirm_btn': '完全に削除する', + 'settings.cancel': 'キャンセル', + 'settings.appearance': '外観', + 'settings.theme': 'テーマ', + 'settings.theme_light': 'ライト', + 'settings.theme_dark': 'ダーク', + 'settings.theme_system': 'システムに合わせる', + 'settings.language': '言語', + 'settings.about': 'このアプリについて', + 'settings.version': 'バージョン', + 'settings.protocol': 'プロトコル', + 'settings.role': '権限', + 'settings.groups': 'グループの通知', + 'settings.notifications': '通知', + 'settings.node_key': 'node を連携', + 'settings.node_key_desc': 'MeshBay の node を運営している場合は、その Ed25519 ' + + '公開鍵をここに貼り付けて、アカウントに連携してください。' + + 'この鍵は node の管理ダッシュボードで確認できます。', + 'settings.node_key_placeholder': 'node の Ed25519 公開鍵を貼り付け(base64)', + 'settings.node_key_submit': 'node を連携', + 'settings.node_key_success': 'node を連携しました', + 'settings.node_key_current': '連携済みの node 鍵', + + // Sidebar + 'sidebar.create_group': 'グループを作成', + 'sidebar.settings': '設定', + 'sidebar.admin': '管理', + + // User menu + 'usermenu.language': '言語', + 'usermenu.settings': '設定', + 'usermenu.theme_light': 'ライトテーマ', + 'usermenu.theme_dark': 'ダークテーマ', + 'usermenu.logout': 'ログアウト', + + // Admin panel + 'admin.title': '管理', + 'admin.tab_stats': '統計', + 'admin.tab_users': 'ユーザー', + 'admin.tab_groups': 'グループ', + 'admin.tab_nodes': 'node', + 'admin.tab_logs': 'ログ', + 'admin.col_observed_ip': '接続元', + 'admin.col_hint': '申告されたアドレス', + 'admin.col_last_seen': '最終通知', + 'admin.node_online': 'オンライン', + 'admin.node_offline': 'オフライン', + 'admin.no_nodes': '登録された node はありません', + 'admin.nodes_hint': '「接続元」は、node の鍵で署名されたリクエストとして' + + '通知が実際に届いたアドレスです。照会に答える際はこちらを用いてください。' + + '申告されたアドレスは、node が STUN サーバーから知り得た「自分のアドレス」として' + + '送ってきたものです。node に到達するには役立ちますが、何かの証拠にはなりません。', + 'admin.tab_blocklist': 'ブロックリスト', + + // Admin stats + 'admin.stat_users': 'ユーザー', + 'admin.stat_groups': 'グループ', + 'admin.stat_nodes': 'node', + 'admin.stat_online': 'オンラインの node', + + // Admin users + 'admin.users_search': 'ユーザーを検索…', + 'admin.col_username': 'ユーザー名', + 'admin.col_role': '権限', + 'admin.col_status': '状態', + 'admin.col_created': '作成日', + 'admin.col_actions': '操作', + 'admin.col_email': 'メールアドレス', + 'admin.col_groups': 'グループ', + 'admin.no_users': 'ユーザーが見つかりません', + 'admin.btn_suspend': '停止', + 'admin.btn_unsuspend': '再開', + 'admin.btn_details': '詳細', + 'admin.btn_delete': '削除', + 'admin.delete_confirm': 'アカウント「{user}」を削除しますか?この操作は取り消せません。' + + 'このユーザーがアップロードしたファイルは、それをホストする node に残り、' + + '各 node は固定した識別情報を、その運営者が解除するまで保持します。' + + '停止であれば元に戻せます。', + 'admin.user_detail': 'ユーザーの詳細', + 'admin.btn_close': '閉じる', + 'admin.self_note': '(ご自身)', + + // Admin groups + 'admin.col_name': '名前', + 'admin.col_visibility': '公開範囲', + 'admin.col_members': 'メンバー', + 'admin.no_groups': 'グループが見つかりません', + + // Admin logs + 'admin.col_time': '時刻', + 'admin.col_event': 'イベント', + 'admin.col_user': 'ユーザー', + 'admin.col_ip': 'IP', + 'admin.col_detail': '詳細', + 'admin.filter_all': 'すべてのイベント', + 'admin.no_logs': 'ログが見つかりません', + 'admin.btn_load_more': 'さらに読み込む', + + // Admin blocklist + 'admin.col_hash': 'ハッシュ', + 'admin.col_reason': '理由', + 'admin.col_date': '日付', + 'admin.col_added_by': '追加者', + 'admin.no_blocked': 'ブロック中のハッシュはありません', + 'admin.add_hash': 'ハッシュをブロックリストに追加', + 'admin.hash_placeholder': 'blake3 ハッシュ(16 進数 64 文字)', + 'admin.reason_placeholder': '理由', + 'admin.btn_block': 'ブロック', + 'admin.btn_unblock': 'ブロック解除', + + // Notifications + // Create group + 'create_group.title': 'グループを作成', + 'create_group.name': 'グループ名', + 'create_group.name_placeholder': '例:家族の写真、プロジェクトの資料…', + 'create_group.visibility': '公開範囲', + 'create_group.private': '非公開', + 'create_group.private_desc': '招待されたメンバーだけがファイルを見て利用できます', + 'create_group.public': '公開', + 'create_group.public_desc': '誰でもこのグループを見つけて閲覧できます', + 'create_group.join_policy': '参加方法', + 'create_group.invite': '招待制', + 'create_group.invite_desc': 'メンバーは管理者から招待される必要があります', + 'create_group.open': 'オープン(誰でも参加できます)', + 'create_group.open_desc': '承認なしで誰でも参加できます', + 'create_group.description': '説明', + 'create_group.description_hint': 'どんなグループですか?(任意)', + 'create_group.submit': '作成', + 'create_group.hint': 'グループにはファイルをホストする node が必要です。' + + '先にグループを作成し、あとから node を接続することもできます。', + 'create_group.creating': '作成しています…', + + // Members + 'members.col_role': '権限', + 'members.group_role': 'グループ内の権限', + 'members.admin': '管理者', + 'members.group_admin': 'グループ管理者', + 'members.owner': '所有者', + 'members.member': 'メンバー', + 'members.invite_title': 'メンバーを招待', + 'members.username_placeholder': 'ユーザー名', + 'members.invite_btn': '招待', + 'members.invite_needs_pairing': '招待は、この node がお使いのブラウザー用に' + + '固定した鍵で署名されます。どなたかを招待する前に、下でペアリングしてください。', + 'members.invite_ask_operator': '招待できるのは、このグループをホストしている ' + + 'node の運営者だけで、その node とペアリング済みのブラウザーからに限られます。', + 'members.remove': '解除', + 'members.remove_confirm': '{user} をこのグループから外しますか?' + + 'そのアカウント、ほかのグループ、アップロード済みのファイルには影響しません。', + 'members.remove_hint': 'メンバーを外すと参加資格がなくなり、node はその人に' + + 'グループ鍵を渡さなくなります。アカウントが削除されるわけではなく、' + + 'すでに渡っている鍵を取り戻すこともできません。それが重要であれば、' + + 'node 上で鍵をローテーションしてください(meshbay-node gek-init)。', + 'members.pair_title': 'このブラウザーを node とペアリング', + 'members.pair_hint': 'node が運営者としての操作(招待、ファイルの削除)を受け付けるのは、' + + 'ペアリング済みのブラウザーからだけです。node で ' + + '`meshbay-node operator pair` を実行し、表示されたコードをここに入力してください。' + + 'このコードが hub を経由することはありません。', + 'members.pair_btn': 'ペアリング', + 'members.pair_success': 'このブラウザーは node とペアリングされました。', + 'members.invite_code_ready': '{user} 宛ての招待コードです。' + + 'いつもの連絡手段でお送りください。使えるのは 1 回だけで、hub を経由することはありません。', + 'members.invite_code_hint': '相手がこのグループを初めて開いたときに入力します。' + + 'そのときお客様がオンラインである必要はありません。', + 'transfers.title': '転送', + 'transfers.clear': '完了分を消去', + 'transfers.cancel': 'キャンセル', + 'transfers.open': '開く', + 'transfers.done': '完了', + 'transfers.cancelled': 'キャンセル済み', + 'transfers.failed': '失敗', + 'group.select': '選択', + 'group.select_done': '完了', + 'group.actions': '操作({n})', + 'group.download_n': 'ダウンロード({n})', + 'group.download_zip_n': 'フォルダーを zip でダウンロード({n})', + 'group.delete_n': '削除({n})', + 'group.delete_n_confirm': { + other: '{n} 件を削除しますか?{names}', + }, + 'group.download_zip': { + other: 'zip でダウンロード({n} 個のファイル)', + }, + 'group.zip_empty': 'そのフォルダーにダウンロードできるものはありません。', + 'group.zip_no_stream': 'このブラウザーはダウンロードをディスクへ直接書き込めないため、' + + '{name}({size})をいったんメモリー上で組み立てる必要があります。' + + '大きなアーカイブでは失敗することがあります。続けますか?', + 'group.rmdir': 'フォルダーを削除', + 'group.rmdir_confirm': 'フォルダー「{name}」を削除しますか?空である必要があります。', + 'group.rmdir_not_empty': 'そのフォルダーは空ではありません。先に中身を削除してください。' + + 'ここでの操作が、目に見えないファイルを消すことはありません。', + 'group.desc_edit': '説明を編集', + 'group.desc_add': '説明を追加', + 'group.desc_save': '保存', + 'group.desc_cancel': 'キャンセル', + 'group.desc_placeholder': 'このグループの目的。メンバーのホーム画面に表示されます。', + 'group.join_code_title': 'この node がお客様を認識する必要があります', + 'group.join_code_hint': '招待してくれた方にワンタイムコードを聞いて、ここに入力してください。' + + 'それ以降このブラウザーは認識され、再び尋ねられることはありません。', + 'group.join_code_btn': '参加', + + 'notif.purge': 'すべて消去', + 'notif.title': '通知', + 'notif.empty': '通知はありません', + 'notif.mark_all_read': 'すべて既読にする', + + // Sidebar + 'sidebar.search': 'ファイルを検索', + + // Status + + // Search + 'search.title': 'ファイルを検索', + 'search.placeholder': 'すべてのグループから検索…', + 'search.no_results': '検索条件に一致するファイルはありません。', + 'search.synced': '{ago}に同期', + 'search.cache_note': '検索は、各グループを最後に開いたときの状態を参照します。' + + 'グループを開くと、そのファイルがここで最新になります。', + 'search.col_group': 'グループ', + 'search.result_count': { + other: '{n} 個のファイルが見つかりました', + }, + 'search.hint': 'キャッシュされたすべてのグループインデックスからファイル名を検索します。', +}; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js new file mode 100644 index 0000000..2f778b3 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -0,0 +1,406 @@ +/** + * Dutch — translated from locales/en.js. + * + * Register: formal ("u"). + * `hub`, `node` and `GEK` are left in English: they name the CLI + * (`meshbay-node`), node.toml and the docs, which are English throughout. + * Grammatical gender chosen here: de hub, de node, de GEK (de sleutel). + */ + +export default { + // Nav + 'nav.toggle_menu': 'Menu tonen', + 'nav.light_mode': 'Licht thema', + 'nav.dark_mode': 'Donker thema', + 'nav.logout': 'Afmelden', + 'nav.login': 'Aanmelden', + + // Sidebar + 'sidebar.my_groups': 'Mijn groepen', + 'sidebar.no_groups': 'Nog geen groepen', + 'sidebar.discover': 'Ontdekken', + 'sidebar.public_groups': 'Openbare groepen', + + // Login + 'login.title': 'Aanmelden', + 'login.username': 'Gebruikersnaam', + 'login.password': 'Wachtwoord', + 'login.submit': 'Aanmelden', + 'login.loading': 'Bezig met aanmelden...', + 'login.no_account': 'Nog geen account?', + 'login.register_link': 'Account aanmaken', + + // Register + 'register.title': 'Account aanmaken', + 'register.username': 'Gebruikersnaam', + 'register.email': 'E-mailadres', + 'register.password': 'Wachtwoordzin (minstens 12 tekens)', + 'register.confirm': 'Wachtwoord bevestigen', + 'register.submit': 'Account aanmaken', + 'register.loading': 'Account wordt aangemaakt...', + 'register.has_account': 'Hebt u al een account?', + 'register.login_link': 'Aanmelden', + 'register.success_title': 'Account aangemaakt', + 'register.success_msg': 'U kunt zich nu met uw gegevens aanmelden.', + 'register.go_login': 'Naar het aanmelden', + 'register.err_mismatch': 'De wachtwoorden komen niet overeen', + 'register.err_min_len': { + one: 'Gebruik minstens {n} teken', + other: 'Gebruik minstens {n} tekens', + }, + 'register.err_too_weak': 'Te makkelijk te raden. Uw wachtwoordzin is wat uw sleutels ' + + 'beschermt daar waar ze bewaard worden — een paar woorden zonder onderling ' + + 'verband werken goed.', + 'register.strength': 'Sterkte: ongeveer {bits} bits. Ze beschermt de kopie van uw ' + + 'sleutels die op de nodes staat waar u zich bij aansluit, dus het loont om er ' + + 'even bij stil te staan.', + + // Home + 'home.welcome': 'Welkom bij MeshBay', + 'home.no_groups': 'U bent nog van geen enkele groep lid.', + 'home.browse_prefix': 'Bekijk de ', + 'home.browse_link': 'openbare groepen', + 'home.browse_suffix': ' of vraag een groepsbeheerder om u uit te nodigen.', + 'home.my_groups': 'Mijn groepen', + + // Explore + 'explore.title': 'Openbare groepen', + 'explore.search': 'Groepen zoeken...', + 'explore.loading': 'Bezig met laden...', + 'explore.empty': 'Geen openbare groepen beschikbaar.', + 'explore.create_group': 'Groep aanmaken', + 'explore.join': 'Deelnemen', + 'explore.member': 'Lid', + + // Group page + 'group.default_name': 'Groep', + 'group.tab_files': 'Bestanden', + 'group.tab_chat': 'Chat', + 'group.tab_members': 'Leden', + 'group.filter': 'Bestanden filteren...', + 'group.col_name': 'Naam', + 'group.col_size': 'Grootte', + 'group.col_type': 'Type', + 'group.col_date': 'Datum', + 'group.empty_filter': 'Geen bestand voldoet aan het filter', + 'group.empty_dir': 'Deze map is leeg', + 'group.download': 'Downloaden', + 'group.play': 'Afspelen', + 'group.offline_title': 'Er is momenteel geen node online voor deze groep.', + 'group.offline_hint': 'De bestanden verschijnen zodra er een node verbinding maakt ' + + 'die deze groep host.', + 'group.upload': 'Uploaden', + 'group.mkdir': 'Nieuwe map', + 'group.mkdir_prompt': 'Naam van de nieuwe map:', + 'group.view': 'Bekijken', + 'group.delete': 'Verwijderen', + 'group.delete_confirm': '{name} verwijderen?', + 'group.delete_group': 'Groep verwijderen', + 'group.delete_group_confirm': '"{name}" definitief verwijderen en alle leden ' + + 'verwijderen? De bestanden op de node blijven bewaard.', + 'group.err_transport': 'Transportmodule niet geladen', + + // Status + 'status.idle': 'Inactief', + 'status.discovering': 'Nodes zoeken...', + 'status.connecting': 'Verbinden via WebRTC...', + 'status.fetching': 'Index ophalen...', + 'status.files': { + one: '{n} bestand', + other: '{n} bestanden', + }, + 'status.offline': 'Geen node online', + 'status.error': 'Verbinding mislukt', + + // Chat + 'chat.empty': 'Nog geen berichten. Begin het gesprek!', + 'chat.placeholder': 'Typ een bericht...', + 'chat.send': 'Versturen', + 'chat.attach': 'Bestand bijvoegen', + + // Video player + 'video.loading': '{name} wordt geladen...', + 'video.buffering': 'Bezig met bufferen...', + 'video.close': 'Sluiten (Esc)', + 'preview.pdf_fallback': 'Deze browser toont de PDF niet in de pagina zelf. Download ' + + 'hem in plaats daarvan — ontsleuteld werd hij hoe dan ook hier.', + 'group.upload_indexing': 'indexeren…', + 'video.err_transport': 'Transport niet verbonden', + 'video.err_mse': 'Codec wordt niet ondersteund voor streamen: {codec}', + + // Settings + 'settings.title': 'Instellingen', + 'settings.coming_soon': 'Binnenkort beschikbaar.', + 'settings.downloads': 'Downloads', + 'settings.dl_auto': 'Automatisch opslaan', + 'settings.dl_auto_hint': 'Bestanden gaan zonder venster rechtstreeks naar de map die ' + + 'u hieronder kiest. Twintig downloads zetten daar twintig bestanden neer.', + 'settings.dl_ask': 'Elke keer vragen', + 'settings.dl_ask_hint': 'Een venster "Opslaan als" per bestand — één per bestand, ' + + 'ook wanneer u een selectie downloadt.', + 'settings.dl_folder': 'Map: {name}', + 'settings.dl_no_folder': 'Geen map gekozen — downloads komen terecht waar uw browser ' + + 'ze neerzet', + 'settings.dl_choose': 'Map kiezen', + 'settings.dl_change': 'Wijzigen', + 'settings.dl_forget': 'Vergeten', + 'settings.dl_path_note': 'Aan een webpagina kan geen pad worden doorgegeven, dus ' + + 'hier valt niets in te typen: uw browser verleent toegang tot de map die u ' + + 'aanwijst, en MeshBay schrijft uitsluitend daarbinnen. Mogelijk wordt u één keer ' + + 'per sessie om bevestiging gevraagd.', + 'settings.dl_unsupported': 'Deze browser kan niet schrijven naar een map van uw keuze ' + + '(geen File System Access API), dus downloads gaan naar zijn eigen downloadmap. ' + + 'Chrome en Edge laten wel een keuze toe.', + 'settings.profile': 'Profiel', + 'settings.username': 'Gebruikersnaam', + 'settings.node_pins': 'Node-identiteiten', + 'settings.node_pins_hint': 'De identiteitssleutel van elke node wordt bij de eerste ' + + 'verbinding onthouden. Verandert die, dan wordt de verbinding geweigerd — wat ' + + 'alleen te verwachten is wanneer een beheerder een node opnieuw installeert. ' + + 'Vraag het daar na voordat u ze wist.', + 'settings.node_pins_count': { + one: '{n} vastgezet', + other: '{n} vastgezet', + }, + 'settings.node_pins_clear': 'Vastgezette identiteiten wissen', + 'settings.danger': 'Account verwijderen', + 'settings.delete_hint': 'Verwijdert uw account, uw groepslidmaatschappen en uw ' + + 'meldingen van de hub, en geeft uw gebruikersnaam vrij. Wat op de nodes staat, ' + + 'wordt hiermee niet bereikt: bestanden die u hebt geüpload blijven waar hun ' + + 'beheerder ze bewaart, en elke node houdt de voor u vastgezette identiteit tot ' + + 'zijn beheerder die weghaalt. Verbindingslogboeken worden een jaar bewaard, zoals ' + + 'de wet voorschrijft.', + 'settings.delete_account': 'Mijn account verwijderen', + 'settings.delete_confirm': 'Voer uw wachtwoordzin in ter bevestiging. Dit kan niet ' + + 'ongedaan worden gemaakt.', + 'settings.delete_confirm_btn': 'Definitief verwijderen', + 'settings.cancel': 'Annuleren', + 'settings.appearance': 'Weergave', + 'settings.theme': 'Thema', + 'settings.theme_light': 'Licht', + 'settings.theme_dark': 'Donker', + 'settings.theme_system': 'Systeem', + 'settings.language': 'Taal', + 'settings.about': 'Over', + 'settings.version': 'Versie', + 'settings.protocol': 'Protocol', + 'settings.role': 'Rol', + 'settings.groups': 'Groepsmeldingen', + 'settings.notifications': 'Meldingen', + 'settings.node_key': 'Node koppelen', + 'settings.node_key_desc': 'Als u een MeshBay-node beheert, plak hier dan de ' + + 'openbare Ed25519-sleutel ervan om hem aan uw account te koppelen. U vindt die ' + + 'sleutel in het beheerdersdashboard van uw node.', + 'settings.node_key_placeholder': 'Plak de openbare Ed25519-sleutel van de node (base64)', + 'settings.node_key_submit': 'Node koppelen', + 'settings.node_key_success': 'Node gekoppeld', + 'settings.node_key_current': 'Sleutel van de gekoppelde node', + + // Sidebar + 'sidebar.create_group': 'Groep aanmaken', + 'sidebar.settings': 'Instellingen', + 'sidebar.admin': 'Beheer', + + // User menu + 'usermenu.language': 'Taal', + 'usermenu.settings': 'Instellingen', + 'usermenu.theme_light': 'Licht thema', + 'usermenu.theme_dark': 'Donker thema', + 'usermenu.logout': 'Afmelden', + + // Admin panel + 'admin.title': 'Beheer', + 'admin.tab_stats': 'Statistieken', + 'admin.tab_users': 'Gebruikers', + 'admin.tab_groups': 'Groepen', + 'admin.tab_nodes': 'Nodes', + 'admin.tab_logs': 'Logboeken', + 'admin.col_observed_ip': 'Gezien vanaf', + 'admin.col_hint': 'Opgegeven adres', + 'admin.col_last_seen': 'Laatste melding', + 'admin.node_online': 'online', + 'admin.node_offline': 'offline', + 'admin.no_nodes': 'Geen nodes geregistreerd', + 'admin.nodes_hint': '"Gezien vanaf" is het adres waarvandaan de melding werkelijk ' + + 'binnenkwam, in een verzoek dat met de sleutel van de node is ondertekend — dat ' + + 'is het adres om een vordering mee te beantwoorden. Het opgegeven adres is wat de ' + + 'node zelf denkt te zijn, vernomen van een STUN-server en naar ons gestuurd; het ' + + 'helpt de node te bereiken en bewijst niets.', + 'admin.tab_blocklist': 'Blokkeerlijst', + + // Admin stats + 'admin.stat_users': 'Gebruikers', + 'admin.stat_groups': 'Groepen', + 'admin.stat_nodes': 'Nodes', + 'admin.stat_online': 'Nodes online', + + // Admin users + 'admin.users_search': 'Gebruikers zoeken...', + 'admin.col_username': 'Gebruikersnaam', + 'admin.col_role': 'Rol', + 'admin.col_status': 'Status', + 'admin.col_created': 'Aangemaakt', + 'admin.col_actions': 'Acties', + 'admin.col_email': 'E-mail', + 'admin.col_groups': 'Groepen', + 'admin.no_users': 'Geen gebruikers gevonden', + 'admin.btn_suspend': 'Schorsen', + 'admin.btn_unsuspend': 'Herstellen', + 'admin.btn_details': 'Details', + 'admin.btn_delete': 'Verwijderen', + 'admin.delete_confirm': 'Het account "{user}" verwijderen? Dit kan niet ongedaan ' + + 'worden gemaakt. De bestanden die deze persoon heeft geüpload blijven op de nodes ' + + 'die ze hosten, en elke node houdt de vastgezette identiteit tot zijn beheerder ' + + 'die weghaalt. Schorsen is de omkeerbare optie.', + 'admin.user_detail': 'Gebruikersgegevens', + 'admin.btn_close': 'Sluiten', + 'admin.self_note': '(u)', + + // Admin groups + 'admin.col_name': 'Naam', + 'admin.col_visibility': 'Zichtbaarheid', + 'admin.col_members': 'Leden', + 'admin.no_groups': 'Geen groepen gevonden', + + // Admin logs + 'admin.col_time': 'Tijd', + 'admin.col_event': 'Gebeurtenis', + 'admin.col_user': 'Gebruiker', + 'admin.col_ip': 'IP', + 'admin.col_detail': 'Detail', + 'admin.filter_all': 'Alle gebeurtenissen', + 'admin.no_logs': 'Geen logboeken gevonden', + 'admin.btn_load_more': 'Meer laden', + + // Admin blocklist + 'admin.col_hash': 'Hash', + 'admin.col_reason': 'Reden', + 'admin.col_date': 'Datum', + 'admin.col_added_by': 'Toegevoegd door', + 'admin.no_blocked': 'Geen geblokkeerde hashes', + 'admin.add_hash': 'Hash aan de blokkeerlijst toevoegen', + 'admin.hash_placeholder': 'blake3-hash (64 hexadecimale tekens)', + 'admin.reason_placeholder': 'Reden', + 'admin.btn_block': 'Blokkeren', + 'admin.btn_unblock': 'Deblokkeren', + + // Notifications + // Create group + 'create_group.title': 'Groep aanmaken', + 'create_group.name': 'Groepsnaam', + 'create_group.name_placeholder': 'bijv. Familiefotos, Projectbestanden...', + 'create_group.visibility': 'Zichtbaarheid', + 'create_group.private': 'Privé', + 'create_group.private_desc': 'Alleen uitgenodigde leden zien de bestanden en hebben er toegang toe', + 'create_group.public': 'Openbaar', + 'create_group.public_desc': 'Iedereen kan deze groep vinden en doorbladeren', + 'create_group.join_policy': 'Toetredingsbeleid', + 'create_group.invite': 'Alleen op uitnodiging', + 'create_group.invite_desc': 'Leden moeten door een beheerder worden uitgenodigd', + 'create_group.open': 'Open (iedereen kan deelnemen)', + 'create_group.open_desc': 'Iedereen kan zonder goedkeuring deelnemen', + 'create_group.description': 'Beschrijving', + 'create_group.description_hint': 'Waar gaat deze groep over? (optioneel)', + 'create_group.submit': 'Aanmaken', + 'create_group.hint': 'Een groep heeft een node nodig om de bestanden te hosten. U ' + + 'kunt de groep nu aanmaken en later een node aansluiten.', + 'create_group.creating': 'Bezig met aanmaken...', + + // Members + 'members.col_role': 'Rol', + 'members.group_role': 'Rol in de groep', + 'members.admin': 'Beheerder', + 'members.group_admin': 'Groepsbeheerder', + 'members.owner': 'Eigenaar', + 'members.member': 'Lid', + 'members.invite_title': 'Lid uitnodigen', + 'members.username_placeholder': 'Gebruikersnaam', + 'members.invite_btn': 'Uitnodigen', + 'members.invite_needs_pairing': 'Uitnodigingen worden ondertekend met de sleutel die ' + + 'deze node voor uw browser heeft vastgezet, dus koppel hem hieronder voordat u ' + + 'iemand uitnodigt.', + 'members.invite_ask_operator': 'Alleen de beheerder van de node die deze groep host ' + + 'kan uitnodigen, vanuit een browser die eraan gekoppeld is.', + 'members.remove': 'Verwijderen', + 'members.remove_confirm': '{user} uit deze groep verwijderen? Het account, de andere ' + + 'groepen en de geüploade bestanden van deze persoon blijven ongemoeid.', + 'members.remove_hint': 'Iemand verwijderen neemt het lidmaatschap weg en zorgt ervoor ' + + 'dat de node de groepssleutel niet meer aan die persoon geeft. Het account wordt ' + + 'er niet mee verwijderd, en de sleutel die de persoon al heeft, komt er niet mee ' + + 'terug — roteer die op de node (meshbay-node gek-init) als dat van belang is.', + 'members.pair_title': 'Deze browser aan uw node koppelen', + 'members.pair_hint': 'Uw node accepteert beheerdersacties — uitnodigingen, bestanden ' + + 'verwijderen — alleen vanuit een browser waarmee hij gekoppeld is. Voer op de ' + + 'node `meshbay-node operator pair` uit en typ de code hier in. De code gaat nooit ' + + 'via de hub.', + 'members.pair_btn': 'Koppelen', + 'members.pair_success': 'Deze browser is nu aan de node gekoppeld.', + 'members.invite_code_ready': 'Uitnodigingscode voor {user} — stuur die op de manier ' + + 'waarop u elkaar gewoonlijk spreekt. Hij werkt één keer en gaat nooit via de hub.', + 'members.invite_code_hint': 'De persoon voert hem in de eerste keer dat deze groep ' + + 'wordt geopend. U hoeft dan niet online te zijn.', + 'transfers.title': 'Overdrachten', + 'transfers.clear': 'Voltooide wissen', + 'transfers.cancel': 'Annuleren', + 'transfers.open': 'Openen', + 'transfers.done': 'Voltooid', + 'transfers.cancelled': 'Geannuleerd', + 'transfers.failed': 'Mislukt', + 'group.select': 'Selecteren', + 'group.select_done': 'Klaar', + 'group.actions': 'Acties ({n})', + 'group.download_n': 'Downloaden ({n})', + 'group.download_zip_n': 'Mappen als zip downloaden ({n})', + 'group.delete_n': 'Verwijderen ({n})', + 'group.delete_n_confirm': { + one: '{n} item verwijderen? {names}', + other: '{n} items verwijderen? {names}', + }, + 'group.download_zip': { + one: 'Als zip downloaden ({n} bestand)', + other: 'Als zip downloaden ({n} bestanden)', + }, + 'group.zip_empty': 'Die map bevat niets om te downloaden.', + 'group.zip_no_stream': 'Deze browser kan een download niet rechtstreeks naar schijf ' + + 'schrijven, dus {name} ({size}) moet eerst in het geheugen worden opgebouwd. Bij ' + + 'een groot archief kan dat mislukken. Doorgaan?', + 'group.rmdir': 'Map verwijderen', + 'group.rmdir_confirm': 'De map "{name}" verwijderen? Ze moet leeg zijn.', + 'group.rmdir_not_empty': 'Die map is niet leeg. Verwijder eerst wat erin staat — ' + + 'niets hier verwijdert bestanden die u niet ziet.', + 'group.desc_edit': 'Beschrijving bewerken', + 'group.desc_add': 'Beschrijving toevoegen', + 'group.desc_save': 'Opslaan', + 'group.desc_cancel': 'Annuleren', + 'group.desc_placeholder': 'Waar deze groep voor dient — leden zien dit op hun ' + + 'startpagina.', + 'group.join_code_title': 'Deze node moet u herkennen', + 'group.join_code_hint': 'Vraag degene die u heeft uitgenodigd om de eenmalige code en ' + + 'voer die hier in. Daarna is deze browser bekend en wordt het u niet opnieuw ' + + 'gevraagd.', + 'group.join_code_btn': 'Deelnemen', + + 'notif.purge': 'Alles wissen', + 'notif.title': 'Meldingen', + 'notif.empty': 'Geen meldingen', + 'notif.mark_all_read': 'Alles als gelezen markeren', + + // Sidebar + 'sidebar.search': 'Bestanden zoeken', + + // Status + + // Search + 'search.title': 'Bestanden zoeken', + 'search.placeholder': 'In alle groepen zoeken...', + 'search.no_results': 'Geen bestand voldoet aan uw zoekopdracht.', + 'search.synced': 'gesynchroniseerd {ago}', + 'search.cache_note': 'De zoekopdracht gaat uit van hoe elke groep eruitzag toen u ' + + 'die voor het laatst opende. Open een groep om de bestanden hier bij te werken.', + 'search.col_group': 'Groep', + 'search.result_count': { + one: '{n} bestand gevonden', + other: '{n} bestanden gevonden', + }, + 'search.hint': 'Zoekt bestandsnamen in alle groepsindexen in de cache.', +}; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js new file mode 100644 index 0000000..d77a160 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -0,0 +1,419 @@ +/** + * Polish — translated from locales/en.js. + * + * Register: formal — the polite impersonal ("Proszę wpisać…", "Można…"), which is + * how formal Polish software addresses the reader; direct "Pan / Pani" is used + * only where a sentence needs a subject. + * `hub`, `node` and `GEK` are left in English: they name the CLI + * (`meshbay-node`), node.toml and the docs, which are English throughout. + * Polish uses four plural categories, so counted strings carry all of + * one / few / many / other. + */ + +export default { + // Nav + 'nav.toggle_menu': 'Pokaż menu', + 'nav.light_mode': 'Motyw jasny', + 'nav.dark_mode': 'Motyw ciemny', + 'nav.logout': 'Wyloguj', + 'nav.login': 'Zaloguj', + + // Sidebar + 'sidebar.my_groups': 'Moje grupy', + 'sidebar.no_groups': 'Nie ma jeszcze żadnych grup', + 'sidebar.discover': 'Odkrywaj', + 'sidebar.public_groups': 'Grupy publiczne', + + // Login + 'login.title': 'Logowanie', + 'login.username': 'Nazwa użytkownika', + 'login.password': 'Hasło', + 'login.submit': 'Zaloguj', + 'login.loading': 'Logowanie...', + 'login.no_account': 'Nie ma jeszcze konta?', + 'login.register_link': 'Załóż konto', + + // Register + 'register.title': 'Zakładanie konta', + 'register.username': 'Nazwa użytkownika', + 'register.email': 'Adres e-mail', + 'register.password': 'Hasło-fraza (co najmniej 12 znaków)', + 'register.confirm': 'Potwierdź hasło', + 'register.submit': 'Załóż konto', + 'register.loading': 'Tworzenie konta...', + 'register.has_account': 'Konto już istnieje?', + 'register.login_link': 'Zaloguj', + 'register.success_title': 'Konto zostało założone', + 'register.success_msg': 'Można się już zalogować przy użyciu swoich danych.', + 'register.go_login': 'Przejdź do logowania', + 'register.err_mismatch': 'Hasła nie są zgodne', + 'register.err_min_len': { + one: 'Proszę użyć co najmniej {n} znaku', + few: 'Proszę użyć co najmniej {n} znaków', + many: 'Proszę użyć co najmniej {n} znaków', + other: 'Proszę użyć co najmniej {n} znaku', + }, + 'register.err_too_weak': 'Zbyt łatwe do odgadnięcia. To hasło-fraza chroni klucze ' + + 'tam, gdzie są przechowywane — kilka niepowiązanych ze sobą słów sprawdza się ' + + 'bardzo dobrze.', + 'register.strength': 'Siła: około {bits} bitów. Chroni kopię kluczy przechowywaną na ' + + 'nodes, do których się dołącza, więc warto ją dobrać starannie.', + + // Home + 'home.welcome': 'Witamy w MeshBay', + 'home.no_groups': 'Nie należy Pan(i) jeszcze do żadnej grupy.', + 'home.browse_prefix': 'Warto przejrzeć ', + 'home.browse_link': 'grupy publiczne', + 'home.browse_suffix': ' albo poprosić administratora grupy o zaproszenie.', + 'home.my_groups': 'Moje grupy', + + // Explore + 'explore.title': 'Grupy publiczne', + 'explore.search': 'Szukaj grup...', + 'explore.loading': 'Wczytywanie...', + 'explore.empty': 'Brak dostępnych grup publicznych.', + 'explore.create_group': 'Utwórz grupę', + 'explore.join': 'Dołącz', + 'explore.member': 'Należy', + + // Group page + 'group.default_name': 'Grupa', + 'group.tab_files': 'Pliki', + 'group.tab_chat': 'Czat', + 'group.tab_members': 'Członkowie', + 'group.filter': 'Filtruj pliki...', + 'group.col_name': 'Nazwa', + 'group.col_size': 'Rozmiar', + 'group.col_type': 'Typ', + 'group.col_date': 'Data', + 'group.empty_filter': 'Żaden plik nie pasuje do filtra', + 'group.empty_dir': 'Ten folder jest pusty', + 'group.download': 'Pobierz', + 'group.play': 'Odtwórz', + 'group.offline_title': 'Obecnie żaden node nie jest dostępny dla tej grupy.', + 'group.offline_hint': 'Pliki pojawią się, gdy połączy się node hostujący tę grupę.', + 'group.upload': 'Wyślij', + 'group.mkdir': 'Nowy folder', + 'group.mkdir_prompt': 'Nazwa nowego folderu:', + 'group.view': 'Podgląd', + 'group.delete': 'Usuń', + 'group.delete_confirm': 'Usunąć {name}?', + 'group.delete_group': 'Usuń grupę', + 'group.delete_group_confirm': 'Trwale usunąć grupę „{name}” i odłączyć wszystkich ' + + 'członków? Pliki znajdujące się na node zostaną zachowane.', + 'group.err_transport': 'Moduł transportu nie został wczytany', + + // Status + 'status.idle': 'Bezczynny', + 'status.discovering': 'Szukanie nodes...', + 'status.connecting': 'Łączenie przez WebRTC...', + 'status.fetching': 'Pobieranie indeksu...', + 'status.files': { + one: '{n} plik', + few: '{n} pliki', + many: '{n} plików', + other: '{n} pliku', + }, + 'status.offline': 'Żaden node nie jest dostępny', + 'status.error': 'Połączenie nie powiodło się', + + // Chat + 'chat.empty': 'Nie ma jeszcze wiadomości. Warto zacząć rozmowę!', + 'chat.placeholder': 'Napisz wiadomość...', + 'chat.send': 'Wyślij', + 'chat.attach': 'Załącz plik', + + // Video player + 'video.loading': 'Wczytywanie {name}...', + 'video.buffering': 'Buforowanie...', + 'video.close': 'Zamknij (Esc)', + 'preview.pdf_fallback': 'Ta przeglądarka nie wyświetli pliku PDF na stronie. Proszę ' + + 'go pobrać — i tak został odszyfrowany tutaj.', + 'group.upload_indexing': 'indeksowanie…', + 'video.err_transport': 'Transport nie jest połączony', + 'video.err_mse': 'Kodek nieobsługiwany przy odtwarzaniu strumieniowym: {codec}', + + // Settings + 'settings.title': 'Ustawienia', + 'settings.coming_soon': 'Wkrótce.', + 'settings.downloads': 'Pobieranie', + 'settings.dl_auto': 'Zapisuj automatycznie', + 'settings.dl_auto_hint': 'Pliki trafiają prosto do folderu wybranego poniżej, bez ' + + 'żadnego okna. Pobranie dwudziestu plików umieści tam dwadzieścia plików.', + 'settings.dl_ask': 'Pytaj za każdym razem', + 'settings.dl_ask_hint': 'Okno „Zapisz jako” dla każdego pliku — po jednym na plik, ' + + 'także przy pobieraniu zaznaczonych pozycji.', + 'settings.dl_folder': 'Folder: {name}', + 'settings.dl_no_folder': 'Nie wybrano folderu — pobrane pliki trafiają tam, gdzie ' + + 'umieszcza je przeglądarka', + 'settings.dl_choose': 'Wybierz folder', + 'settings.dl_change': 'Zmień', + 'settings.dl_forget': 'Zapomnij', + 'settings.dl_path_note': 'Stronie internetowej nie da się podać ścieżki, więc nie ma ' + + 'tu czego wpisywać: to przeglądarka udziela dostępu do wskazanego folderu, a ' + + 'MeshBay zapisuje wyłącznie w jego wnętrzu. Raz na sesję może pojawić się prośba ' + + 'o potwierdzenie.', + 'settings.dl_unsupported': 'Ta przeglądarka nie potrafi zapisywać w dowolnie ' + + 'wybranym folderze (brak File System Access API), więc pobrane pliki trafiają do ' + + 'jej własnego folderu pobierania. Chrome i Edge pozwalają wybrać folder.', + 'settings.profile': 'Profil', + 'settings.username': 'Nazwa użytkownika', + 'settings.node_pins': 'Tożsamości nodes', + 'settings.node_pins_hint': 'Klucz tożsamości każdego node jest zapamiętywany przy ' + + 'pierwszym połączeniu. Jeśli się zmieni, połączenie zostanie odrzucone — czego ' + + 'należy się spodziewać wyłącznie wtedy, gdy operator przeinstalował node. Przed ' + + 'wyczyszczeniem warto to z nim potwierdzić.', + 'settings.node_pins_count': { + one: '{n} przypięta', + few: '{n} przypięte', + many: '{n} przypiętych', + other: '{n} przypiętej', + }, + 'settings.node_pins_clear': 'Wyczyść przypięte tożsamości', + 'settings.danger': 'Usunięcie konta', + 'settings.delete_hint': 'Usuwa z huba konto, członkostwa w grupach i powiadomienia ' + + 'oraz zwalnia nazwę użytkownika. Nie sięga tego, co znajduje się na nodes: ' + + 'wysłane pliki pozostają tam, gdzie przechowuje je ich operator, a każdy node ' + + 'zachowuje przypiętą tożsamość do czasu, aż jego operator ją odepnie. Dzienniki ' + + 'połączeń są przechowywane przez rok, zgodnie z wymogami prawa.', + 'settings.delete_account': 'Usuń moje konto', + 'settings.delete_confirm': 'Proszę wpisać hasło-frazę, aby potwierdzić. Tej operacji ' + + 'nie da się cofnąć.', + 'settings.delete_confirm_btn': 'Usuń bezpowrotnie', + 'settings.cancel': 'Anuluj', + 'settings.appearance': 'Wygląd', + 'settings.theme': 'Motyw', + 'settings.theme_light': 'Jasny', + 'settings.theme_dark': 'Ciemny', + 'settings.theme_system': 'Systemowy', + 'settings.language': 'Język', + 'settings.about': 'O programie', + 'settings.version': 'Wersja', + 'settings.protocol': 'Protokół', + 'settings.role': 'Rola', + 'settings.groups': 'Powiadomienia z grup', + 'settings.notifications': 'Powiadomienia', + 'settings.node_key': 'Powiąż node', + 'settings.node_key_desc': 'Osoby prowadzące własny node MeshBay mogą wkleić tutaj ' + + 'jego klucz publiczny Ed25519, aby powiązać go ze swoim kontem. Klucz ten znajduje ' + + 'się w panelu administracyjnym node.', + 'settings.node_key_placeholder': 'Wklej klucz publiczny Ed25519 node (base64)', + 'settings.node_key_submit': 'Powiąż node', + 'settings.node_key_success': 'Node został powiązany', + 'settings.node_key_current': 'Klucz powiązanego node', + + // Sidebar + 'sidebar.create_group': 'Utwórz grupę', + 'sidebar.settings': 'Ustawienia', + 'sidebar.admin': 'Administracja', + + // User menu + 'usermenu.language': 'Język', + 'usermenu.settings': 'Ustawienia', + 'usermenu.theme_light': 'Motyw jasny', + 'usermenu.theme_dark': 'Motyw ciemny', + 'usermenu.logout': 'Wyloguj', + + // Admin panel + 'admin.title': 'Administracja', + 'admin.tab_stats': 'Statystyki', + 'admin.tab_users': 'Użytkownicy', + 'admin.tab_groups': 'Grupy', + 'admin.tab_nodes': 'Nodes', + 'admin.tab_logs': 'Dzienniki', + 'admin.col_observed_ip': 'Widziany z', + 'admin.col_hint': 'Zgłoszony adres', + 'admin.col_last_seen': 'Ostatnie zgłoszenie', + 'admin.node_online': 'dostępny', + 'admin.node_offline': 'niedostępny', + 'admin.no_nodes': 'Brak zarejestrowanych nodes', + 'admin.nodes_hint': '„Widziany z” to adres, z którego zgłoszenie rzeczywiście ' + + 'dotarło, w żądaniu podpisanym kluczem node — i to na nim należy się opierać, ' + + 'odpowiadając na zapytanie. Zgłoszony adres to ten, który node uważa za własny, ' + + 'poznany od serwera STUN i przesłany do nas; przydaje się do nawiązania ' + + 'połączenia i niczego nie dowodzi.', + 'admin.tab_blocklist': 'Lista blokad', + + // Admin stats + 'admin.stat_users': 'Użytkownicy', + 'admin.stat_groups': 'Grupy', + 'admin.stat_nodes': 'Nodes', + 'admin.stat_online': 'Dostępne nodes', + + // Admin users + 'admin.users_search': 'Szukaj użytkowników...', + 'admin.col_username': 'Nazwa użytkownika', + 'admin.col_role': 'Rola', + 'admin.col_status': 'Status', + 'admin.col_created': 'Utworzono', + 'admin.col_actions': 'Działania', + 'admin.col_email': 'E-mail', + 'admin.col_groups': 'Grupy', + 'admin.no_users': 'Nie znaleziono użytkowników', + 'admin.btn_suspend': 'Zawieś', + 'admin.btn_unsuspend': 'Przywróć', + 'admin.btn_details': 'Szczegóły', + 'admin.btn_delete': 'Usuń', + 'admin.delete_confirm': 'Usunąć konto „{user}”? Tej operacji nie da się cofnąć. ' + + 'Wysłane przez tę osobę pliki pozostaną na hostujących je nodes, a każdy node ' + + 'zachowa przypiętą tożsamość do czasu, aż jego operator ją odepnie. Zawieszenie ' + + 'jest opcją odwracalną.', + 'admin.user_detail': 'Szczegóły użytkownika', + 'admin.btn_close': 'Zamknij', + 'admin.self_note': '(to Pan/Pani)', + + // Admin groups + 'admin.col_name': 'Nazwa', + 'admin.col_visibility': 'Widoczność', + 'admin.col_members': 'Członkowie', + 'admin.no_groups': 'Nie znaleziono grup', + + // Admin logs + 'admin.col_time': 'Czas', + 'admin.col_event': 'Zdarzenie', + 'admin.col_user': 'Użytkownik', + 'admin.col_ip': 'IP', + 'admin.col_detail': 'Szczegóły', + 'admin.filter_all': 'Wszystkie zdarzenia', + 'admin.no_logs': 'Nie znaleziono wpisów', + 'admin.btn_load_more': 'Wczytaj więcej', + + // Admin blocklist + 'admin.col_hash': 'Skrót', + 'admin.col_reason': 'Powód', + 'admin.col_date': 'Data', + 'admin.col_added_by': 'Dodane przez', + 'admin.no_blocked': 'Brak zablokowanych skrótów', + 'admin.add_hash': 'Dodaj skrót do listy blokad', + 'admin.hash_placeholder': 'skrót blake3 (64 znaki szesnastkowe)', + 'admin.reason_placeholder': 'Powód', + 'admin.btn_block': 'Zablokuj', + 'admin.btn_unblock': 'Odblokuj', + + // Notifications + // Create group + 'create_group.title': 'Tworzenie grupy', + 'create_group.name': 'Nazwa grupy', + 'create_group.name_placeholder': 'np. Zdjęcia rodzinne, Pliki projektu...', + 'create_group.visibility': 'Widoczność', + 'create_group.private': 'Prywatna', + 'create_group.private_desc': 'Tylko zaproszeni członkowie widzą pliki i mają do nich dostęp', + 'create_group.public': 'Publiczna', + 'create_group.public_desc': 'Każdy może odnaleźć i przeglądać tę grupę', + 'create_group.join_policy': 'Zasady dołączania', + 'create_group.invite': 'Tylko z zaproszeniem', + 'create_group.invite_desc': 'Członków musi zaprosić administrator', + 'create_group.open': 'Otwarta (każdy może dołączyć)', + 'create_group.open_desc': 'Każdy może dołączyć bez zatwierdzenia', + 'create_group.description': 'Opis', + 'create_group.description_hint': 'Czego dotyczy ta grupa? (opcjonalnie)', + 'create_group.submit': 'Utwórz', + 'create_group.hint': 'Grupa potrzebuje node, który będzie hostował pliki. Można ' + + 'utworzyć grupę teraz, a node podłączyć później.', + 'create_group.creating': 'Tworzenie...', + + // Members + 'members.col_role': 'Rola', + 'members.group_role': 'Rola w grupie', + 'members.admin': 'Administrator', + 'members.group_admin': 'Administrator grupy', + 'members.owner': 'Właściciel', + 'members.member': 'Członek', + 'members.invite_title': 'Zaproszenie członka', + 'members.username_placeholder': 'Nazwa użytkownika', + 'members.invite_btn': 'Zaproś', + 'members.invite_needs_pairing': 'Zaproszenia są podpisywane kluczem, który ten node ' + + 'przypiął dla tej przeglądarki — proszę najpierw sparować ją poniżej, zanim ' + + 'kogokolwiek Pan(i) zaprosi.', + 'members.invite_ask_operator': 'Zapraszać może wyłącznie operator node hostującego tę ' + + 'grupę, z przeglądarki sparowanej z tym node.', + 'members.remove': 'Usuń', + 'members.remove_confirm': 'Usunąć użytkownika {user} z tej grupy? Jego konto, ' + + 'pozostałe grupy i wysłane przez niego pliki pozostaną nienaruszone.', + 'members.remove_hint': 'Usunięcie kogoś odbiera mu członkostwo i sprawia, że node ' + + 'przestaje wydawać mu klucz grupy. Nie usuwa to jego konta ani nie odbiera ' + + 'klucza, który już posiada — jeśli to istotne, należy zmienić klucz na node ' + + '(meshbay-node gek-init).', + 'members.pair_title': 'Sparowanie tej przeglądarki z Pana/Pani node', + 'members.pair_hint': 'Node przyjmuje działania operatora — zaproszenia, usuwanie ' + + 'plików — wyłącznie z przeglądarki, z którą został sparowany. Proszę uruchomić na ' + + 'node polecenie `meshbay-node operator pair` i wpisać tutaj kod. Kod nigdy nie ' + + 'przechodzi przez hub.', + 'members.pair_btn': 'Sparuj', + 'members.pair_success': 'Ta przeglądarka jest już sparowana z node.', + 'members.invite_code_ready': 'Kod zaproszenia dla użytkownika {user} — proszę ' + + 'przekazać go tak, jak zwykle się Państwo kontaktują. Działa jednorazowo i nigdy ' + + 'nie przechodzi przez hub.', + 'members.invite_code_hint': 'Osoba zaproszona wpisze go, otwierając tę grupę po raz ' + + 'pierwszy. Nie trzeba być wtedy dostępnym.', + 'transfers.title': 'Transfery', + 'transfers.clear': 'Wyczyść zakończone', + 'transfers.cancel': 'Anuluj', + 'transfers.open': 'Otwórz', + 'transfers.done': 'Zakończony', + 'transfers.cancelled': 'Anulowany', + 'transfers.failed': 'Nie powiódł się', + 'group.select': 'Zaznacz', + 'group.select_done': 'Gotowe', + 'group.actions': 'Działania ({n})', + 'group.download_n': 'Pobierz ({n})', + 'group.download_zip_n': 'Pobierz foldery jako zip ({n})', + 'group.delete_n': 'Usuń ({n})', + 'group.delete_n_confirm': { + one: 'Usunąć {n} element? {names}', + few: 'Usunąć {n} elementy? {names}', + many: 'Usunąć {n} elementów? {names}', + other: 'Usunąć {n} elementu? {names}', + }, + 'group.download_zip': { + one: 'Pobierz jako zip ({n} plik)', + few: 'Pobierz jako zip ({n} pliki)', + many: 'Pobierz jako zip ({n} plików)', + other: 'Pobierz jako zip ({n} pliku)', + }, + 'group.zip_empty': 'W tym folderze nie ma nic do pobrania.', + 'group.zip_no_stream': 'Ta przeglądarka nie potrafi zapisywać pobieranych danych ' + + 'wprost na dysk, więc {name} ({size}) trzeba najpierw zbudować w pamięci. Przy ' + + 'dużym archiwum może się to nie udać. Kontynuować?', + 'group.rmdir': 'Usuń folder', + 'group.rmdir_confirm': 'Usunąć folder „{name}”? Musi być pusty.', + 'group.rmdir_not_empty': 'Ten folder nie jest pusty. Proszę najpierw usunąć jego ' + + 'zawartość — nic tutaj nie kasuje plików, których nie widać.', + 'group.desc_edit': 'Edytuj opis', + 'group.desc_add': 'Dodaj opis', + 'group.desc_save': 'Zapisz', + 'group.desc_cancel': 'Anuluj', + 'group.desc_placeholder': 'Do czego służy ta grupa — członkowie zobaczą to na swojej ' + + 'stronie głównej.', + 'group.join_code_title': 'Ten node musi Pana/Panią rozpoznać', + 'group.join_code_hint': 'Proszę poprosić osobę zapraszającą o kod jednorazowy i ' + + 'wpisać go tutaj. Od tej pory przeglądarka jest rozpoznawana i pytanie się nie ' + + 'powtórzy.', + 'group.join_code_btn': 'Dołącz', + + 'notif.purge': 'Wyczyść wszystko', + 'notif.title': 'Powiadomienia', + 'notif.empty': 'Brak powiadomień', + 'notif.mark_all_read': 'Oznacz wszystkie jako przeczytane', + + // Sidebar + 'sidebar.search': 'Szukaj plików', + + // Status + + // Search + 'search.title': 'Wyszukiwanie plików', + 'search.placeholder': 'Szukaj we wszystkich grupach...', + 'search.no_results': 'Żaden plik nie pasuje do zapytania.', + 'search.synced': 'zsynchronizowano {ago}', + 'search.cache_note': 'Wyszukiwanie opiera się na stanie każdej grupy z chwili jej ' + + 'ostatniego otwarcia. Otwarcie grupy odświeża tutaj jej pliki.', + 'search.col_group': 'Grupa', + 'search.result_count': { + one: 'Znaleziono {n} plik', + few: 'Znaleziono {n} pliki', + many: 'Znaleziono {n} plików', + other: 'Znaleziono {n} pliku', + }, + 'search.hint': 'Przeszukuje nazwy plików we wszystkich zbuforowanych indeksach grup.', +}; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js new file mode 100644 index 0000000..9d00ece --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -0,0 +1,403 @@ +/** + * Brazilian Portuguese — translated from locales/en.js. + * + * Register: formal. In Brazilian Portuguese that is "você" with polite phrasing, + * not "o senhor / a senhora" — the latter reads as stiff in software and is + * reserved for addressing someone markedly senior. + * `hub`, `node` and `GEK` are left in English: they name the CLI + * (`meshbay-node`), node.toml and the docs, which are English throughout. + */ + +export default { + // Nav + 'nav.toggle_menu': 'Exibir o menu', + 'nav.light_mode': 'Tema claro', + 'nav.dark_mode': 'Tema escuro', + 'nav.logout': 'Sair', + 'nav.login': 'Entrar', + + // Sidebar + 'sidebar.my_groups': 'Meus grupos', + 'sidebar.no_groups': 'Nenhum grupo ainda', + 'sidebar.discover': 'Descobrir', + 'sidebar.public_groups': 'Grupos públicos', + + // Login + 'login.title': 'Entrar', + 'login.username': 'Nome de usuário', + 'login.password': 'Senha', + 'login.submit': 'Entrar', + 'login.loading': 'Entrando...', + 'login.no_account': 'Ainda não tem conta?', + 'login.register_link': 'Criar uma conta', + + // Register + 'register.title': 'Criar uma conta', + 'register.username': 'Nome de usuário', + 'register.email': 'E-mail', + 'register.password': 'Frase secreta (mínimo de 12 caracteres)', + 'register.confirm': 'Confirmar a senha', + 'register.submit': 'Criar a conta', + 'register.loading': 'Criando a conta...', + 'register.has_account': 'Já tem uma conta?', + 'register.login_link': 'Entrar', + 'register.success_title': 'Conta criada', + 'register.success_msg': 'Agora você pode entrar com suas credenciais.', + 'register.go_login': 'Ir para o login', + 'register.err_mismatch': 'As senhas não coincidem', + 'register.err_min_len': { + one: 'Use pelo menos {n} caractere', + other: 'Use pelo menos {n} caracteres', + }, + 'register.err_too_weak': 'Fácil demais de adivinhar. Sua frase secreta é o que ' + + 'protege suas chaves onde elas ficam guardadas — algumas palavras sem relação ' + + 'entre si funcionam muito bem.', + 'register.strength': 'Robustez: cerca de {bits} bits. Ela protege a cópia das suas ' + + 'chaves mantida nos nodes dos quais você participa, então vale a pena caprichar.', + + // Home + 'home.welcome': 'Bem-vindo ao MeshBay', + 'home.no_groups': 'Você ainda não participa de nenhum grupo.', + 'home.browse_prefix': 'Explore os ', + 'home.browse_link': 'grupos públicos', + 'home.browse_suffix': ' ou peça a um administrador de grupo que o convide.', + 'home.my_groups': 'Meus grupos', + + // Explore + 'explore.title': 'Grupos públicos', + 'explore.search': 'Pesquisar grupos...', + 'explore.loading': 'Carregando...', + 'explore.empty': 'Nenhum grupo público disponível.', + 'explore.create_group': 'Criar um grupo', + 'explore.join': 'Participar', + 'explore.member': 'Participando', + + // Group page + 'group.default_name': 'Grupo', + 'group.tab_files': 'Arquivos', + 'group.tab_chat': 'Conversa', + 'group.tab_members': 'Membros', + 'group.filter': 'Filtrar arquivos...', + 'group.col_name': 'Nome', + 'group.col_size': 'Tamanho', + 'group.col_type': 'Tipo', + 'group.col_date': 'Data', + 'group.empty_filter': 'Nenhum arquivo corresponde ao filtro', + 'group.empty_dir': 'Esta pasta está vazia', + 'group.download': 'Baixar', + 'group.play': 'Reproduzir', + 'group.offline_title': 'Nenhum node está on-line para este grupo no momento.', + 'group.offline_hint': 'Os arquivos aparecerão quando um node que hospeda este grupo ' + + 'se conectar.', + 'group.upload': 'Enviar', + 'group.mkdir': 'Nova pasta', + 'group.mkdir_prompt': 'Nome da nova pasta:', + 'group.view': 'Visualizar', + 'group.delete': 'Excluir', + 'group.delete_confirm': 'Excluir {name}?', + 'group.delete_group': 'Excluir o grupo', + 'group.delete_group_confirm': 'Excluir "{name}" definitivamente e remover todos os ' + + 'membros? Os arquivos que estão no node são preservados.', + 'group.err_transport': 'Módulo de transporte não carregado', + + // Status + 'status.idle': 'Ocioso', + 'status.discovering': 'Procurando nodes...', + 'status.connecting': 'Conectando via WebRTC...', + 'status.fetching': 'Obtendo o índice...', + 'status.files': { + one: '{n} arquivo', + other: '{n} arquivos', + }, + 'status.offline': 'Nenhum node on-line', + 'status.error': 'Falha na conexão', + + // Chat + 'chat.empty': 'Ainda não há mensagens. Comece a conversa!', + 'chat.placeholder': 'Escreva uma mensagem...', + 'chat.send': 'Enviar', + 'chat.attach': 'Anexar arquivo', + + // Video player + 'video.loading': 'Carregando {name}...', + 'video.buffering': 'Armazenando em buffer...', + 'video.close': 'Fechar (Esc)', + 'preview.pdf_fallback': 'Este navegador não exibirá o PDF na própria página. Baixe ' + + 'o arquivo — de todo modo ele foi descriptografado aqui.', + 'group.upload_indexing': 'indexando…', + 'video.err_transport': 'Transporte não conectado', + 'video.err_mse': 'Codec sem suporte para transmissão: {codec}', + + // Settings + 'settings.title': 'Configurações', + 'settings.coming_soon': 'Em breve.', + 'settings.downloads': 'Downloads', + 'settings.dl_auto': 'Salvar automaticamente', + 'settings.dl_auto_hint': 'Os arquivos vão direto para a pasta escolhida abaixo, sem ' + + 'nenhuma caixa de diálogo. Baixar vinte arquivos coloca vinte arquivos lá.', + 'settings.dl_ask': 'Perguntar sempre', + 'settings.dl_ask_hint': 'Uma caixa "Salvar como" por arquivo — uma para cada ' + + 'arquivo, inclusive quando você baixa uma seleção.', + 'settings.dl_folder': 'Pasta: {name}', + 'settings.dl_no_folder': 'Nenhuma pasta escolhida — os downloads vão para onde o ' + + 'seu navegador os colocar', + 'settings.dl_choose': 'Escolher pasta', + 'settings.dl_change': 'Alterar', + 'settings.dl_forget': 'Esquecer', + 'settings.dl_path_note': 'Não é possível informar um caminho a uma página web, ' + + 'portanto não há nada a digitar aqui: o seu navegador concede acesso à pasta que ' + + 'você indicar, e o MeshBay só escreve dentro dela. Talvez seja pedida uma ' + + 'confirmação uma vez por sessão.', + 'settings.dl_unsupported': 'Este navegador não consegue escrever em uma pasta de sua ' + + 'escolha (não tem a File System Access API), então os downloads vão para a pasta ' + + 'de downloads dele. Chrome e Edge permitem escolher uma.', + 'settings.profile': 'Perfil', + 'settings.username': 'Nome de usuário', + 'settings.node_pins': 'Identidades dos nodes', + 'settings.node_pins_hint': 'A chave de identidade de cada node é memorizada na ' + + 'primeira conexão. Se ela mudar, a conexão é recusada — o que só é esperado ' + + 'quando um operador reinstala um node. Confirme com ele antes de limpar.', + 'settings.node_pins_count': { + one: '{n} fixada', + other: '{n} fixadas', + }, + 'settings.node_pins_clear': 'Limpar as identidades fixadas', + 'settings.danger': 'Excluir a conta', + 'settings.delete_hint': 'Remove sua conta, suas participações em grupos e suas ' + + 'notificações do hub, e libera seu nome de usuário. Não alcança o que está nos ' + + 'nodes: os arquivos que você enviou permanecem onde o operador deles os guarda, e ' + + 'cada node mantém a identidade que fixou para você até que o operador a remova. ' + + 'Os registros de conexão são guardados por um ano, como a lei exige.', + 'settings.delete_account': 'Excluir minha conta', + 'settings.delete_confirm': 'Digite sua frase secreta para confirmar. Esta ação não ' + + 'pode ser desfeita.', + 'settings.delete_confirm_btn': 'Excluir definitivamente', + 'settings.cancel': 'Cancelar', + 'settings.appearance': 'Aparência', + 'settings.theme': 'Tema', + 'settings.theme_light': 'Claro', + 'settings.theme_dark': 'Escuro', + 'settings.theme_system': 'Sistema', + 'settings.language': 'Idioma', + 'settings.about': 'Sobre', + 'settings.version': 'Versão', + 'settings.protocol': 'Protocolo', + 'settings.role': 'Função', + 'settings.groups': 'Notificações de grupo', + 'settings.notifications': 'Notificações', + 'settings.node_key': 'Vincular um node', + 'settings.node_key_desc': 'Se você opera um node MeshBay, cole aqui a chave pública ' + + 'Ed25519 dele para vinculá-lo à sua conta. Essa chave está no painel de ' + + 'administração do seu node.', + 'settings.node_key_placeholder': 'Cole a chave pública Ed25519 do node (base64)', + 'settings.node_key_submit': 'Vincular o node', + 'settings.node_key_success': 'Node vinculado com sucesso', + 'settings.node_key_current': 'Chave do node vinculado', + + // Sidebar + 'sidebar.create_group': 'Criar um grupo', + 'sidebar.settings': 'Configurações', + 'sidebar.admin': 'Administração', + + // User menu + 'usermenu.language': 'Idioma', + 'usermenu.settings': 'Configurações', + 'usermenu.theme_light': 'Tema claro', + 'usermenu.theme_dark': 'Tema escuro', + 'usermenu.logout': 'Sair', + + // Admin panel + 'admin.title': 'Administração', + 'admin.tab_stats': 'Estatísticas', + 'admin.tab_users': 'Usuários', + 'admin.tab_groups': 'Grupos', + 'admin.tab_nodes': 'Nodes', + 'admin.tab_logs': 'Registros', + 'admin.col_observed_ip': 'Visto de', + 'admin.col_hint': 'Endereço anunciado', + 'admin.col_last_seen': 'Último anúncio', + 'admin.node_online': 'on-line', + 'admin.node_offline': 'off-line', + 'admin.no_nodes': 'Nenhum node registrado', + 'admin.nodes_hint': '"Visto de" é o endereço de onde o anúncio chegou, em uma ' + + 'requisição assinada com a chave do node — é esse que serve para responder a uma ' + + 'requisição judicial. O endereço anunciado é o que o node acredita ser o seu, ' + + 'aprendido de um servidor STUN e enviado a nós; serve para alcançar o node e não ' + + 'prova coisa alguma.', + 'admin.tab_blocklist': 'Lista de bloqueio', + + // Admin stats + 'admin.stat_users': 'Usuários', + 'admin.stat_groups': 'Grupos', + 'admin.stat_nodes': 'Nodes', + 'admin.stat_online': 'Nodes on-line', + + // Admin users + 'admin.users_search': 'Pesquisar usuários...', + 'admin.col_username': 'Nome de usuário', + 'admin.col_role': 'Função', + 'admin.col_status': 'Status', + 'admin.col_created': 'Criação', + 'admin.col_actions': 'Ações', + 'admin.col_email': 'E-mail', + 'admin.col_groups': 'Grupos', + 'admin.no_users': 'Nenhum usuário encontrado', + 'admin.btn_suspend': 'Suspender', + 'admin.btn_unsuspend': 'Reativar', + 'admin.btn_details': 'Detalhes', + 'admin.btn_delete': 'Excluir', + 'admin.delete_confirm': 'Excluir a conta "{user}"? Esta ação não pode ser desfeita. ' + + 'Os arquivos que essa pessoa enviou permanecem nos nodes que os hospedam, e cada ' + + 'node mantém a identidade que fixou até que o operador dele a remova. Suspender é ' + + 'a opção reversível.', + 'admin.user_detail': 'Detalhes do usuário', + 'admin.btn_close': 'Fechar', + 'admin.self_note': '(você)', + + // Admin groups + 'admin.col_name': 'Nome', + 'admin.col_visibility': 'Visibilidade', + 'admin.col_members': 'Membros', + 'admin.no_groups': 'Nenhum grupo encontrado', + + // Admin logs + 'admin.col_time': 'Horário', + 'admin.col_event': 'Evento', + 'admin.col_user': 'Usuário', + 'admin.col_ip': 'IP', + 'admin.col_detail': 'Detalhe', + 'admin.filter_all': 'Todos os eventos', + 'admin.no_logs': 'Nenhum registro encontrado', + 'admin.btn_load_more': 'Carregar mais', + + // Admin blocklist + 'admin.col_hash': 'Hash', + 'admin.col_reason': 'Motivo', + 'admin.col_date': 'Data', + 'admin.col_added_by': 'Adicionado por', + 'admin.no_blocked': 'Nenhum hash bloqueado', + 'admin.add_hash': 'Adicionar um hash à lista de bloqueio', + 'admin.hash_placeholder': 'hash blake3 (64 caracteres hexadecimais)', + 'admin.reason_placeholder': 'Motivo', + 'admin.btn_block': 'Bloquear', + 'admin.btn_unblock': 'Desbloquear', + + // Notifications + // Create group + 'create_group.title': 'Criar um grupo', + 'create_group.name': 'Nome do grupo', + 'create_group.name_placeholder': 'ex.: Fotos da família, Arquivos do projeto...', + 'create_group.visibility': 'Visibilidade', + 'create_group.private': 'Privado', + 'create_group.private_desc': 'Só os membros convidados veem os arquivos e acessam a eles', + 'create_group.public': 'Público', + 'create_group.public_desc': 'Qualquer pessoa pode descobrir e explorar este grupo', + 'create_group.join_policy': 'Política de entrada', + 'create_group.invite': 'Somente por convite', + 'create_group.invite_desc': 'Os membros precisam ser convidados por um administrador', + 'create_group.open': 'Aberto (qualquer pessoa pode entrar)', + 'create_group.open_desc': 'Qualquer pessoa pode entrar sem aprovação', + 'create_group.description': 'Descrição', + 'create_group.description_hint': 'Sobre o que é este grupo? (opcional)', + 'create_group.submit': 'Criar', + 'create_group.hint': 'Um grupo precisa de um node para hospedar os arquivos. Você ' + + 'pode criar o grupo agora e conectar um node depois.', + 'create_group.creating': 'Criando...', + + // Members + 'members.col_role': 'Função', + 'members.group_role': 'Função no grupo', + 'members.admin': 'Administrador', + 'members.group_admin': 'Administrador do grupo', + 'members.owner': 'Proprietário', + 'members.member': 'Membro', + 'members.invite_title': 'Convidar um membro', + 'members.username_placeholder': 'Nome de usuário', + 'members.invite_btn': 'Convidar', + 'members.invite_needs_pairing': 'Os convites são assinados com a chave que este node ' + + 'fixou para o seu navegador, então faça o pareamento abaixo antes de convidar ' + + 'alguém.', + 'members.invite_ask_operator': 'Só o operador do node que hospeda este grupo pode ' + + 'convidar, a partir de um navegador pareado com ele.', + 'members.remove': 'Remover', + 'members.remove_confirm': 'Remover {user} deste grupo? A conta dessa pessoa, os ' + + 'outros grupos dela e os arquivos que ela enviou não são afetados.', + 'members.remove_hint': 'Remover alguém retira a participação dessa pessoa e faz o ' + + 'node parar de entregar a ela a chave do grupo. Não exclui a conta dela, e não ' + + 'recupera a chave que ela já tem em mãos — rotacione a chave no node ' + + '(meshbay-node gek-init) se isso importar.', + 'members.pair_title': 'Parear este navegador com o seu node', + 'members.pair_hint': 'O seu node só aceita ações de operador — convites, exclusão de ' + + 'arquivos — de um navegador com o qual foi pareado. Execute ' + + '`meshbay-node operator pair` no node e digite o código aqui. O código nunca ' + + 'passa pelo hub.', + 'members.pair_btn': 'Parear', + 'members.pair_success': 'Este navegador agora está pareado com o node.', + 'members.invite_code_ready': 'Código de convite para {user} — envie a ele pelo meio ' + + 'que vocês costumam usar. Funciona uma única vez e nunca passa pelo hub.', + 'members.invite_code_hint': 'A pessoa digita o código na primeira vez que abrir este ' + + 'grupo. Você não precisa estar on-line nesse momento.', + 'transfers.title': 'Transferências', + 'transfers.clear': 'Limpar as concluídas', + 'transfers.cancel': 'Cancelar', + 'transfers.open': 'Abrir', + 'transfers.done': 'Concluída', + 'transfers.cancelled': 'Cancelada', + 'transfers.failed': 'Falhou', + 'group.select': 'Selecionar', + 'group.select_done': 'Concluir', + 'group.actions': 'Ações ({n})', + 'group.download_n': 'Baixar ({n})', + 'group.download_zip_n': 'Baixar as pastas em zip ({n})', + 'group.delete_n': 'Excluir ({n})', + 'group.delete_n_confirm': { + one: 'Excluir {n} item? {names}', + other: 'Excluir {n} itens? {names}', + }, + 'group.download_zip': { + one: 'Baixar em zip ({n} arquivo)', + other: 'Baixar em zip ({n} arquivos)', + }, + 'group.zip_empty': 'Essa pasta não tem nada para baixar.', + 'group.zip_no_stream': 'Este navegador não consegue escrever um download direto no ' + + 'disco, então {name} ({size}) precisa ser montado antes na memória. Em um arquivo ' + + 'grande isso pode falhar. Continuar?', + 'group.rmdir': 'Excluir a pasta', + 'group.rmdir_confirm': 'Excluir a pasta "{name}"? Ela precisa estar vazia.', + 'group.rmdir_not_empty': 'Essa pasta não está vazia. Exclua antes o que está dentro ' + + 'dela — nada aqui apaga arquivos que você não vê.', + 'group.desc_edit': 'Editar a descrição', + 'group.desc_add': 'Adicionar uma descrição', + 'group.desc_save': 'Salvar', + 'group.desc_cancel': 'Cancelar', + 'group.desc_placeholder': 'Para que serve este grupo — os membros veem isto na ' + + 'página inicial deles.', + 'group.join_code_title': 'Este node precisa reconhecer você', + 'group.join_code_hint': 'Peça o código de uso único a quem convidou você e digite-o ' + + 'aqui. Depois disso este navegador fica reconhecido e a pergunta não se repete.', + 'group.join_code_btn': 'Participar', + + 'notif.purge': 'Limpar tudo', + 'notif.title': 'Notificações', + 'notif.empty': 'Nenhuma notificação', + 'notif.mark_all_read': 'Marcar tudo como lido', + + // Sidebar + 'sidebar.search': 'Pesquisar arquivos', + + // Status + + // Search + 'search.title': 'Pesquisar arquivos', + 'search.placeholder': 'Pesquisar em todos os grupos...', + 'search.no_results': 'Nenhum arquivo corresponde à sua pesquisa.', + 'search.synced': 'sincronizado {ago}', + 'search.cache_note': 'A pesquisa usa o estado de cada grupo na última vez que você o ' + + 'abriu. Abra um grupo para atualizar os arquivos dele aqui.', + 'search.col_group': 'Grupo', + 'search.result_count': { + one: '{n} arquivo encontrado', + other: '{n} arquivos encontrados', + }, + 'search.hint': 'Pesquisa nomes de arquivo em todos os índices de grupo em cache.', +}; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js new file mode 100644 index 0000000..bfa6d05 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -0,0 +1,376 @@ +/** + * Simplified Chinese — translated from locales/en.js. + * + * Register: formal — the reader is addressed as 您 throughout. + * `hub`, `node` and `GEK` are left in English: they name the CLI + * (`meshbay-node`), node.toml and the docs, which are English throughout. + * Chinese has a single plural category, so counted strings need only `other`. + */ + +export default { + // Nav + 'nav.toggle_menu': '显示菜单', + 'nav.light_mode': '浅色主题', + 'nav.dark_mode': '深色主题', + 'nav.logout': '退出登录', + 'nav.login': '登录', + + // Sidebar + 'sidebar.my_groups': '我的群组', + 'sidebar.no_groups': '暂无群组', + 'sidebar.discover': '发现', + 'sidebar.public_groups': '公开群组', + + // Login + 'login.title': '登录', + 'login.username': '用户名', + 'login.password': '密码', + 'login.submit': '登录', + 'login.loading': '正在登录…', + 'login.no_account': '还没有账户?', + 'login.register_link': '注册', + + // Register + 'register.title': '注册', + 'register.username': '用户名', + 'register.email': '电子邮箱', + 'register.password': '密码短语(至少 12 个字符)', + 'register.confirm': '确认密码', + 'register.submit': '注册', + 'register.loading': '正在创建账户…', + 'register.has_account': '已经有账户了?', + 'register.login_link': '登录', + 'register.success_title': '账户已创建', + 'register.success_msg': '您现在可以使用您的凭据登录了。', + 'register.go_login': '前往登录', + 'register.err_mismatch': '两次输入的密码不一致', + 'register.err_min_len': { + other: '请至少使用 {n} 个字符', + }, + 'register.err_too_weak': '太容易被猜到了。您的密码短语正是保护密钥存放之处的东西——' + + '几个互不相关的词就很好用。', + 'register.strength': '强度:约 {bits} 位。它保护的是您所加入的各个 node 上保存的密钥副本,' + + '因此值得认真设置。', + + // Home + 'home.welcome': '欢迎使用 MeshBay', + 'home.no_groups': '您还不是任何群组的成员。', + 'home.browse_prefix': '浏览', + 'home.browse_link': '公开群组', + 'home.browse_suffix': ',或请群组管理员邀请您。', + 'home.my_groups': '我的群组', + + // Explore + 'explore.title': '公开群组', + 'explore.search': '搜索群组…', + 'explore.loading': '加载中…', + 'explore.empty': '暂无可用的公开群组。', + 'explore.create_group': '创建群组', + 'explore.join': '加入', + 'explore.member': '已加入', + + // Group page + 'group.default_name': '群组', + 'group.tab_files': '文件', + 'group.tab_chat': '聊天', + 'group.tab_members': '成员', + 'group.filter': '筛选文件…', + 'group.col_name': '名称', + 'group.col_size': '大小', + 'group.col_type': '类型', + 'group.col_date': '日期', + 'group.empty_filter': '没有文件符合筛选条件', + 'group.empty_dir': '此文件夹为空', + 'group.download': '下载', + 'group.play': '播放', + 'group.offline_title': '当前没有为此群组提供服务的 node 在线。', + 'group.offline_hint': '当托管此群组的 node 连接后,文件就会出现。', + 'group.upload': '上传', + 'group.mkdir': '新建文件夹', + 'group.mkdir_prompt': '新文件夹的名称:', + 'group.view': '查看', + 'group.delete': '删除', + 'group.delete_confirm': '删除 {name}?', + 'group.delete_group': '删除群组', + 'group.delete_group_confirm': '永久删除“{name}”并移除所有成员?node 上的文件会被保留。', + 'group.err_transport': '传输模块未加载', + + // Status + 'status.idle': '空闲', + 'status.discovering': '正在查找 node…', + 'status.connecting': '正在通过 WebRTC 连接…', + 'status.fetching': '正在获取索引…', + 'status.files': { + other: '{n} 个文件', + }, + 'status.offline': '没有 node 在线', + 'status.error': '连接失败', + + // Chat + 'chat.empty': '还没有消息。开始聊天吧!', + 'chat.placeholder': '输入消息…', + 'chat.send': '发送', + 'chat.attach': '添加附件', + + // Video player + 'video.loading': '正在加载 {name}…', + 'video.buffering': '正在缓冲…', + 'video.close': '关闭(Esc)', + 'preview.pdf_fallback': '此浏览器不会在页面内显示该 PDF。请改为下载——无论如何它都已在本地解密。', + 'group.upload_indexing': '建立索引中…', + 'video.err_transport': '传输未连接', + 'video.err_mse': '该编解码器不支持流式播放:{codec}', + + // Settings + 'settings.title': '设置', + 'settings.coming_soon': '即将推出。', + 'settings.downloads': '下载', + 'settings.dl_auto': '自动保存', + 'settings.dl_auto_hint': '文件会直接进入您在下方选择的文件夹,不弹出任何对话框。' + + '下载二十个文件,那里就会多出二十个文件。', + 'settings.dl_ask': '每次都询问', + 'settings.dl_ask_hint': '每个文件弹出一次“另存为”对话框——每个文件一次,' + + '批量下载时也是如此。', + 'settings.dl_folder': '文件夹:{name}', + 'settings.dl_no_folder': '未选择文件夹——下载内容会保存到浏览器指定的位置', + 'settings.dl_choose': '选择文件夹', + 'settings.dl_change': '更改', + 'settings.dl_forget': '忘记', + 'settings.dl_path_note': '网页无法被指定一个路径,因此这里没有什么需要输入:' + + '由您的浏览器授予对所选文件夹的访问权限,而 MeshBay 只会写入该文件夹之内。' + + '每个会话可能会要求您确认一次。', + 'settings.dl_unsupported': '此浏览器无法写入您指定的文件夹(不支持 File System ' + + 'Access API),因此下载内容会进入它自己的下载文件夹。Chrome 和 Edge 支持自行选择。', + 'settings.profile': '个人资料', + 'settings.username': '用户名', + 'settings.node_pins': 'node 身份', + 'settings.node_pins_hint': '每个 node 的身份密钥都会在您首次连接时被记住。' + + '如果它发生变化,连接会被拒绝——只有当运营者重装 node 时才应如此。' + + '清除之前请先与对方核实。', + 'settings.node_pins_count': { + other: '已固定 {n} 个', + }, + 'settings.node_pins_clear': '清除已固定的身份', + 'settings.danger': '删除账户', + 'settings.delete_hint': '这会从 hub 上删除您的账户、您的群组成员身份和您的通知,' + + '并释放您的用户名。它无法触及存放在各个 node 上的内容:您上传的文件仍留在其运营者' + + '保管之处,每个 node 也会保留它为您固定的身份,直到其运营者取消固定。' + + '连接日志按法律要求保存一年。', + 'settings.delete_account': '删除我的账户', + 'settings.delete_confirm': '请输入您的密码短语以确认。此操作无法撤销。', + 'settings.delete_confirm_btn': '永久删除', + 'settings.cancel': '取消', + 'settings.appearance': '外观', + 'settings.theme': '主题', + 'settings.theme_light': '浅色', + 'settings.theme_dark': '深色', + 'settings.theme_system': '跟随系统', + 'settings.language': '语言', + 'settings.about': '关于', + 'settings.version': '版本', + 'settings.protocol': '协议', + 'settings.role': '角色', + 'settings.groups': '群组通知', + 'settings.notifications': '通知', + 'settings.node_key': '关联 node', + 'settings.node_key_desc': '如果您运营着一个 MeshBay node,请在此粘贴它的 Ed25519 ' + + '公钥,以将其关联到您的账户。该密钥可在您 node 的管理面板中找到。', + 'settings.node_key_placeholder': '粘贴 node 的 Ed25519 公钥(base64)', + 'settings.node_key_submit': '关联 node', + 'settings.node_key_success': 'node 关联成功', + 'settings.node_key_current': '已关联的 node 密钥', + + // Sidebar + 'sidebar.create_group': '创建群组', + 'sidebar.settings': '设置', + 'sidebar.admin': '管理', + + // User menu + 'usermenu.language': '语言', + 'usermenu.settings': '设置', + 'usermenu.theme_light': '浅色主题', + 'usermenu.theme_dark': '深色主题', + 'usermenu.logout': '退出登录', + + // Admin panel + 'admin.title': '管理', + 'admin.tab_stats': '统计', + 'admin.tab_users': '用户', + 'admin.tab_groups': '群组', + 'admin.tab_nodes': 'node', + 'admin.tab_logs': '日志', + 'admin.col_observed_ip': '来源地址', + 'admin.col_hint': '自报地址', + 'admin.col_last_seen': '最近通告', + 'admin.node_online': '在线', + 'admin.node_offline': '离线', + 'admin.no_nodes': '没有已注册的 node', + 'admin.nodes_hint': '“来源地址”是该通告实际到达时的地址,请求由 node 密钥签名——' + + '需要答复查询时应以此为准。自报地址是 node 自认为的地址,从 STUN 服务器获知后发给我们;' + + '它有助于连上该 node,但不能证明任何事情。', + 'admin.tab_blocklist': '屏蔽列表', + + // Admin stats + 'admin.stat_users': '用户', + 'admin.stat_groups': '群组', + 'admin.stat_nodes': 'node', + 'admin.stat_online': '在线 node', + + // Admin users + 'admin.users_search': '搜索用户…', + 'admin.col_username': '用户名', + 'admin.col_role': '角色', + 'admin.col_status': '状态', + 'admin.col_created': '创建时间', + 'admin.col_actions': '操作', + 'admin.col_email': '电子邮箱', + 'admin.col_groups': '群组', + 'admin.no_users': '未找到用户', + 'admin.btn_suspend': '停用', + 'admin.btn_unsuspend': '恢复', + 'admin.btn_details': '详情', + 'admin.btn_delete': '删除', + 'admin.delete_confirm': '删除账户“{user}”?此操作无法撤销。' + + '该用户上传的文件仍留在托管它们的 node 上,每个 node 也会保留它固定的身份,' + + '直到其运营者取消固定。停用是可撤销的选项。', + 'admin.user_detail': '用户详情', + 'admin.btn_close': '关闭', + 'admin.self_note': '(您)', + + // Admin groups + 'admin.col_name': '名称', + 'admin.col_visibility': '可见性', + 'admin.col_members': '成员', + 'admin.no_groups': '未找到群组', + + // Admin logs + 'admin.col_time': '时间', + 'admin.col_event': '事件', + 'admin.col_user': '用户', + 'admin.col_ip': 'IP', + 'admin.col_detail': '详情', + 'admin.filter_all': '全部事件', + 'admin.no_logs': '未找到日志', + 'admin.btn_load_more': '加载更多', + + // Admin blocklist + 'admin.col_hash': '哈希', + 'admin.col_reason': '原因', + 'admin.col_date': '日期', + 'admin.col_added_by': '添加者', + 'admin.no_blocked': '没有被屏蔽的哈希', + 'admin.add_hash': '将哈希添加到屏蔽列表', + 'admin.hash_placeholder': 'blake3 哈希(64 个十六进制字符)', + 'admin.reason_placeholder': '原因', + 'admin.btn_block': '屏蔽', + 'admin.btn_unblock': '解除屏蔽', + + // Notifications + // Create group + 'create_group.title': '创建群组', + 'create_group.name': '群组名称', + 'create_group.name_placeholder': '例如:家庭照片、项目文件…', + 'create_group.visibility': '可见性', + 'create_group.private': '私密', + 'create_group.private_desc': '只有受邀成员才能看到并访问文件', + 'create_group.public': '公开', + 'create_group.public_desc': '任何人都可以发现并浏览此群组', + 'create_group.join_policy': '加入方式', + 'create_group.invite': '仅限邀请', + 'create_group.invite_desc': '成员必须由管理员邀请', + 'create_group.open': '开放(任何人都可加入)', + 'create_group.open_desc': '任何人都可以加入,无需批准', + 'create_group.description': '简介', + 'create_group.description_hint': '这个群组是做什么的?(可选)', + 'create_group.submit': '创建', + 'create_group.hint': '群组需要一个 node 来托管文件。您可以现在创建群组,稍后再接入 node。', + 'create_group.creating': '正在创建…', + + // Members + 'members.col_role': '角色', + 'members.group_role': '群组角色', + 'members.admin': '管理员', + 'members.group_admin': '群组管理员', + 'members.owner': '所有者', + 'members.member': '成员', + 'members.invite_title': '邀请成员', + 'members.username_placeholder': '用户名', + 'members.invite_btn': '邀请', + 'members.invite_needs_pairing': '邀请是用此 node 为您的浏览器固定的密钥签名的,' + + '因此请先在下方完成配对,再邀请任何人。', + 'members.invite_ask_operator': '只有托管此群组的 node 的运营者才能邀请,' + + '且须从与该 node 配对过的浏览器发起。', + 'members.remove': '移除', + 'members.remove_confirm': '将 {user} 从此群组移除?该用户的账户、其他群组' + + '以及其上传的文件都不受影响。', + 'members.remove_hint': '移除某人会取消其成员身份,并使 node 不再向其提供群组密钥。' + + '这不会删除其账户,也无法收回其已经持有的密钥——如果这一点很重要,' + + '请在 node 上轮换密钥(meshbay-node gek-init)。', + 'members.pair_title': '将此浏览器与您的 node 配对', + 'members.pair_hint': '您的 node 只接受来自已配对浏览器的运营者操作——邀请、删除文件。' + + '请在 node 上运行 `meshbay-node operator pair`,并在此输入配对码。' + + '该配对码绝不会经过 hub。', + 'members.pair_btn': '配对', + 'members.pair_success': '此浏览器现已与该 node 配对。', + 'members.invite_code_ready': '给 {user} 的邀请码——用您平时联系的方式发给对方。' + + '它只能使用一次,并且绝不会经过 hub。', + 'members.invite_code_hint': '对方第一次打开此群组时输入即可。那时您不需要在线。', + 'transfers.title': '传输', + 'transfers.clear': '清除已完成', + 'transfers.cancel': '取消', + 'transfers.open': '打开', + 'transfers.done': '已完成', + 'transfers.cancelled': '已取消', + 'transfers.failed': '失败', + 'group.select': '选择', + 'group.select_done': '完成', + 'group.actions': '操作({n})', + 'group.download_n': '下载({n})', + 'group.download_zip_n': '将文件夹打包为 zip 下载({n})', + 'group.delete_n': '删除({n})', + 'group.delete_n_confirm': { + other: '删除 {n} 个项目?{names}', + }, + 'group.download_zip': { + other: '打包为 zip 下载({n} 个文件)', + }, + 'group.zip_empty': '该文件夹里没有可下载的内容。', + 'group.zip_no_stream': '此浏览器无法把下载内容直接写入磁盘,' + + '因此 {name}({size})必须先在内存中构建。若压缩包很大,这可能会失败。是否继续?', + 'group.rmdir': '删除文件夹', + 'group.rmdir_confirm': '删除文件夹“{name}”?它必须是空的。', + 'group.rmdir_not_empty': '该文件夹不是空的。请先删除其中的内容——' + + '这里的任何操作都不会删除您看不到的文件。', + 'group.desc_edit': '编辑简介', + 'group.desc_add': '添加简介', + 'group.desc_save': '保存', + 'group.desc_cancel': '取消', + 'group.desc_placeholder': '这个群组的用途——成员会在他们的主页上看到。', + 'group.join_code_title': '此 node 需要认识您', + 'group.join_code_hint': '向邀请您的人索取一次性代码,并在此输入。' + + '之后此浏览器即被认可,不会再次询问。', + 'group.join_code_btn': '加入', + + 'notif.purge': '全部清除', + 'notif.title': '通知', + 'notif.empty': '没有通知', + 'notif.mark_all_read': '全部标为已读', + + // Sidebar + 'sidebar.search': '搜索文件', + + // Status + + // Search + 'search.title': '搜索文件', + 'search.placeholder': '在所有群组中搜索…', + 'search.no_results': '没有文件符合您的搜索。', + 'search.synced': '{ago}同步', + 'search.cache_note': '搜索依据的是您上次打开每个群组时的状态。' + + '打开某个群组,即可在此更新它的文件。', + 'search.col_group': '群组', + 'search.result_count': { + other: '找到 {n} 个文件', + }, + 'search.hint': '在所有已缓存的群组索引中搜索文件名。', +}; diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index 851c07e..11c108b 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -152,7 +152,12 @@ a:hover { text-decoration: underline; } box-shadow: var(--shadow-lg); min-width: 220px; z-index: 120; - overflow: hidden; + /* The language submenu expands to ten entries inside this dropdown, which + overruns a short viewport. Clip horizontally as before, scroll vertically + rather than hiding the last languages where nothing can reach them. */ + max-height: calc(100vh - 72px); + overflow-x: hidden; + overflow-y: auto; padding: 4px 0; } |