-
-
+
@@ -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">
-
-
- Message
-
+
+
@@ -79,10 +85,9 @@
-
-
+
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 = ` `;
+ $id("main-header").innerHTML = ` `;
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 = ``;
+ div.innerHTML = ``;
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 `
-
-
Members
+
(\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}membersShare
Quit
#${safeRoomName}
${onlineCount} ${t('ui.members', 'members')}${t('action.share', 'Share')}
${t('action.exit', 'Quit')}
${safeUserName}${isMe?' (me)':''}
${safeUserName}${isMe?t('ui.me', ' (me)'):''}
+ return ` ENTER
+ ${t('ui.enter', 'ENTER')}
`;
}
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 = `
+
-
+
-
+
-
+
- Enter a Node
${t('ui.enter_node', 'Enter a Node')}
${t('file.drag_drop', 'Drag and drop files here')}
+${t('file.or', 'or')}
+
- ×
- Upload Files
+${t('file.upload_files', 'Upload Files')}
-
+
`);
@@ -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 = `
📁
-
Drag and drop files here
-or
${t('file.drag_drop', 'Drag and drop files here')}
+${t('file.or', 'or')}
- Selected Files
- Clear All
+ ${t('file.selected_files', 'Selected Files')}
+ ${t('file.clear_all', 'Clear All')}
- Cancel
- Send Files
+ ${t('file.cancel', 'Cancel')}
+ ${t('file.send_files', 'Send Files')}
-
-
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 => `
- `;
-
- 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)
@@ -106,10 +138,32 @@ function setupSettingsPanel() {
`).join('')}