summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-16 00:21:07 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-16 00:21:07 +0200
commit3b2d03a919e56d14f8097844b97b844ddf526cd6 (patch)
tree31b222fedf6d608809eb47c45c5f431d7001d87a /packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
parent75065cc1ed5d9cd733871d478dc4f915663d795c (diff)
downloadmeshbay-3b2d03a919e56d14f8097844b97b844ddf526cd6.tar.gz
feat(hub): translate the web client into nine more languages
French, Spanish, Brazilian Portuguese, Simplified Chinese, Japanese, German, Italian, Dutch and Polish, all in the formal register. `hub`, `node` and `GEK` stay in English: they name the CLI, node.toml and the docs, and translating them would cut the interface off from everything an operator reads and types. Catalogues move out of i18n.js into locales/, one file per language, fetched with a dynamic import. A visitor downloads their language plus English as a fallback — about 36 KB rather than the ~180 KB that ten inlined catalogues would have cost everyone. i18n.js keeps only the loader, so the first render now waits for initLocale(). Three things the old code got wrong, none of them visible until there was a second language: - Resolution trimmed a tag to its base before matching, so a browser reporting pt-BR looked for a `pt` catalogue that does not exist and fell back to English. Matching is now exact first, then by base language. - Counted strings were single strings, so Polish could not express 1 plik / 2 pliki / 5 plików at all. t() selects through Intl.PluralRules; en.js gains the same treatment, which incidentally fixes "1 files". - Interpolation used String.replace, which reads `$&` in the replacement. A file named rap$&sody.mp3 rendered corrupted in its own delete dialog. Five strings were still hardcoded in app.js — the group name placeholder and the four visibility/join-policy descriptions — and are now keyed. test_locales.py holds the nine translations to the shape of en.js: same keys, a counted string stays counted everywhere, every plural entry covers each category Intl actually produces for that language, and the {placeholders} survive translation. Verified failing first, against a catalogue with a key removed, a placeholder dropped and the Polish `few` form deleted. The language menu also grew from one entry to ten, which overran a short viewport inside a dropdown that clipped instead of scrolling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js376
1 files changed, 376 insertions, 0 deletions
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': '在所有已缓存的群组索引中搜索文件名。',
+};