添加i18n支持
This commit is contained in:
1 parent
97f4bb47e0
commit
009de99527
9 files changed
+636
-108
No files matched your search
@@ -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 {
|
||||
|
||||
+25
-20
@@ -8,31 +8,40 @@
|
||||
<link rel="stylesheet" href="css/phone.css">
|
||||
<link rel="stylesheet" href="css/form-animation.css">
|
||||
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
|
||||
<style>
|
||||
/* 添加一个预加载样式,避免闪烁 */
|
||||
.preload * {
|
||||
-webkit-transition: none !important;
|
||||
-moz-transition: none !important;
|
||||
-ms-transition: none !important;
|
||||
-o-transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body> <div class="login-container" id="login-container">
|
||||
<h1>Enter a Node</h1>
|
||||
<body class="preload">
|
||||
<div class="login-container" id="login-container">
|
||||
<h1 id="login-title"></h1>
|
||||
<form id="login-form">
|
||||
<!-- 登录表单内容将由JavaScript动态生成 -->
|
||||
<!-- Login form content will be dynamically generated by JavaScript -->
|
||||
</form>
|
||||
</div> <div class="container" id="chat-container" style="display:none;">
|
||||
</div><div class="container" id="chat-container" style="display:none;">
|
||||
<div class="mobile-sidebar-mask" id="mobile-sidebar-mask"></div>
|
||||
<div class="mobile-rightbar-mask" id="mobile-rightbar-mask"></div>
|
||||
<!-- Settings panel as a sidebar replacement -->
|
||||
<div class="settings-sidebar" id="settings-sidebar" style="display:none;">
|
||||
<div class="settings-header"> <button class="settings-back-btn" id="settings-back-btn" title="Back" aria-label="Back">
|
||||
<div class="settings-sidebar" id="settings-sidebar" style="display:none;"> <div class="settings-header"> <button class="settings-back-btn" id="settings-back-btn" title="" aria-label="">
|
||||
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 18L9 12L15 6" stroke="#808080" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<h2 class="settings-title">Settings</h2>
|
||||
</button><h2 class="settings-title" id="settings-title"></h2>
|
||||
</div>
|
||||
<div class="settings-content" id="settings-content">
|
||||
<!-- Settings content will be populated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar-user"> <span class="avatar" id="sidebar-user-avatar"></span> <span class="sidebar-username" id="sidebar-username">My Name</span> <button class="settings-btn" id="settings-btn" title="Settings" aria-label="Settings"> <svg width="30px" height="30px" viewBox="0 0 24 24" fill="none"
|
||||
<div class="sidebar-user"> <span class="avatar" id="sidebar-user-avatar"></span> <span class="sidebar-username" id="sidebar-username"></span> <button class="settings-btn" id="settings-btn" title="" aria-label=""> <svg width="30px" height="30px" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
@@ -41,18 +50,16 @@
|
||||
d="M10.65 3L9.93163 3.53449L9.32754 5.54812L7.47651 4.55141L6.5906 4.68143L4.68141 6.59062L4.55139 7.47652L5.5481 9.32755L3.53449 9.93163L3 10.65V13.35L3.53449 14.0684L5.54811 14.6725L4.55142 16.5235L4.68144 17.4094L6.59063 19.3186L7.47653 19.4486L9.32754 18.4519L9.93163 20.4655L10.65 21H13.35L14.0684 20.4655L14.6725 18.4519L16.5235 19.4486L17.4094 19.3185L19.3186 17.4094L19.4486 16.5235L18.4519 14.6724L20.4655 14.0684L21 13.35V10.65L20.4655 9.93163L18.4519 9.32754L19.4486 7.47654L19.3186 6.59063L17.4094 4.68144L16.5235 4.55142L14.6725 5.54812L14.0684 3.53449L13.35 3H10.65ZM10.4692 6.96284L11.208 4.5H12.792L13.5308 6.96284L13.8753 7.0946C13.9654 7.12908 14.0543 7.16597 14.142 7.2052L14.4789 7.35598L16.7433 6.13668L17.8633 7.25671L16.644 9.52111L16.7948 9.85803C16.834 9.9457 16.8709 10.0346 16.9054 10.1247L17.0372 10.4692L19.5 11.208V12.792L17.0372 13.5308L16.9054 13.8753C16.8709 13.9654 16.834 14.0543 16.7948 14.1419L16.644 14.4789L17.8633 16.7433L16.7433 17.8633L14.4789 16.644L14.142 16.7948C14.0543 16.834 13.9654 16.8709 13.8753 16.9054L13.5308 17.0372L12.792 19.5H11.208L10.4692 17.0372L10.1247 16.9054C10.0346 16.8709 9.94569 16.834 9.85803 16.7948L9.52111 16.644L7.25671 17.8633L6.13668 16.7433L7.35597 14.4789L7.2052 14.142C7.16597 14.0543 7.12908 13.9654 7.0946 13.8753L6.96284 13.5308L4.5 12.792L4.5 11.208L6.96284 10.4692L7.0946 10.1247C7.12907 10.0346 7.16596 9.94571 7.20519 9.85805L7.35596 9.52113L6.13666 7.2567L7.25668 6.13667L9.5211 7.35598L9.85803 7.2052C9.9457 7.16597 10.0346 7.12908 10.1247 7.0946L10.4692 6.96284ZM14.25 12C14.25 13.2426 13.2426 14.25 12 14.25C10.7574 14.25 9.75 13.2426 9.75 12C9.75 10.7574 10.7574 9.75 12 9.75C13.2426 9.75 14.25 10.7574 14.25 12ZM15.75 12C15.75 14.0711 14.0711 15.75 12 15.75C9.92893 15.75 8.25 14.0711 8.25 12C8.25 9.92893 9.92893 8.25 12 8.25C14.0711 8.25 15.75 9.92893 15.75 12Z"
|
||||
fill="#808080"></path>
|
||||
</g>
|
||||
</svg> </button> </div>
|
||||
<div class="join-room"> <svg class="MuiSvgIcon-root icon-22-middle" focusable="false" viewBox="0 0 24 24" aria-hidden="true">
|
||||
</svg> </button> </div> <div class="join-room"> <svg class="MuiSvgIcon-root icon-22-middle" focusable="false" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M21 3.01H3c-1.1 0-2 .9-2 2V9h2V4.99h18v14.03H3V15H1v4.01c0 1.1.9 1.98 2 1.98h18c1.1 0 2-.88 2-1.98v-14c0-1.11-.9-2-2-2zM11 16l4-4-4-4v3H1v2h10v3z"></path>
|
||||
</svg> Enter a Node </div>
|
||||
</svg> <span id="join-room-text"></span> </div>
|
||||
<div class="rooms" id="room-list"> </div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="main-header" id="main-header"> </div>
|
||||
<div class="chat-area" id="chat-area"> </div>
|
||||
<div class="chat-input-area">
|
||||
<div class="rows-wrapper chat-input-wrapper chat-input-main-wrapper chat-rows-wrapper">
|
||||
<div class="new-message-wrapper rows-wrapper-row" data-offset="commands"> <button class="chat-emoji-btn" type="button" title="Emoji"> <svg width="30px" height="30px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<div class="rows-wrapper chat-input-wrapper chat-input-main-wrapper chat-rows-wrapper"> <div class="new-message-wrapper rows-wrapper-row" data-offset="commands"> <button class="chat-emoji-btn" type="button" title=""> <svg width="30px" height="30px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
@@ -60,10 +67,9 @@
|
||||
d="M19.5 12C19.5 16.1421 16.1421 19.5 12 19.5C7.85786 19.5 4.5 16.1421 4.5 12C4.5 7.85786 7.85786 4.5 12 4.5C16.1421 4.5 19.5 7.85786 19.5 12ZM21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12ZM9.375 10.5C9.99632 10.5 10.5 9.99632 10.5 9.375C10.5 8.75368 9.99632 8.25 9.375 8.25C8.75368 8.25 8.25 8.75368 8.25 9.375C8.25 9.99632 8.75368 10.5 9.375 10.5ZM15.75 9.375C15.75 9.99632 15.2463 10.5 14.625 10.5C14.0037 10.5 13.5 9.99632 13.5 9.375C13.5 8.75368 14.0037 8.25 14.625 8.25C15.2463 8.25 15.75 8.75368 15.75 9.375ZM12 15C10.1783 15 9 13.8451 9 12.75H7.5C7.5 14.9686 9.67954 16.5 12 16.5C14.3205 16.5 16.5 14.9686 16.5 12.75H15C15 13.8451 13.8217 15 12 15Z"
|
||||
fill="#808080"></path>
|
||||
</g>
|
||||
</svg> </button>
|
||||
<div class="input-message-container">
|
||||
<div class="input-message-input is-empty scrollable scrollable-y no-scrollbar input-message-input-init" contenteditable="true" dir="auto" tabindex="-1"></div> <span class="input-field-placeholder i18n is-empty">Message</span>
|
||||
<div contenteditable="true" tabindex="-1" class="input-message-input is-empty scrollable scrollable-y no-scrollbar input-field-input-fake"></div> <button class="chat-attach-btn chat-attach-btn-abs" type="button" title="Attach file"> <svg width="30px" height="30px"
|
||||
</svg> </button> <div class="input-message-container">
|
||||
<div class="input-message-input is-empty scrollable scrollable-y no-scrollbar input-message-input-init" contenteditable="true" dir="auto" tabindex="-1"></div> <span class="input-field-placeholder i18n is-empty"></span>
|
||||
<div contenteditable="true" tabindex="-1" class="input-message-input is-empty scrollable scrollable-y no-scrollbar input-field-input-fake"></div> <button class="chat-attach-btn chat-attach-btn-abs" type="button" title=""> <svg width="30px" height="30px"
|
||||
viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
@@ -79,10 +85,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="rightbar" id="rightbar">
|
||||
<div class="rightbar-header">
|
||||
<div class="rightbar-group">
|
||||
<div class="rightbar-header"> <div class="rightbar-group">
|
||||
<div class="group-info">
|
||||
<div class="title">Members</div>
|
||||
<div class="title" id="members-title"></div>
|
||||
<div class="group-desc" id="rightbar-group-desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+8
-7
@@ -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(/<br\s*\/?>(\s*)?/gi, '').replace(/ /g, '').replace(/\u200B/g, '').trim();
|
||||
placeholder.style.opacity = (html === '') ? '1' : '0'
|
||||
|
||||
+30
-14
@@ -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;
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+41
-25
@@ -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 = `<button class="mobile-menu-btn"id="mobile-menu-btn"aria-label="Open Sidebar"><svg width="35px"height="35px"viewBox="0 0 24 24"fill="none"xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier"stroke-width="0"></g><g id="SVGRepo_tracerCarrier"stroke-linecap="round"stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill-rule="evenodd"clip-rule="evenodd"d="M21.4498 10.275L11.9998 3.1875L2.5498 10.275L2.9998 11.625H3.7498V20.25H20.2498V11.625H20.9998L21.4498 10.275ZM5.2498 18.75V10.125L11.9998 5.0625L18.7498 10.125V18.75H14.9999V14.3333L14.2499 13.5833H9.74988L8.99988 14.3333V18.75H5.2498ZM10.4999 18.75H13.4999V15.0833H10.4999V18.75Z"fill="#808080"></path></g></svg></button><div class="main-header-center"id="main-header-center"><div class="main-header-flex"><div class="group-title group-title-bold">#${safeRoomName}</div><span class="main-header-members">${onlineCount}members</span></div></div><div class="main-header-actions"><button class="more-btn"id="more-btn"aria-label="More Options"><svg width="35px"height="35px"viewBox="0 0 24 24"fill="none"xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier"stroke-width="0"></g><g id="SVGRepo_tracerCarrier"stroke-linecap="round"stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><circle cx="12"cy="6"r="1.5"fill="#808080"></circle><circle cx="12"cy="12"r="1.5"fill="#808080"></circle><circle cx="12"cy="18"r="1.5"fill="#808080"></circle></g></svg></button><button class="mobile-info-btn"id="mobile-info-btn"aria-label="Open Members"><svg width="35px"height="35px"viewBox="0 0 24 24"fill="none"xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier"stroke-width="0"></g><g id="SVGRepo_tracerCarrier"stroke-linecap="round"stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill-rule="evenodd"clip-rule="evenodd"d="M16.0603 18.307C14.89 19.0619 13.4962 19.5 12 19.5C10.5038 19.5 9.10996 19.0619 7.93972 18.307C8.66519 16.7938 10.2115 15.75 12 15.75C13.7886 15.75 15.3349 16.794 16.0603 18.307ZM17.2545 17.3516C16.2326 15.5027 14.2632 14.25 12 14.25C9.73663 14.25 7.76733 15.5029 6.74545 17.3516C5.3596 15.9907 4.5 14.0958 4.5 12C4.5 7.85786 7.85786 4.5 12 4.5C16.1421 4.5 19.5 7.85786 19.5 12C19.5 14.0958 18.6404 15.9908 17.2545 17.3516ZM21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12ZM12 12C13.2426 12 14.25 10.9926 14.25 9.75C14.25 8.50736 13.2426 7.5 12 7.5C10.7574 7.5 9.75 8.50736 9.75 9.75C9.75 10.9926 10.7574 12 12 12ZM12 13.5C14.0711 13.5 15.75 11.8211 15.75 9.75C15.75 7.67893 14.0711 6 12 6C9.92893 6 8.25 7.67893 8.25 9.75C8.25 11.8211 9.92893 13.5 12 13.5Z"fill="#808080"></path></g></svg></button><div class="more-menu"id="more-menu"><div class="more-menu-item"data-action="share">Share</div><div class="more-menu-item"data-action="exit">Quit</div></div></div>`;
|
||||
$id("main-header").innerHTML = `<button class="mobile-menu-btn"id="mobile-menu-btn"aria-label="Open Sidebar"><svg width="35px"height="35px"viewBox="0 0 24 24"fill="none"xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier"stroke-width="0"></g><g id="SVGRepo_tracerCarrier"stroke-linecap="round"stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill-rule="evenodd"clip-rule="evenodd"d="M21.4498 10.275L11.9998 3.1875L2.5498 10.275L2.9998 11.625H3.7498V20.25H20.2498V11.625H20.9998L21.4498 10.275ZM5.2498 18.75V10.125L11.9998 5.0625L18.7498 10.125V18.75H14.9999V14.3333L14.2499 13.5833H9.74988L8.99988 14.3333V18.75H5.2498ZM10.4999 18.75H13.4999V15.0833H10.4999V18.75Z"fill="#808080"></path></g></svg></button><div class="main-header-center"id="main-header-center"><div class="main-header-flex"><div class="group-title group-title-bold">#${safeRoomName}</div><span class="main-header-members">${onlineCount} ${t('ui.members', 'members')}</span></div></div><div class="main-header-actions"><button class="more-btn"id="more-btn"aria-label="More Options"><svg width="35px"height="35px"viewBox="0 0 24 24"fill="none"xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier"stroke-width="0"></g><g id="SVGRepo_tracerCarrier"stroke-linecap="round"stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><circle cx="12"cy="6"r="1.5"fill="#808080"></circle><circle cx="12"cy="12"r="1.5"fill="#808080"></circle><circle cx="12"cy="18"r="1.5"fill="#808080"></circle></g></svg></button><button class="mobile-info-btn"id="mobile-info-btn"aria-label="Open Members"><svg width="35px"height="35px"viewBox="0 0 24 24"fill="none"xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier"stroke-width="0"></g><g id="SVGRepo_tracerCarrier"stroke-linecap="round"stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill-rule="evenodd"clip-rule="evenodd"d="M16.0603 18.307C14.89 19.0619 13.4962 19.5 12 19.5C10.5038 19.5 9.10996 19.0619 7.93972 18.307C8.66519 16.7938 10.2115 15.75 12 15.75C13.7886 15.75 15.3349 16.794 16.0603 18.307ZM17.2545 17.3516C16.2326 15.5027 14.2632 14.25 12 14.25C9.73663 14.25 7.76733 15.5029 6.74545 17.3516C5.3596 15.9907 4.5 14.0958 4.5 12C4.5 7.85786 7.85786 4.5 12 4.5C16.1421 4.5 19.5 7.85786 19.5 12C19.5 14.0958 18.6404 15.9908 17.2545 17.3516ZM21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12ZM12 12C13.2426 12 14.25 10.9926 14.25 9.75C14.25 8.50736 13.2426 7.5 12 7.5C10.7574 7.5 9.75 8.50736 9.75 9.75C9.75 10.9926 10.7574 12 12 12ZM12 13.5C14.0711 13.5 15.75 11.8211 15.75 9.75C15.75 7.67893 14.0711 6 12 6C9.92893 6 8.25 7.67893 8.25 9.75C8.25 11.8211 9.92893 13.5 12 13.5Z"fill="#808080"></path></g></svg></button><div class="more-menu"id="more-menu"><div class="more-menu-item"data-action="share">${t('action.share', 'Share')}</div><div class="more-menu-item"data-action="exit">${t('action.exit', 'Quit')}</div></div></div>`;
|
||||
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 = `<span class="avatar"></span><div class="member-info"><div class="member-name">${safeUserName}${isMe?' (me)':''}</div></div>`;
|
||||
div.innerHTML = `<span class="avatar"></span><div class="member-info"><div class="member-name">${safeUserName}${isMe?t('ui.me', ' (me)'):''}</div></div>`;
|
||||
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 `
|
||||
<div class="input-group">
|
||||
return ` <div class="input-group">
|
||||
<input id="userName${idPrefix}" type="text" autocomplete="username" required minlength="1" maxlength="15" placeholder="">
|
||||
<label for="userName${idPrefix}" class="floating-label">Username</label>
|
||||
<label for="userName${idPrefix}" class="floating-label">${t('ui.username', 'Username')}</label>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input id="roomName${idPrefix}" type="text" required minlength="1" maxlength="15" placeholder="">
|
||||
<label for="roomName${idPrefix}" class="floating-label">Node Name</label>
|
||||
<label for="roomName${idPrefix}" class="floating-label">${t('ui.node_name', 'Node Name')}</label>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input id="password${idPrefix}" type="password" autocomplete="${isModal ? 'off' : 'current-password'}" minlength="1" maxlength="15" placeholder="">
|
||||
<label for="password${idPrefix}" class="floating-label">Node Password <span class="optional">(optional)</span></label>
|
||||
<label for="password${idPrefix}" class="floating-label">${t('ui.node_password', 'Node Password')} <span class="optional">${t('ui.optional', '(optional)')}</span></label>
|
||||
</div>
|
||||
<button type="submit" class="login-btn">ENTER</button>
|
||||
<button type="submit" class="login-btn">${t('ui.enter', 'ENTER')}</button>
|
||||
`;
|
||||
}
|
||||
export function openLoginModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'login-modal';
|
||||
modal.innerHTML = `<div class="login-modal-bg"></div><div class="login-modal-card"><button class="login-modal-close login-modal-close-abs">×</button><h1>Enter a Node</h1><form id="login-form-modal">${generateLoginForm(true)}</form></div>`;
|
||||
modal.innerHTML = `<div class="login-modal-bg"></div><div class="login-modal-card"><button class="login-modal-close login-modal-close-abs">×</button><h1>${t('ui.enter_node', 'Enter a Node')}</h1><form id="login-form-modal">${generateLoginForm(true)}</form></div>`;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
});
|
||||
@@ -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<fileId, File>
|
||||
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 = `
|
||||
<p><strong>${t('file.drag_drop', 'Drag and drop files here')}</strong></p>
|
||||
<p>${t('file.or', 'or')} <button class="file-browse-btn" type="button">${t('file.browse_files', 'browse files')}</button></p>
|
||||
`;
|
||||
|
||||
// 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'
|
||||
}, `
|
||||
<div class="file-upload-overlay"></div>
|
||||
<div class="file-upload-container">
|
||||
<div class="file-upload-overlay"></div> <div class="file-upload-container">
|
||||
<div class="file-upload-header">
|
||||
<h3>Upload Files</h3>
|
||||
<h3>${t('file.upload_files', 'Upload Files')}</h3>
|
||||
<button class="file-upload-close">×</button>
|
||||
</div>
|
||||
<div class="file-upload-content">
|
||||
<div class="file-drop-zone" id="file-drop-zone">
|
||||
<div class="file-upload-content"> <div class="file-drop-zone" id="file-drop-zone">
|
||||
<div class="file-drop-icon">📁</div>
|
||||
<div class="file-drop-text">
|
||||
<p><strong>Drag and drop files here</strong></p>
|
||||
<p>or <button class="file-browse-btn" type="button">browse files</button></p>
|
||||
<p><strong>${t('file.drag_drop', 'Drag and drop files here')}</strong></p>
|
||||
<p>${t('file.or', 'or')} <button class="file-browse-btn" type="button">${t('file.browse_files', 'browse files')}</button></p>
|
||||
</div>
|
||||
<input type="file" id="file-upload-input" multiple style="display: none;" accept="*/*">
|
||||
</div>
|
||||
<div class="file-list-container" id="file-list-container" style="display: none;">
|
||||
<div class="file-list-header">
|
||||
<span class="file-list-title">Selected Files</span>
|
||||
<button class="file-clear-all-btn" type="button">Clear All</button>
|
||||
<span class="file-list-title">${t('file.selected_files', 'Selected Files')}</span>
|
||||
<button class="file-clear-all-btn" type="button">${t('file.clear_all', 'Clear All')}</button>
|
||||
</div>
|
||||
<div class="file-list" id="file-list"></div>
|
||||
<div class="file-list-summary" id="file-list-summary"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-upload-footer">
|
||||
<button class="file-upload-cancel-btn" type="button">Cancel</button>
|
||||
<button class="file-upload-send-btn" type="button" disabled>Send Files</button>
|
||||
<button class="file-upload-cancel-btn" type="button">${t('file.cancel', 'Cancel')}</button>
|
||||
<button class="file-upload-send-btn" type="button" disabled>${t('file.send_files', 'Send Files')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
@@ -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 = `
|
||||
<strong>${selectedFiles.size}</strong> files selected,
|
||||
<strong>${formatFileSize(totalSize)}</strong> 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
+82
-18
@@ -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 = `
|
||||
<div class="settings-section">
|
||||
<div class="settings-section-title">Notification Settings</div>
|
||||
<div class="settings-section-title">${t('settings.notification', 'Notification Settings')}</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-label">
|
||||
<div>Desktop Notifications</div>
|
||||
<div>${t('settings.desktop_notifications', 'Desktop Notifications')}</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="settings-notify" ${settings.notify ? 'checked' : ''}>
|
||||
@@ -90,15 +106,31 @@ function setupSettingsPanel() {
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-label">
|
||||
<div>Sound Notifications</div>
|
||||
<div>${t('settings.sound_notifications', 'Sound Notifications')}</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="settings-sound" ${settings.sound ? 'checked' : ''}>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div> <div class="settings-section">
|
||||
<div class="settings-section-title">Theme Settings</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<div class="settings-section-title">${t('settings.language', 'Language Settings')}</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-item-label">
|
||||
<div>${t('settings.language_switch', 'Language')}</div>
|
||||
</div>
|
||||
<div class="language-selector">
|
||||
<select id="settings-language" class="language-select">
|
||||
<option value="en" ${settings.language === 'en' ? 'selected' : ''}>🇺🇸 English</option>
|
||||
<option value="zh" ${settings.language === 'zh' ? 'selected' : ''}>🇨🇳 中文</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<div class="settings-section-title">${t('settings.theme', 'Theme Settings')}</div>
|
||||
<div class="theme-selector" id="theme-selector">
|
||||
${THEMES.map(theme => `
|
||||
<div class="theme-item ${settings.theme === theme.id ? 'active' : ''}" data-theme-id="${theme.id}" style="background: ${theme.background}; background-size: cover; background-position: center;">
|
||||
@@ -106,10 +138,32 @@ function setupSettingsPanel() {
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user