From 009de99527a68a0781704983634cd67afd424cd2 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Sat, 31 May 2025 18:43:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0i18n=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/css/style.css | 58 +++++++ client/index.html | 45 +++--- client/js/chat.js | 15 +- client/js/main.js | 44 +++-- client/js/room.js | 8 +- client/js/ui.js | 66 +++++--- client/js/util.fileUpload.js | 102 +++++++++--- client/js/util.i18n.js | 306 +++++++++++++++++++++++++++++++++++ client/js/util.settings.js | 100 +++++++++--- 9 files changed, 636 insertions(+), 108 deletions(-) create mode 100644 client/js/util.i18n.js diff --git a/client/css/style.css b/client/css/style.css index 8228d85..c105352 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -138,6 +138,64 @@ body { margin-top: 4px; } +/* Language switcher specific styles */ +/* 语言切换器专用样式 */ +.language-current { + font-weight: 500; + color: #30a8f7; + font-size: 0.85rem; + letter-spacing: 0.3px; +} + +.settings-item .switch + .language-indicator { + display: none; +} + +/* Language selector styles */ +/* 语言选择器样式 */ +.language-selector { + position: relative; +} + +.language-select { + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 8px 32px 8px 12px; + font-size: 14px; + color: #333; + cursor: pointer; + outline: none; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + min-width: 120px; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 8px center; + background-size: 16px; +} + +.language-select:hover { + border-color: #30a8f7; +} + +.language-select:focus { + border-color: #30a8f7; + box-shadow: 0 0 0 3px rgba(48, 168, 247, 0.1); +} + +.language-select option { + padding: 8px 12px; + color: #333; + background: #fff; +} + +.language-select option:hover { + background: #f5f5f5; +} + /* Theme selector styles */ /* 主题选择器样式 */ .theme-selector { diff --git a/client/index.html b/client/index.html index 477ca9e..229ba26 100644 --- a/client/index.html +++ b/client/index.html @@ -8,31 +8,40 @@ + -
-

Enter a Node

+ +
+

-
-
-
-
-
Message -
+
+
-
-
+
-
Members
+
diff --git a/client/js/chat.js b/client/js/chat.js index 5624ee9..7a656d3 100644 --- a/client/js/chat.js +++ b/client/js/chat.js @@ -23,6 +23,9 @@ import { import { formatFileSize } from './util.file.js'; +import { + t +} from './util.i18n.js'; // Render the chat area // 渲染聊天区域 @@ -119,11 +122,10 @@ export function addMsg(text, isHistory = false, msgType = 'text', timestamp = nu export function addOtherMsg(msg, userName = '', avatar = '', isHistory = false, msgType = 'text', timestamp = null) { if (!userName && activeRoomIndex >= 0) { const rd = roomsData[activeRoomIndex]; - if (rd && msg && msg.clientId && rd.userMap[msg.clientId]) { - userName = rd.userMap[msg.clientId].userName || rd.userMap[msg.clientId].username || rd.userMap[msg.clientId].name || 'Anonymous' + if (rd && msg && msg.clientId && rd.userMap[msg.clientId]) { userName = rd.userMap[msg.clientId].userName || rd.userMap[msg.clientId].username || rd.userMap[msg.clientId].name || t('ui.anonymous', 'Anonymous') } } - userName = userName || 'Anonymous'; + userName = userName || t('ui.anonymous', 'Anonymous'); let ts = isHistory ? timestamp : (timestamp || Date.now()); if (!isHistory && activeRoomIndex >= 0) { roomsData[activeRoomIndex].messages.push({ @@ -236,15 +238,14 @@ export function updateChatInputStyle() { const chatInputArea = $('.chat-input-area'); const placeholder = $('.input-field-placeholder'); const inputMessageInput = $('.input-message-input'); - if (!chatInputArea || !placeholder || !inputMessageInput) return; - if (rd && rd.privateChatTargetId) { + if (!chatInputArea || !placeholder || !inputMessageInput) return; if (rd && rd.privateChatTargetId) { addClass(chatInputArea, 'private-mode'); addClass(inputMessageInput, 'private-mode'); - placeholder.textContent = `Private Message to ${escapeHTML(rd.privateChatTargetName)}` + placeholder.textContent = `${t('ui.private_message_to', 'Private Message to')} ${escapeHTML(rd.privateChatTargetName)}` } else { removeClass(chatInputArea, 'private-mode'); removeClass(inputMessageInput, 'private-mode'); - placeholder.textContent = 'Message' + placeholder.textContent = t('ui.message', 'Message') } const html = inputMessageInput.innerHTML.replace(/(\s*)?/gi, '').replace(/ /g, '').replace(/\u200B/g, '').trim(); placeholder.style.opacity = (html === '') ? '1' : '0' diff --git a/client/js/main.js b/client/js/main.js index a682e9f..797848a 100644 --- a/client/js/main.js +++ b/client/js/main.js @@ -30,6 +30,7 @@ import { initSettings, // 初始化设置 / Initialize settings notifyMessage // 通知信息提示 / Display notification message } from './util.settings.js'; +import { t, updateStaticTexts } from './util.i18n.js'; // 从 util.theme.js 中导入主题功能函数 // Import theme functions from util.theme.js @@ -86,6 +87,11 @@ window.config = { debug: true // 是否开启调试模式 / Enable debug mode }; +// 在文档开始加载前就初始化语言设置,防止闪烁 +// Initialize language settings before document starts loading +initSettings(); +updateStaticTexts(); + // 把一些函数挂载到 window 对象上供其他模块使用 // Expose functions to the global window object for accessibility window.addSystemMsg = addSystemMsg; @@ -99,6 +105,12 @@ window.downloadFile = downloadFile; // 当 DOM 内容加载完成后执行初始化逻辑 // Run initialization logic when the DOM content is fully loaded window.addEventListener('DOMContentLoaded', () => { + // 移除预加载样式类,允许过渡效果 + // Remove preload class to allow transitions + setTimeout(() => { + document.body.classList.remove('preload'); + }, 300); + // 初始化登录表单 / Initialize login form initLoginForm(); @@ -121,13 +133,14 @@ window.addEventListener('DOMContentLoaded', () => { preventSpaceInput($id('password')); // 初始化辅助功能和界面设置 // Initialize autofill, input placeholders, and menus - autofillRoomPwd(); - setupInputPlaceholder(); + autofillRoomPwd(); setupInputPlaceholder(); setupMoreBtnMenu(); - setupImagePreview(); - setupEmojiPicker(); - initSettings(); + setupImagePreview(); setupEmojiPicker(); + // 由于我们已经在DOM加载前预先初始化了语言设置,这里不需要重复初始化 + // initSettings(); + // updateStaticTexts(); // 在初始化设置后更新静态文本 / Update static texts after initializing settings initTheme(); // 初始化主题 / Initialize theme + const settingsBtn = $id('settings-btn'); // 设置按钮 / Settings button if (settingsBtn) { settingsBtn.onclick = (e) => { @@ -198,11 +211,10 @@ window.addEventListener('DOMContentLoaded', () => { p: encryptedClientMessage, c: rd.privateChatTargetId }; - const encryptedMessageForServer = rd.chat.encryptServerMessage(serverRelayPayload, rd.chat.serverShared); - rd.chat.sendMessage(encryptedMessageForServer); + const encryptedMessageForServer = rd.chat.encryptServerMessage(serverRelayPayload, rd.chat.serverShared); rd.chat.sendMessage(encryptedMessageForServer); addMsg(messageContent, false, 'image_private'); } else { - addSystemMsg(`Cannot send private message to ${rd.privateChatTargetName}.User might not be fully connected.`) + addSystemMsg(`${t('system.private_message_failed', 'Cannot send private message to')} ${rd.privateChatTargetName}. ${t('system.user_not_connected', 'User might not be fully connected.')}`) } } else { // 公共频道图片消息发送 @@ -232,10 +244,9 @@ window.addEventListener('DOMContentLoaded', () => { c: rd.privateChatTargetId }; const encryptedMessageForServer = rd.chat.encryptServerMessage(serverRelayPayload, rd.chat.serverShared); - rd.chat.sendMessage(encryptedMessageForServer); - addMsg(text, false, 'text_private'); + rd.chat.sendMessage(encryptedMessageForServer); addMsg(text, false, 'text_private'); } else { - addSystemMsg(`Cannot send private message to ${rd.privateChatTargetName}.User might not be fully connected.`) + addSystemMsg(`${t('system.private_message_failed', 'Cannot send private message to')} ${rd.privateChatTargetName}. ${t('system.user_not_connected', 'User might not be fully connected.')}`) } } else { // 公共频道消息发送 @@ -292,9 +303,8 @@ window.addEventListener('DOMContentLoaded', () => { // 添加到自己的聊天记录 if (message.type === 'file_start') { addMsg(message, false, 'file_private'); - } - } else { - addSystemMsg(`Cannot send private file to ${rd.privateChatTargetName}. User might not be fully connected.`) + } } else { + addSystemMsg(`${t('system.private_file_failed', 'Cannot send private file to')} ${rd.privateChatTargetName}. ${t('system.user_not_connected', 'User might not be fully connected.')}`) } } else { // 公共频道文件发送 @@ -350,6 +360,12 @@ window.addEventListener('DOMContentLoaded', () => { } }); +// Listen for language change events +// 监听语言切换事件 +window.addEventListener('languageChange', (event) => { + updateStaticTexts(); +}); + // 全局拖拽文件自动打开附件功能 // Global drag file to auto trigger attach button let dragCounter = 0; diff --git a/client/js/room.js b/client/js/room.js index 00117c1..a6169e7 100644 --- a/client/js/room.js +++ b/client/js/room.js @@ -20,6 +20,7 @@ import { $id, createElement } from './util.dom.js'; +import { t } from './util.i18n.js'; let roomsData = []; let activeRoomIndex = -1; @@ -126,7 +127,7 @@ export function joinRoom(userName, roomName, password, modal = null, onResult) { closed = true; onResult(true) } - addSystemMsg("Server connection secured") + addSystemMsg(t('system.secured', 'connection secured')) }, onClientSecured: (user) => handleClientSecured(idx, user), onClientList: (list, selfId) => handleClientList(idx, list, selfId), @@ -189,9 +190,8 @@ export function handleClientSecured(idx, user) { } const isNew = !rd.knownUserIds.has(user.clientId); if (isNew) { - rd.knownUserIds.add(user.clientId); - const name = user.userName || user.username || user.name || 'Anonymous'; - const msg = `${name} joined`; + rd.knownUserIds.add(user.clientId); const name = user.userName || user.username || user.name || t('ui.anonymous', 'Anonymous'); + const msg = `${name} ${t('system.joined', 'joined the conversation')}`; rd.messages.push({ type: 'system', text: msg diff --git a/client/js/ui.js b/client/js/ui.js index bb83c3d..4dec30d 100644 --- a/client/js/ui.js +++ b/client/js/ui.js @@ -19,6 +19,12 @@ import { import { closeSettingsPanel } from './util.settings.js'; +import { + t +} from './util.i18n.js'; +import { + updateChatInputStyle +} from './chat.js'; // Utility functions for security and error handling // 安全和错误处理工具函数 @@ -64,9 +70,9 @@ function validateRoomData(roomData) { // Copy text to clipboard with fallback // 复制文本到剪贴板(含降级处理) -function copyToClipboard(text, successMessage = 'Copied to clipboard!', errorPrefix = 'Copy failed, text:') { +function copyToClipboard(text, successMessage = t('action.copied', 'Copied to clipboard!'), errorPrefix = t('action.copy_failed', 'Copy failed, text:')) { if (!text) { - window.addSystemMsg && window.addSystemMsg('Nothing to copy'); + window.addSystemMsg && window.addSystemMsg(t('action.nothing_to_copy', 'Nothing to copy')); return; } @@ -89,7 +95,7 @@ function showFallbackCopy(text, prefix) { prompt(prefix, text); } else { // For environments where prompt is not available - window.addSystemMsg && window.addSystemMsg('Copy not supported in this environment'); + window.addSystemMsg && window.addSystemMsg(t('action.copy_not_supported', 'Copy not supported in this environment')); } } @@ -109,7 +115,7 @@ function executeMenuAction(action, closeMenuCallback) { } } catch (error) { console.error('Menu action failed:', error); - window.addSystemMsg && window.addSystemMsg('Action failed. Please try again.'); + window.addSystemMsg && window.addSystemMsg(t('action.action_failed', 'Action failed. Please try again.')); } finally { closeMenuCallback && closeMenuCallback(); } @@ -120,7 +126,7 @@ function executeMenuAction(action, closeMenuCallback) { function handleShareAction() { const validation = validateRoomData(roomsData[activeRoomIndex]); if (!validation.valid) { - window.addSystemMsg && window.addSystemMsg(`Cannot share: ${validation.error}`); + window.addSystemMsg && window.addSystemMsg(`${t('action.cannot_share', 'Cannot share:')} ${validation.error}`); return; } @@ -138,7 +144,7 @@ function handleShareAction() { url += `&p=${encodeURIComponent(encryptedPwd)}`; } - copyToClipboard(url, 'Share link copied!', 'Copy failed, url:'); + copyToClipboard(url, t('action.share_copied', 'Share link copied!'), t('action.copy_url_failed', 'Copy failed, url:')); } // Handle exit action @@ -166,7 +172,7 @@ export function renderMainHeader() { onlineCount += 1 } const safeRoomName = escapeHTML(roomName); - $id("main-header").innerHTML = `
#${safeRoomName}
${onlineCount}members
Share
Quit
`; + $id("main-header").innerHTML = `
#${safeRoomName}
${onlineCount} ${t('ui.members', 'members')}
${t('action.share', 'Share')}
${t('action.exit', 'Quit')}
`; setupMoreBtnMenu(); setupMobileUIHandlers() } @@ -274,7 +280,7 @@ export function createUserItem(user, isMe) { div.className = 'member' + (isMe ? ' me' : '') + (isPrivateTarget ? ' private-chat-active' : ''); const rawName = user.userName || user.username || user.name || ''; const safeUserName = escapeHTML(rawName); - div.innerHTML = `
${safeUserName}${isMe?' (me)':''}
`; + div.innerHTML = `
${safeUserName}${isMe?t('ui.me', ' (me)'):''}
`; const avatarEl = div.querySelector('.avatar'); if (avatarEl) { const svg = createAvatarSVG(rawName); @@ -401,20 +407,18 @@ export function loginFormHandler(modal) { warnTip.style.color = '#e74c3c'; warnTip.style.fontSize = '13px'; warnTip.style.marginTop = '4px'; - warnTip.textContent = 'Node already exists'; + warnTip.textContent = t('ui.node_exists', 'Node already exists'); roomInput.parentNode.appendChild(warnTip); roomInput._warnTip = warnTip; roomInput.focus() - } - if (btn) { + } if (btn) { btn.disabled = false; - btn.innerText = 'ENTER' + btn.innerText = t('ui.enter', 'ENTER') } return - } - if (btn) { + } if (btn) { btn.disabled = true; - btn.innerText = 'Connecting...' + btn.innerText = t('ui.connecting', 'Connecting...') } window.joinRoom(userName, roomName, password, modal, function(success) { if (!success && btn) { @@ -429,26 +433,25 @@ export function loginFormHandler(modal) { // Generate login form HTML export function generateLoginForm(isModal = false) { const idPrefix = isModal ? '-modal' : ''; - return ` -
+ return `
- +
- +
- +
- + `; } export function openLoginModal() { const modal = document.createElement('div'); modal.className = 'login-modal'; - modal.innerHTML = ``; + modal.innerHTML = ``; document.body.appendChild(modal); modal.querySelector('.login-modal-close').onclick = () => modal.remove(); preventSpaceInput(modal.querySelector('#userName-modal')); @@ -503,7 +506,7 @@ export function autofillRoomPwd(formPrefix = '') { // Show security warning for plaintext URLs if (window.addSystemMsg) { - window.addSystemMsg('⚠️ This link uses an old format. Room data is not encrypted.', true); + window.addSystemMsg(t('system.security_warning', '⚠️ This link uses an old format. Room data is not encrypted.'), true); } } // Fill in the form fields @@ -542,7 +545,20 @@ export function autofillRoomPwd(formPrefix = '') { // Initialize login form export function initLoginForm() { const loginFormContainer = document.getElementById('login-form'); - if (loginFormContainer) { + if (loginFormContainer && loginFormContainer.children.length === 0) { + // 只有当登录表单为空时才初始化 + // Only initialize if login form is empty loginFormContainer.innerHTML = generateLoginForm(false); } -} \ No newline at end of file +} + +// Listen for language change events to refresh UI +// 监听语言变更事件刷新UI +window.addEventListener('languageChange', () => { + // Refresh main header and user list + renderMainHeader(); + renderUserList(false); + + // Refresh chat input placeholder + updateChatInputStyle(); +}); \ No newline at end of file diff --git a/client/js/util.fileUpload.js b/client/js/util.fileUpload.js index a4cd3ce..cded9e9 100644 --- a/client/js/util.fileUpload.js +++ b/client/js/util.fileUpload.js @@ -10,13 +10,79 @@ import { removeClass } from './util.dom.js'; import { formatFileSize } from './util.file.js'; +import { t } from './util.i18n.js'; // File upload modal state // 文件上传模态框状态 let uploadModal = null; -let selectedFiles = new Map(); // Map +let selectedFiles = new Map(); +let fileIdCounter = 0; let onSendCallback = null; +// Listen for language changes to update modal text +// 监听语言变更以更新模态框文本 +window.addEventListener('languageChange', () => { + if (uploadModal) { + updateModalTexts(); + } +}); + +// Update modal texts when language changes +// 语言切换时更新模态框文本 +function updateModalTexts() { + if (!uploadModal) return; + + // Update modal title + const modalTitle = uploadModal.querySelector('.file-upload-header h3'); + if (modalTitle) { + modalTitle.textContent = t('file.upload_files', 'Upload Files'); + } + + // Update file list title + const fileListTitle = uploadModal.querySelector('.file-list-title'); + if (fileListTitle) { + fileListTitle.textContent = t('file.selected_files', 'Selected Files'); + } + + // Update clear all button + const clearAllBtn = uploadModal.querySelector('.file-clear-all-btn'); + if (clearAllBtn) { + clearAllBtn.textContent = t('file.clear_all', 'Clear All'); + } + + // Update cancel button + const cancelBtn = uploadModal.querySelector('.file-upload-cancel-btn'); + if (cancelBtn) { + cancelBtn.textContent = t('file.cancel', 'Cancel'); + } + + // Update send files button + const sendBtn = uploadModal.querySelector('.file-upload-send-btn'); + if (sendBtn) { + sendBtn.textContent = t('file.send_files', 'Send Files'); + } + + // Update drag drop text + const dragDropText = uploadModal.querySelector('.file-drop-text'); + if (dragDropText) { + dragDropText.innerHTML = ` +

${t('file.drag_drop', 'Drag and drop files here')}

+

${t('file.or', 'or')}

+ `; + + // Re-attach browse button event + const browseBtn = dragDropText.querySelector('.file-browse-btn'); + if (browseBtn) { + on(browseBtn, 'click', handleBrowseClick); + } + } + + // Update summary if files are selected + if (selectedFiles.size > 0) { + updateFileListDisplay(); + } +} + // Generate unique file ID // 生成唯一文件ID function generateFileId() { @@ -29,33 +95,31 @@ function createUploadModal() { const modal = createElement('div', { class: 'file-upload-modal' }, ` -
-
+
-

Upload Files

+

${t('file.upload_files', 'Upload Files')}

-
-
+
📁
-

Drag and drop files here

-

or

+

${t('file.drag_drop', 'Drag and drop files here')}

+

${t('file.or', 'or')}

`); @@ -247,14 +311,12 @@ function updateFileList() { e.preventDefault(); removeFile(fileId); }); - } - - // Update summary + } // Update summary const totalSize = Array.from(selectedFiles.values()).reduce((sum, file) => sum + file.size, 0); - fileListSummary.innerHTML = ` - ${selectedFiles.size} files selected, - ${formatFileSize(totalSize)} total - `; + const summaryText = t('file.files_selected', '{count} files selected, {size} total') + .replace('{count}', selectedFiles.size) + .replace('{size}', formatFileSize(totalSize)); + fileListSummary.innerHTML = summaryText; } // Update send button state @@ -283,7 +345,7 @@ async function handleSendFiles() { } catch (error) { console.error('Error sending files:', error); if (window.addSystemMsg) { - window.addSystemMsg(`Failed to send files: ${error.message}`); + window.addSystemMsg(`${t('system.file_send_failed', 'Failed to send files:')} ${error.message}`); } } } diff --git a/client/js/util.i18n.js b/client/js/util.i18n.js new file mode 100644 index 0000000..f07bf55 --- /dev/null +++ b/client/js/util.i18n.js @@ -0,0 +1,306 @@ +// Simple i18n utility for NodeCrypt +// NodeCrypt 简单国际化工具 + +// Language definitions +// 语言定义 +const LANGUAGES = { + en: { + code: 'en', + name: 'English', + flag: '🇺🇸', + translations: { + // Login and main UI + 'ui.enter_node': 'Enter a Node', + 'ui.username': 'Username', + 'ui.node_name': 'Node Name', + 'ui.node_password': 'Node Password', + 'ui.optional': '(optional)', + 'ui.enter': 'ENTER', + 'ui.connecting': 'Connecting...', + 'ui.node_exists': 'Node already exists', + 'ui.my_name': 'My Name', + 'ui.members': 'Members', + 'ui.message': 'Message', + 'ui.private_message_to': 'Private Message to', + 'ui.me': ' (me)', + 'ui.anonymous': 'Anonymous', + + // Settings panel + 'settings.title': 'Settings', + 'settings.notification': 'Notification Settings', + 'settings.theme': 'Theme Settings', + 'settings.language': 'Language Settings', + 'settings.desktop_notifications': 'Desktop Notifications', + 'settings.sound_notifications': 'Sound Notifications', + 'settings.language_switch': 'Language', + 'settings.chinese': 'Chinese', + 'settings.english': 'English', + + // File upload and transfer + 'file.selected_files': 'Selected Files', + 'file.clear_all': 'Clear All', + 'file.cancel': 'Cancel', + 'file.send_files': 'Send Files', + 'file.sending': 'Sending', + 'file.receiving': 'Receiving', 'file.files': 'files', + 'file.total': 'Total', + 'file.files_selected': '{count} files selected, {size} total', + 'file.upload_files': 'Upload Files', + 'file.attach_file': 'Attach file', + 'file.no_password_required': 'No password required', + 'file.drag_drop': 'Drag and drop files here', + 'file.or': 'or', + 'file.browse_files': 'browse files', + + // Notifications and messages + 'notification.enabled': 'Notifications enabled', + 'notification.alert_here': 'You will receive alerts here.', + 'notification.not_supported': 'Notifications are not supported by your browser.', + 'notification.allow_browser': 'Please allow notifications in your browser settings.', + 'notification.image': '[image]', + 'notification.private': '(Private)', + + // Actions and menu + 'action.share': 'Share', + 'action.exit': 'Exit', + 'action.emoji': 'Emoji', + 'action.settings': 'Settings', + 'action.back': 'Back', + 'action.copied': 'Copied to clipboard!', + 'action.share_copied': 'Share link copied!', + 'action.copy_failed': 'Copy failed, text:', + 'action.copy_url_failed': 'Copy failed, url:', + 'action.nothing_to_copy': 'Nothing to copy', + 'action.copy_not_supported': 'Copy not supported in this environment', + 'action.action_failed': 'Action failed. Please try again.', + 'action.cannot_share': 'Cannot share:', + + // System messages + 'system.security_warning': '⚠️ This link uses an old format. Room data is not encrypted.', + 'system.file_send_failed': 'Failed to send files:', + 'system.joined': 'joined the conversation', + 'system.secured': 'connection secured', + 'system.private_message_failed': 'Cannot send private message to', + 'system.private_file_failed': 'Cannot send private file to', + 'system.user_not_connected': 'User might not be fully connected.', + } + }, + zh: { + code: 'zh', + name: '中文', + flag: '🇨🇳', + translations: { + // Login and main UI + 'ui.enter_node': '进入节点', + 'ui.username': '用户名', + 'ui.node_name': '节点名称', + 'ui.node_password': '节点密码', + 'ui.optional': '(可选)', + 'ui.enter': '进入', + 'ui.connecting': '连接中...', + 'ui.node_exists': '节点已存在', + 'ui.my_name': '我的名字', + 'ui.members': '成员', + 'ui.message': '消息', + 'ui.private_message_to': '私信给', + 'ui.me': '(我)', + 'ui.anonymous': '匿名用户', + + // Settings panel + 'settings.title': '设置', + 'settings.notification': '通知设置', + 'settings.theme': '主题设置', + 'settings.language': '语言设置', + 'settings.desktop_notifications': '桌面通知', + 'settings.sound_notifications': '声音通知', + 'settings.language_switch': '语言', + 'settings.chinese': '中文', + 'settings.english': 'English', + + // File upload and transfer + 'file.selected_files': '已选择的文件', + 'file.clear_all': '清空所有', + 'file.cancel': '取消', + 'file.send_files': '发送文件', + 'file.sending': '发送中', + 'file.receiving': '接收中', 'file.files': '个文件', + 'file.total': '总计', + 'file.files_selected': '选中 {count} 个文件,总计 {size}', + 'file.upload_files': '上传文件', + 'file.attach_file': '附加文件', + 'file.no_password_required': '无需密码', + 'file.drag_drop': '拖拽文件到此处', + 'file.or': '或', + 'file.browse_files': '浏览文件', + + // Notifications and messages + 'notification.enabled': '通知已启用', + 'notification.alert_here': '您将在此处收到通知。', + 'notification.not_supported': '您的浏览器不支持通知功能。', + 'notification.allow_browser': '请在浏览器设置中允许通知。', + 'notification.image': '[图片]', + 'notification.private': '(私信)', + + // Actions and menu + 'action.share': '分享', + 'action.exit': '退出', + 'action.emoji': '表情', + 'action.settings': '设置', + 'action.back': '返回', + 'action.copied': '已复制到剪贴板!', + 'action.share_copied': '分享链接已复制!', + 'action.copy_failed': '复制失败,文本:', + 'action.copy_url_failed': '复制失败,链接:', + 'action.nothing_to_copy': '没有内容可复制', + 'action.copy_not_supported': '此环境不支持复制功能', + 'action.action_failed': '操作失败,请重试。', + 'action.cannot_share': '无法分享:', + + // System messages + 'system.security_warning': '⚠️ 此链接使用旧格式,房间数据未加密。', + 'system.file_send_failed': '文件发送失败:', + 'system.joined': '加入了对话', + 'system.secured': '连接已安全', + 'system.private_message_failed': '无法发送私信给', + 'system.private_file_failed': '无法发送私密文件给', + 'system.user_not_connected': '用户可能未完全连接。', + } + } +}; + +// Current language +// 当前语言 +let currentLanguage = 'en'; + +// Get translation for a key +// 获取翻译文本 +export function t(key, fallback = key) { + const lang = LANGUAGES[currentLanguage]; + if (lang && lang.translations && lang.translations[key]) { + return lang.translations[key]; + } + return fallback; +} + +// Set current language +// 设置当前语言 +export function setLanguage(langCode) { + if (LANGUAGES[langCode]) { + currentLanguage = langCode; + // Update document language attribute + // 更新文档语言属性 + document.documentElement.lang = langCode; + + // Update static HTML texts + // 更新HTML中的静态文本 + updateStaticTexts(); + + // Dispatch language change event for other components to listen + // 派发语言变更事件供其他组件监听 + window.dispatchEvent(new CustomEvent('languageChange', { + detail: { language: langCode } + })); + } +} + +// Get current language +// 获取当前语言 +export function getCurrentLanguage() { + return currentLanguage; +} + +// Get all available languages +// 获取所有可用语言 +export function getAvailableLanguages() { + return Object.keys(LANGUAGES).map(code => ({ + code, + name: LANGUAGES[code].name, + flag: LANGUAGES[code].flag + })); +} + +// Initialize i18n with settings +// 根据设置初始化国际化 +export function initI18n(settings) { + if (settings && settings.language) { + setLanguage(settings.language); + } +} + +// Update static HTML text elements +// 更新HTML中的静态文本元素 +export function updateStaticTexts() { + // 如果DOM还没准备好,等待DOM准备好再更新 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => updateStaticTexts()); + return; + } + + // Update login title + const loginTitle = document.getElementById('login-title'); + if (loginTitle) { + loginTitle.textContent = t('ui.enter_node', 'Enter a Node'); + } + + // Update login form content with new translations + const loginFormContainer = document.getElementById('login-form'); + if (loginFormContainer) { + // Import and regenerate login form with current language + import('./ui.js').then(({ generateLoginForm }) => { + loginFormContainer.innerHTML = generateLoginForm(false); + }); + } + // Update sidebar username label + const sidebarUsername = document.getElementById('sidebar-username'); + if (sidebarUsername) { + sidebarUsername.textContent = t('ui.my_name', 'My Name'); + } + // Update "Enter a Node" text in sidebar + const joinRoomText = document.getElementById('join-room-text'); + if (joinRoomText) { + joinRoomText.textContent = t('ui.enter_node', 'Enter a Node'); + } + + // Update Members title in rightbar + const membersTitle = document.getElementById('members-title'); + if (membersTitle) { + membersTitle.textContent = t('ui.members', 'Members'); + } + + // Update settings title + const settingsTitle = document.getElementById('settings-title'); + if (settingsTitle) { + settingsTitle.textContent = t('settings.title', 'Settings'); + } + + // Update message placeholder + const messagePlaceholder = document.querySelector('.input-field-placeholder'); + if (messagePlaceholder) { + messagePlaceholder.textContent = t('ui.message', 'Message'); + } + + // Update attach button title + const attachBtn = document.querySelector('.chat-attach-btn'); + if (attachBtn) { + attachBtn.title = t('file.attach_file', 'Attach file'); + } + + // Update emoji button title + const emojiBtn = document.querySelector('.chat-emoji-btn'); + if (emojiBtn) { + emojiBtn.title = t('action.emoji', 'Emoji'); + } + // Update settings button title + const settingsBtn = document.getElementById('settings-btn'); + if (settingsBtn) { + settingsBtn.title = t('action.settings', 'Settings'); + settingsBtn.setAttribute('aria-label', t('action.settings', 'Settings')); + } + + // Update back button title + const backBtn = document.getElementById('settings-back-btn'); + if (backBtn) { + backBtn.title = t('action.back', 'Back'); + backBtn.setAttribute('aria-label', t('action.back', 'Back')); + } +} diff --git a/client/js/util.settings.js b/client/js/util.settings.js index 5f9ef96..2d4ac43 100644 --- a/client/js/util.settings.js +++ b/client/js/util.settings.js @@ -14,12 +14,17 @@ import { // Import theme utilities // 导入主题工具函数 import { THEMES, getCurrentTheme, applyTheme } from './util.theme.js'; + +// Import i18n utilities +// 导入国际化工具函数 +import { t, setLanguage, getCurrentLanguage, initI18n } from './util.i18n.js'; // Default settings // 默认设置 const DEFAULT_SETTINGS = { notify: false, sound: false, - theme: 'theme1' + theme: 'theme1', + language: 'en' // 'en' for English, 'zh' for Chinese }; // Load settings from localStorage @@ -43,19 +48,23 @@ function saveSettings(settings) { const { notify, sound, - theme + theme, + language } = settings; localStorage.setItem('settings', JSON.stringify({ notify, sound, - theme + theme, + language })) } // Apply settings to the document // 应用设置到文档 function applySettings(settings) { - document.documentElement.lang = 'en' + // Initialize i18n with current language setting + // 根据当前语言设置初始化国际化 + initI18n(settings); } // Ask for browser notification permission @@ -73,15 +82,22 @@ function askNotificationPermission(callback) { function setupSettingsPanel() { const settingsSidebar = $id('settings-sidebar'); const settingsContent = $id('settings-content'); + const settingsTitle = $id('settings-title'); if (!settingsSidebar || !settingsContent) return; - const settings = loadSettings(); // Create settings content HTML + const settings = loadSettings(); + + // Update settings title + // 更新设置标题 + if (settingsTitle) { + settingsTitle.textContent = t('settings.title', 'Settings'); + }// Create settings content HTML settingsContent.innerHTML = `
-
Notification Settings
+
${t('settings.notification', 'Notification Settings')}
-
Desktop Notifications
+
${t('settings.desktop_notifications', 'Desktop Notifications')}
-
Sound Notifications
+
${t('settings.sound_notifications', 'Sound Notifications')}
-
-
Theme Settings
+
+
+
${t('settings.language', 'Language Settings')}
+
+
+
${t('settings.language_switch', 'Language')}
+
+
+ +
+
+
+ +
+
${t('settings.theme', 'Theme Settings')}
${THEMES.map(theme => `
@@ -106,10 +138,32 @@ function setupSettingsPanel() { `).join('')}
- `; - - const notifyCheckbox = $('#settings-notify', settingsContent); + `; const notifyCheckbox = $('#settings-notify', settingsContent); const soundCheckbox = $('#settings-sound', settingsContent); + const languageSelect = $('#settings-language', settingsContent); + + // Language select event handler + // 语言选择事件处理 + on(languageSelect, 'change', e => { + const newLanguage = e.target.value; + settings.language = newLanguage; + + // Set language immediately + // 立即设置语言 + setLanguage(newLanguage); + + // Save settings + // 保存设置 + saveSettings(settings); + applySettings(settings); + + // Refresh the settings panel to show updated translations + // 刷新设置面板以显示更新的翻译 + setTimeout(() => { + setupSettingsPanel(); + }, 100); + }); + on(notifyCheckbox, 'change', e => { const checked = e.target.checked; if (checked) { @@ -375,6 +429,17 @@ function initSettings() { if (settings.theme) { applyTheme(settings.theme); } + + // Listen for language change events to update UI + // 监听语言变更事件以更新UI + window.addEventListener('languageChange', () => { + // Update settings title if settings panel is open + // 如果设置面板已打开,更新设置标题 + const settingsTitle = $id('settings-title'); + if (settingsTitle) { + settingsTitle.textContent = t('settings.title', 'Settings'); + } + }); } // Maximum notification text length @@ -413,16 +478,15 @@ function playSoundNotification() { function showDesktopNotification(roomName, text, msgType, sender) { if (!('Notification' in window) || Notification.permission !== 'granted') return; let body; - const senderPrefix = sender ? `${sender}:` : ''; - if (msgType === 'image' || msgType === 'private image') { - body = `${senderPrefix}[image]`; + const senderPrefix = sender ? `${sender}:` : ''; if (msgType === 'image' || msgType === 'private image') { + body = `${senderPrefix}${t('notification.image', '[image]')}`; if (msgType === 'private image') { - body = `(Private)${body}` + body = `${t('notification.private', '(Private)')}${body}` } } else if (msgType === 'text' || msgType === 'private text') { body = `${senderPrefix}${truncateText(text)}`; if (msgType === 'private text') { - body = `(Private)${body}` + body = `${t('notification.private', '(Private)')}${body}` } } else { body = truncateText(text)