From 531e0beb96c877c7f89ac8e47e628aaa8093ac1d Mon Sep 17 00:00:00 2001 From: MoringstarsH <2671148074@qq.com> Date: Thu, 28 May 2026 19:17:08 +0800 Subject: [PATCH 01/10] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E5=88=87=E6=8D=A2=E5=92=8C=E7=99=BD=E5=A4=A9?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/channels.js | 36 +- static/chat.js | 338 ++++++++-------- static/i18n.js | 877 ++++++++++++++++++++++++++++++++++++++++++ static/index.html | 173 +++++---- static/jobs.css | 19 + static/jobs.js | 82 ++-- static/rules-panel.js | 40 +- static/sessions.css | 4 + static/sessions.js | 193 ++++++---- static/style.css | 122 +++++- static/theme.js | 46 +++ 11 files changed, 1540 insertions(+), 390 deletions(-) create mode 100644 static/i18n.js create mode 100644 static/theme.js diff --git a/static/channels.js b/static/channels.js index a74b7332..86b5f15b 100644 --- a/static/channels.js +++ b/static/channels.js @@ -63,14 +63,14 @@ function renderChannelTabs() { const editBtn = document.createElement('button'); editBtn.className = 'ch-edit-btn'; - editBtn.title = 'Rename'; + editBtn.title = t('channel.rename.title'); editBtn.innerHTML = ''; editBtn.onclick = (e) => { e.stopPropagation(); showChannelRenameDialog(name); }; actions.appendChild(editBtn); const delBtn = document.createElement('button'); delBtn.className = 'ch-delete-btn'; - delBtn.title = 'Delete'; + delBtn.title = t('channel.delete.title'); delBtn.innerHTML = ''; delBtn.onclick = (e) => { e.stopPropagation(); deleteChannel(name); }; actions.appendChild(delBtn); @@ -142,13 +142,13 @@ function renderChannelSidebar() { actions.className = 'channel-sidebar-row-actions'; const editBtn = document.createElement('button'); - editBtn.title = 'Rename'; + editBtn.title = t('channel.rename.title'); editBtn.innerHTML = ''; editBtn.onclick = (e) => { e.stopPropagation(); _showSidebarRenameDialog(name); }; actions.appendChild(editBtn); const delBtn = document.createElement('button'); - delBtn.title = 'Delete'; + delBtn.title = t('channel.delete.title'); delBtn.innerHTML = ''; delBtn.onclick = (e) => { e.stopPropagation(); _sidebarConfirmDelete(name, row, label); }; actions.appendChild(delBtn); @@ -198,7 +198,7 @@ function _showSidebarRenameDialog(oldName) { const confirm = document.createElement('button'); confirm.className = 'confirm-btn'; confirm.innerHTML = '✓'; - confirm.title = 'Rename'; + confirm.title = t('channel.rename.title'); confirm.onclick = () => { const newName = input.value.trim().toLowerCase(); if (!newName || !/^[a-z0-9][a-z0-9\-]{0,19}$/.test(newName)) return; @@ -217,7 +217,7 @@ function _showSidebarRenameDialog(oldName) { const cancel = document.createElement('button'); cancel.className = 'cancel-btn'; cancel.innerHTML = '✕'; - cancel.title = 'Cancel'; + cancel.title = t('common.cancel'); cancel.onclick = cleanup; wrapper.appendChild(cancel); @@ -245,7 +245,7 @@ function _sidebarConfirmDelete(name, row, label) { const originalOnclick = row.onclick; row.classList.add('confirm-delete'); - label.textContent = `delete #${name}?`; + label.textContent = t('channel.deleteConfirm', { name }); if (actions) actions.style.display = 'none'; const confirmBar = document.createElement('span'); @@ -253,11 +253,11 @@ function _sidebarConfirmDelete(name, row, label) { confirmBar.style.display = 'flex'; const tickBtn = document.createElement('button'); - tickBtn.title = 'Confirm delete'; + tickBtn.title = t('common.confirm'); tickBtn.innerHTML = ''; const crossBtn = document.createElement('button'); - crossBtn.title = 'Cancel'; + crossBtn.title = t('common.cancel'); crossBtn.innerHTML = ''; confirmBar.appendChild(tickBtn); @@ -360,14 +360,14 @@ function showChannelCreateDialog() { const confirm = document.createElement('button'); confirm.className = 'confirm-btn'; confirm.innerHTML = '✓'; - confirm.title = 'Create'; + confirm.title = t('common.create'); confirm.onclick = () => { _submitInlineCreate(input, wrapper); if (addBtn) addBtn.style.display = ''; }; wrapper.appendChild(confirm); const cancel = document.createElement('button'); cancel.className = 'cancel-btn'; cancel.innerHTML = '✕'; - cancel.title = 'Cancel'; + cancel.title = t('common.cancel'); cancel.onclick = cleanup; wrapper.appendChild(cancel); @@ -425,7 +425,7 @@ function showChannelRenameDialog(oldName) { const confirm = document.createElement('button'); confirm.className = 'confirm-btn'; confirm.innerHTML = '✓'; - confirm.title = 'Rename'; + confirm.title = t('channel.rename.title'); confirm.onclick = () => { const newName = input.value.trim().toLowerCase(); if (!newName || !/^[a-z0-9][a-z0-9\-]{0,19}$/.test(newName)) return; @@ -444,7 +444,7 @@ function showChannelRenameDialog(oldName) { const cancel = document.createElement('button'); cancel.className = 'cancel-btn'; cancel.innerHTML = '✕'; - cancel.title = 'Cancel'; + cancel.title = t('common.cancel'); cancel.onclick = cleanup; wrapper.appendChild(cancel); @@ -482,7 +482,7 @@ function deleteChannel(name) { tab.classList.add('confirm-delete'); tab.classList.remove('editing'); - label.textContent = `delete #${name}?`; + label.textContent = t('channel.deleteConfirm', { name }); if (actions) actions.style.display = 'none'; const confirmBar = document.createElement('span'); @@ -490,12 +490,12 @@ function deleteChannel(name) { const tickBtn = document.createElement('button'); tickBtn.className = 'ch-confirm-yes'; - tickBtn.title = 'Confirm delete'; + tickBtn.title = t('common.confirm'); tickBtn.innerHTML = ''; const crossBtn = document.createElement('button'); crossBtn.className = 'ch-confirm-no'; - crossBtn.title = 'Cancel'; + crossBtn.title = t('common.cancel'); crossBtn.innerHTML = ''; confirmBar.appendChild(tickBtn); @@ -577,12 +577,12 @@ function _updateSupportLabel() { if (!label) return; const inSidebar = document.body.classList.contains('channels-in-sidebar'); if (!inSidebar) { - label.textContent = ' Support development'; + label.textContent = t('header.support'); return; } const panel = document.getElementById('channel-sidebar'); const w = panel ? panel.offsetWidth : 200; - label.textContent = w < 200 ? ' Support' : ' Support development'; + label.textContent = w < 200 ? t('header.support.short') : t('header.support'); } function setupChannelSidebarGrip() { diff --git a/static/chat.js b/static/chat.js index 94c5c373..a9a89ff1 100644 --- a/static/chat.js +++ b/static/chat.js @@ -66,14 +66,14 @@ function enableDragScroll(el) { // --- Notification sounds --- const SOUND_OPTIONS = [ - { value: 'soft-chime', label: 'Soft Chime' }, - { value: 'bright-ping', label: 'Bright Ping' }, - { value: 'gentle-pop', label: 'Gentle Pop' }, - { value: 'alert-tone', label: 'Alert Tone' }, - { value: 'pluck', label: 'Pluck' }, - { value: 'click', label: 'Click' }, - { value: 'warm-bell', label: 'Warm Bell' }, - { value: 'none', label: 'None' }, + { value: 'soft-chime', labelKey: 'sound.softChime' }, + { value: 'bright-ping', labelKey: 'sound.brightPing' }, + { value: 'gentle-pop', labelKey: 'sound.gentlePop' }, + { value: 'alert-tone', labelKey: 'sound.alertTone' }, + { value: 'pluck', labelKey: 'sound.pluck' }, + { value: 'click', labelKey: 'sound.click' }, + { value: 'warm-bell', labelKey: 'sound.warmBell' }, + { value: 'none', labelKey: 'sound.none' }, ]; const DEFAULT_SOUND = 'soft-chime'; const CROSS_CHANNEL_SOUND = 'pluck'; @@ -116,8 +116,8 @@ function buildSoundSettings() { row.className = 'sound-row'; const label = document.createElement('span'); label.className = 'sound-label'; - label.textContent = name === 'default' ? 'Default sound' - : name === 'cross-channel' ? 'Background alerts' + label.textContent = name === 'default' ? t('sound.default') + : name === 'cross-channel' ? t('sound.background') : (agentConfig[name]?.label || name); const select = document.createElement('select'); select.className = 'sound-select'; @@ -127,7 +127,7 @@ function buildSoundSettings() { for (const opt of SOUND_OPTIONS) { const o = document.createElement('option'); o.value = opt.value; - o.textContent = opt.label; + o.textContent = t(opt.labelKey); if (currentVal === opt.value) o.selected = true; select.appendChild(o); } @@ -135,7 +135,7 @@ function buildSoundSettings() { if (name !== 'default' && name !== 'cross-channel') { const o = document.createElement('option'); o.value = ''; - o.textContent = 'Use default'; + o.textContent = t('sound.useDefault'); if (!soundPrefs[name]) o.selected = true; select.insertBefore(o, select.firstChild); } @@ -207,9 +207,9 @@ async function checkForUpdate() { return; } - const label = data.state === 'upstream_update' ? 'Upstream update available' : 'Update available'; + const label = data.state === 'upstream_update' ? t('update.upstream') : t('update.available'); pill.href = data.url || 'https://github.com/bcurts/agentchattr/releases'; - pill.innerHTML = `${label}`; + pill.innerHTML = `${label}`; pill.classList.remove('hidden'); } catch { // Silent fail -- version check should never block the UI @@ -347,17 +347,17 @@ function addCodeCopyButtons(container) { if (pre.querySelector('.code-copy-btn')) continue; const btn = document.createElement('button'); btn.className = 'code-copy-btn'; - btn.textContent = 'copy'; + btn.textContent = t('timeline.copyButton'); btn.onclick = async (e) => { e.stopPropagation(); const code = pre.querySelector('code')?.textContent || pre.textContent; try { await navigator.clipboard.writeText(code); - btn.textContent = 'copied!'; - setTimeout(() => { btn.textContent = 'copy'; }, 1500); + btn.textContent = t('timeline.copied'); + setTimeout(() => { btn.textContent = t('timeline.copyButton'); }, 1500); } catch (err) { - btn.textContent = 'failed'; - setTimeout(() => { btn.textContent = 'copy'; }, 1500); + btn.textContent = t('timeline.failed'); + setTimeout(() => { btn.textContent = t('timeline.copyButton'); }, 1500); } }; pre.style.position = 'relative'; @@ -393,7 +393,7 @@ function connectWebSocket() { const choicesEl = existing.querySelector('.decision-choices'); const meta = updated.metadata || {}; if (choicesEl && meta.resolved) { - choicesEl.innerHTML = `
You chose: ${escapeHtml(meta.chosen || '')}
`; + choicesEl.innerHTML = `
${escapeHtml(t('timeline.youChose', { choice: meta.chosen || '' }))}
`; } } } @@ -633,8 +633,8 @@ function formatDateDivider(dateStr) { const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); - if (date.toDateString() === today.toDateString()) return 'Today'; - if (date.toDateString() === yesterday.toDateString()) return 'Yesterday'; + if (date.toDateString() === today.toDateString()) return t('timeline.today'); + if (date.toDateString() === yesterday.toDateString()) return t('timeline.yesterday'); return date.toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' @@ -676,11 +676,11 @@ function appendMessage(msg) { if (msg.type === 'join' || msg.type === 'leave') { el.classList.add('join-msg'); const color = getColor(msg.sender); - el.innerHTML = `${escapeHtml(msg.sender)} ${msg.type === 'join' ? 'joined' : 'left'}`; + el.innerHTML = `${escapeHtml(msg.sender)} ${msg.type === 'join' ? t('timeline.joined') : t('timeline.left')}`; } else if (msg.type === 'summary') { el.classList.add('summary-msg'); const color = getColor(msg.sender); - el.innerHTML = `
Summary${escapeHtml(msg.sender)}
${escapeHtml(msg.text)}
`; + el.innerHTML = `
${t('timeline.summary')}${escapeHtml(msg.sender)}
${escapeHtml(msg.text)}
`; } else if (msg.type === 'job_proposal') { el.classList.add('proposal-msg'); const meta = msg.metadata || {}; @@ -695,22 +695,22 @@ function appendMessage(msg) { el.innerHTML = `
- Job Proposal + ${t('jobs.proposal')} ${escapeHtml(msg.sender)}
${title}
${body ? `
${body}
` : ''} ${isPending ? `
- - - + + +
` : ` -
${status === 'accepted' ? 'Accepted' : 'Dismissed'}
+
${status === 'accepted' ? t('jobs.accepted') : t('jobs.dismissed')}
`}
- ${!isPending ? `
` : ''}`; + ${!isPending ? `
` : ''}`; } else if (msg.type === 'rule_proposal') { el.classList.add('proposal-msg'); const meta = msg.metadata || {}; @@ -721,21 +721,21 @@ function appendMessage(msg) { el.innerHTML = `
- Rule Proposal + ${t('rules.proposal')} ${escapeHtml(msg.sender)}
${ruleText}
${isPending ? `
- - - + + +
` : ` -
${status === 'activated' ? 'Activated' : status === 'drafted' ? 'Added to drafts' : 'Dismissed'}
+
${status === 'activated' ? t('rules.activated') : status === 'drafted' ? t('rules.addedDrafts') : t('jobs.dismissed')}
`}
- ${!isPending ? `
` : ''}`; + ${!isPending ? `
` : ''}`; } else if (window._messageRenderers && window._messageRenderers[msg.type]) { window._messageRenderers[msg.type](el, msg); } else if (msg.type === 'system' || msg.sender === 'system') { @@ -796,21 +796,21 @@ function appendMessage(msg) { el.dataset.rawText = msg.text; const senderRole = _agentRoles[msg.sender] || ''; const roleClass = senderRole ? 'bubble-role has-role' : 'bubble-role'; - const rolePillHtml = !isSelf ? `` : ''; + const rolePillHtml = !isSelf ? `` : ''; // Inline decision choices (if present) let choicesHtml = ''; const meta = msg.metadata || {}; const choicesList = meta.choices || []; if (msg.type === 'decision' && choicesList.length > 0) { if (meta.resolved) { - choicesHtml = `
You chose: ${escapeHtml(meta.chosen || '')}
`; + choicesHtml = `
${escapeHtml(t('timeline.youChose', { choice: meta.chosen || '' }))}
`; } else { choicesHtml = '
' + choicesList.map(c => `` ).join('') + '
'; } } - el.innerHTML = `
${isSelf ? '' : avatarHtml}
${replyHtml}
${escapeHtml(msg.sender)}${rolePillHtml}${msg.time || ''}
${textHtml}
${choicesHtml}${attachmentsHtml}
`; + el.innerHTML = `
${isSelf ? '' : avatarHtml}
${replyHtml}
${escapeHtml(msg.sender)}${rolePillHtml}${msg.time || ''}
${textHtml}
${choicesHtml}${attachmentsHtml}
`; if (todoStatus) el.classList.add('msg-todo', `msg-todo-${todoStatus}`); if (msg.metadata?.session_output) el.classList.add('session-output'); @@ -1173,6 +1173,22 @@ const ROLE_PRESETS = [ { label: 'Hype', emoji: '๐ŸŽ‰' }, ]; +function roleDisplayLabel(label) { + const keyByLabel = { + 'Planner': 'role.planner', + 'Designer': 'role.designer', + 'Architect': 'role.architect', + 'Builder': 'role.builder', + 'Reviewer': 'role.reviewer', + 'Researcher': 'role.researcher', + 'Red Team': 'role.redTeam', + 'Wry': 'role.wry', + 'Unhinged': 'role.unhinged', + 'Hype': 'role.hype', + }; + return keyByLabel[label] ? t(keyByLabel[label]) : label; +} + // --- Agent naming lightbox --- const _pendingNameQueue = []; @@ -1208,8 +1224,8 @@ function showAgentNameModal(opts) {

- - + +
`; // Close on backdrop click @@ -1236,11 +1252,11 @@ function showAgentNameModal(opts) { if (opts.mode === 'pending') { const familyLabel = (baseColors[opts.base] || {}).label || opts.base || 'agent'; - titleEl.textContent = 'Name this agent'; - subtitleEl.textContent = `A new ${familyLabel} instance connected`; + titleEl.textContent = t('agent.nameThis'); + subtitleEl.textContent = t('agent.newInstance', { family: familyLabel }); } else { - titleEl.textContent = 'Rename agent'; - subtitleEl.textContent = `Current ID: @${opts.name}`; + titleEl.textContent = t('agent.rename'); + subtitleEl.textContent = t('agent.currentId', { name: opts.name }); } inputEl.value = opts.label; @@ -1299,7 +1315,7 @@ function showPillPopover(pillEl, opts) { const currentRole = (_agentRoles[opts.name] || '').toLowerCase(); const roleChipsHtml = ROLE_PRESETS.map(p => - `` + `` ).join(''); const customChipsHtml = (window.customRoles || []) .filter(r => r && !ROLE_PRESETS.some(p => p.label.toLowerCase() === r.toLowerCase())) @@ -1309,21 +1325,21 @@ function showPillPopover(pillEl, opts) { popover.innerHTML = `
- +
- +
- +
- + ${roleChipsHtml} ${customChipsHtml}
- +
${(() => { @@ -1339,15 +1355,15 @@ function showPillPopover(pillEl, opts) { const matchesSwatch = swatches.some(c => current === c.toLowerCase()); const colorInputVal = (current && !current.startsWith('var(')) ? current : (opts.color || '#888888'); return `
- +
${swatches.map(c => `` ).join('')}
- - + +
`; })()} @@ -1531,14 +1547,14 @@ function showBubbleRolePicker(btn, agentName) { // None chip const noneChip = document.createElement('button'); noneChip.className = 'role-preset-chip' + (!currentRole ? ' active' : ''); - noneChip.textContent = 'None'; + noneChip.textContent = t('common.none'); noneChip.addEventListener('click', () => { _setRole(agentName, ''); closePicker(); }); picker.appendChild(noneChip); for (const preset of ROLE_PRESETS) { const chip = document.createElement('button'); chip.className = 'role-preset-chip' + (currentRole === preset.label.toLowerCase() ? ' active' : ''); - chip.textContent = `${preset.emoji} ${preset.label}`; + chip.textContent = `${preset.emoji} ${roleDisplayLabel(preset.label)}`; chip.addEventListener('click', () => { _setRole(agentName, preset.label); closePicker(); }); picker.appendChild(chip); } @@ -1559,7 +1575,7 @@ function showBubbleRolePicker(btn, agentName) { const customInput = document.createElement('input'); customInput.type = 'text'; customInput.className = 'bubble-role-input'; - customInput.placeholder = 'Custom...'; + customInput.placeholder = t('agent.custom.placeholder'); customInput.maxLength = 20; customInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { @@ -1793,7 +1809,7 @@ function _clearClearChatConfirm() { const confirmEl = document.getElementById('clear-chat-confirm'); if (confirmEl) confirmEl.remove(); if (btn) { - btn.textContent = 'Clear Chat'; + btn.textContent = t('timeline.clearChat'); btn.classList.remove('confirming'); } document.removeEventListener('click', _clearChatOutsideClick, true); @@ -1823,17 +1839,17 @@ function clearChat() { return; } - btn.textContent = 'Clear Chat?'; + btn.textContent = t('timeline.clearChatConfirm'); btn.classList.add('confirming'); const confirmWrap = document.createElement('span'); confirmWrap.id = 'clear-chat-confirm'; confirmWrap.className = 'session-inline-confirm'; confirmWrap.innerHTML = ` - - `; @@ -1936,7 +1952,7 @@ async function exportHistory() { }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); - showToast(err.error || 'Export failed', 'error'); + showToast(err.error || t('settings.exportFailed'), 'error'); return; } const blob = await resp.blob(); @@ -1950,9 +1966,9 @@ async function exportHistory() { URL.revokeObjectURL(a.href); const counts = []; // Parse counts from manifest if possible, or just show success - showToast('History exported', 'success'); + showToast(t('settings.exportSuccess'), 'success'); } catch (e) { - showToast('Export failed: ' + e.message, 'error'); + showToast(t('settings.exportFailed') + ': ' + e.message, 'error'); } } @@ -1962,7 +1978,7 @@ async function importHistory(input) { input.value = ''; // Reset so same file can be picked again const btn = document.getElementById('import-history-btn'); const origText = btn.textContent; - btn.textContent = 'Importing...'; + btn.textContent = t('settings.importing'); btn.disabled = true; try { const formData = new FormData(); @@ -1974,25 +1990,25 @@ async function importHistory(input) { }); const data = await resp.json(); if (!resp.ok || !data.ok) { - showToast(data.error || 'Import failed', 'error'); + showToast(data.error || t('settings.importFailed'), 'error'); return; } // Build result message const parts = []; const s = data.sections || {}; - if (s.messages) parts.push(`${s.messages.created} messages`); + if (s.messages) parts.push(`${s.messages.created} ${t('settings.messages')}`); if (s.jobs) parts.push(`${s.jobs.created} jobs`); if (s.rules) parts.push(`${s.rules.created} rules`); if (s.summaries) parts.push(`${s.summaries.created + (s.summaries.updated || 0)} summaries`); const dupes = (s.messages?.duplicates || 0) + (s.jobs?.duplicates || 0) + (s.rules?.duplicates || 0); - let msg = 'Imported ' + parts.join(', '); - if (dupes > 0) msg += ` (${dupes} duplicates skipped)`; + let msg = t('settings.imported', { items: parts.join(', ') }); + if (dupes > 0) msg += ` (${t('settings.duplicateSkipped', { count: dupes })})`; if (data.warnings && data.warnings.length > 0) { - msg += `. ${data.warnings.length} warning(s)`; + msg += `. ${t('settings.warnings', { count: data.warnings.length })}`; } showToast(msg, 'success'); } catch (e) { - showToast('Import failed: ' + e.message, 'error'); + showToast(t('settings.importFailed') + ': ' + e.message, 'error'); } finally { btn.textContent = origText; btn.disabled = false; @@ -2049,6 +2065,22 @@ const SLASH_COMMANDS = [ { cmd: '/clear', desc: 'Clear messages in current channel', broadcast: false }, ]; +function slashDescription(item) { + const keyByCmd = { + '/artchallenge': 'slash.artchallenge', + '/hatmaking': 'slash.hatmaking', + '/roastreview': 'slash.roastreview', + '/poetry haiku': 'slash.haiku', + '/poetry limerick': 'slash.limerick', + '/poetry sonnet': 'slash.sonnet', + '/summary': 'slash.summary', + '/summarise': 'slash.summarise', + '/continue': 'slash.continue', + '/clear': 'slash.clear', + }; + return keyByCmd[item.cmd] ? t(keyByCmd[item.cmd]) : item.desc; +} + let slashMenuIndex = 0; let slashMenuVisible = false; let mentionMenuIndex = 0; @@ -2078,7 +2110,7 @@ function updateSlashMenu(text) { matches.forEach((item, i) => { const row = document.createElement('div'); row.className = 'slash-item' + (i === slashMenuIndex ? ' active' : ''); - row.innerHTML = `${escapeHtml(item.cmd)}${escapeHtml(item.desc)}`; + row.innerHTML = `${escapeHtml(item.cmd)}${escapeHtml(slashDescription(item))}`; row.addEventListener('mousedown', (e) => { e.preventDefault(); selectSlashCommand(item.cmd); @@ -2581,7 +2613,7 @@ function renderReplyPreview() { } const truncated = replyingTo.text.length > 100 ? replyingTo.text.slice(0, 100) + '...' : replyingTo.text; const color = getColor(replyingTo.sender); - container.innerHTML = `replying to ${escapeHtml(replyingTo.sender)}: ${escapeHtml(truncated)} `; + container.innerHTML = `${t('timeline.replyingTo')} ${escapeHtml(replyingTo.sender)}: ${escapeHtml(truncated)} `; } function cancelReply() { @@ -2601,9 +2633,9 @@ function scrollToMessage(msgId) { // --- Todos --- function todoStatusLabel(status) { - if (!status) return 'pin'; - if (status === 'todo') return 'done?'; - return 'unpin'; + if (!status) return t('pins.pin'); + if (status === 'todo') return t('pins.done?'); + return t('pins.unpin'); } function todoCycle(msgId) { @@ -2755,7 +2787,7 @@ function showDeleteBar() { if (!bar) { bar = document.createElement('div'); bar.id = 'delete-bar'; - bar.innerHTML = ``; + bar.innerHTML = ``; const footer = document.querySelector('footer'); footer.parentNode.insertBefore(bar, footer); } @@ -2765,10 +2797,10 @@ function showDeleteBar() { function updateDeleteBar() { const count = deleteSelected.size; const span = document.querySelector('.delete-bar-count'); - if (span) span.textContent = count > 0 ? `${count} selected` : 'Select messages'; + if (span) span.textContent = count > 0 ? t('timeline.selected', { count }) : t('timeline.selectMessages'); const btn = document.querySelector('.delete-bar-confirm'); if (btn) { - btn.textContent = count > 0 ? `Delete (${count})` : 'Delete'; + btn.textContent = count > 0 ? t('timeline.deleteCount', { count }) : t('common.delete'); btn.disabled = count === 0; } } @@ -2868,7 +2900,7 @@ function renderTodosPanel() { const todoIds = Object.keys(todos); if (todoIds.length === 0) { - list.innerHTML = '
No pinned messages
'; + list.innerHTML = `
${t('pins.empty')}
`; return; } @@ -2892,7 +2924,7 @@ function renderTodosPanel() { const checkClass = status === 'done' ? 'todo-check done' : 'todo-check'; const msgChannel = el.dataset.channel || 'general'; - item.innerHTML = `#${msgChannel} ${escapeHtml(time)} ${escapeHtml(sender)} ${escapeHtml(text)}`; + item.innerHTML = `#${msgChannel} ${escapeHtml(time)} ${escapeHtml(sender)} ${escapeHtml(text)}`; item.addEventListener('click', (e) => { if (e.target.closest('button')) return; // Cross-channel pin: switch channel if needed @@ -3239,22 +3271,22 @@ function renderSchedulesBar() { const s = schedulesList[0]; const isPaused = s.active === false; const targetStr = (s.targets || []).map(t => '@' + t).join(', '); - countEl.textContent = `${targetStr} "${s.prompt}"` + (isPaused ? ' (paused)' : ''); + countEl.textContent = `${targetStr} "${s.prompt}"` + (isPaused ? t('schedule.pausedSuffix') : ''); const nextStr = (!isPaused && s.next_run) ? formatScheduleTime(s.next_run) : ''; nextEl.textContent = nextStr - ? formatScheduleInterval(s) + ' -- next in ' + nextStr + ? formatScheduleInterval(s) + ' -- ' + t('schedule.nextIn', { time: nextStr }) : formatScheduleInterval(s); } else if (active.length > 0) { const paused = schedulesList.length - active.length; - const parts = [`${active.length} active`]; - if (paused > 0) parts.push(`${paused} paused`); + const parts = [t('schedule.activeCount', { count: active.length })]; + if (paused > 0) parts.push(t('schedule.pausedCount', { count: paused })); countEl.textContent = parts.join(', '); const futureRuns = active.filter(s => s.next_run && s.next_run * 1000 > Date.now()).map(s => s.next_run); const nextTimeStr = futureRuns.length > 0 ? formatScheduleTime(Math.min(...futureRuns)) : ''; - nextEl.textContent = nextTimeStr ? 'next in ' + nextTimeStr : ''; + nextEl.textContent = nextTimeStr ? t('schedule.nextIn', { time: nextTimeStr }) : ''; } else { const paused = schedulesList.length; - countEl.textContent = `${paused} paused schedule${paused !== 1 ? 's' : ''}`; + countEl.textContent = t('schedule.paused', { count: paused, plural: paused !== 1 ? 's' : '' }); nextEl.textContent = ''; } @@ -3269,13 +3301,13 @@ function renderSchedulesBar() { pauseBtn.innerHTML = isPaused ? '' : ''; - pauseBtn.title = isPaused ? 'Resume' : 'Pause'; + pauseBtn.title = isPaused ? t('schedule.resume') : t('schedule.pause'); pauseBtn.onclick = () => toggleSchedule(s.id); const trashBtn = document.createElement('button'); trashBtn.className = 'schedule-delete-inline'; trashBtn.innerHTML = ''; - trashBtn.title = 'Delete'; + trashBtn.title = t('common.delete'); trashBtn.onclick = () => deleteSchedule(s.id); summaryDiv.append(pauseBtn, trashBtn); @@ -3283,7 +3315,7 @@ function renderSchedulesBar() { // Collapse expanded list โ€” summary has everything bar.classList.remove('expanded'); document.getElementById('schedules-list')?.classList.add('hidden'); - if (seeAllLink) seeAllLink.textContent = 'See all'; + if (seeAllLink) seeAllLink.textContent = t('schedule.seeAll'); } else { if (seeAllLink) seeAllLink.style.display = ''; } @@ -3314,13 +3346,13 @@ function renderSchedulesBar() { const toggleBtn = document.createElement('button'); toggleBtn.className = 'schedule-toggle'; toggleBtn.textContent = s.active === false ? 'โ–ถ' : 'โธ'; - toggleBtn.title = s.active === false ? 'Resume' : 'Pause'; + toggleBtn.title = s.active === false ? t('schedule.resume') : t('schedule.pause'); toggleBtn.onclick = () => toggleSchedule(s.id); const deleteBtn = document.createElement('button'); deleteBtn.className = 'schedule-delete'; deleteBtn.innerHTML = ''; - deleteBtn.title = 'Delete'; + deleteBtn.title = t('common.delete'); deleteBtn.onclick = () => deleteSchedule(s.id); row.append(targets, prompt, interval, toggleBtn, deleteBtn); @@ -3344,11 +3376,11 @@ function formatScheduleTime(ts) { } function formatScheduleInterval(s) { - if (s.daily_at) return 'daily at ' + s.daily_at; + if (s.daily_at) return t('schedule.dailyAt', { time: s.daily_at }); const sec = s.interval_seconds || 0; - if (sec < 3600) return 'every ' + Math.round(sec / 60) + 'm'; - if (sec < 86400) return 'every ' + Math.round(sec / 3600) + 'h'; - return 'every ' + Math.round(sec / 86400) + 'd'; + if (sec < 3600) return t('schedule.everyShort', { value: Math.round(sec / 60), unit: 'm' }); + if (sec < 86400) return t('schedule.everyShort', { value: Math.round(sec / 3600), unit: 'h' }); + return t('schedule.everyShort', { value: Math.round(sec / 86400), unit: 'd' }); } function toggleSchedulesExpand() { @@ -3358,7 +3390,7 @@ function toggleSchedulesExpand() { const expanded = bar.classList.toggle('expanded'); list.classList.toggle('hidden', !expanded); const link = bar.querySelector('.schedules-see-all'); - if (link) link.textContent = expanded ? 'Hide' : 'See all'; + if (link) link.textContent = expanded ? t('schedule.hide') : t('schedule.seeAll'); } async function toggleSchedule(id) { @@ -3446,14 +3478,14 @@ function populateScheduleDropdowns() { // Date options: Today, Tomorrow, then next 5 weekdays dateEl.innerHTML = ''; - const days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; + const days = [t('schedule.sunday'), t('schedule.monday'), t('schedule.tuesday'), t('schedule.wednesday'), t('schedule.thursday'), t('schedule.friday'), t('schedule.saturday')]; const now = new Date(); for (let i = 0; i < 7; i++) { const d = new Date(now); d.setDate(d.getDate() + i); const opt = document.createElement('option'); opt.value = d.toISOString().slice(0, 10); - opt.textContent = i === 0 ? 'Today' : i === 1 ? 'Tomorrow' : days[d.getDay()]; + opt.textContent = i === 0 ? t('schedule.today') : i === 1 ? t('schedule.tomorrow') : days[d.getDay()]; dateEl.appendChild(opt); } @@ -3515,7 +3547,7 @@ function updateSchedulePopoverState() { const targets = new Set(mentionMatches.map(m => m.slice(1))); for (const name of activeMentions) targets.add(name); if (targets.size === 0) { - if (errEl) { errEl.textContent = 'Toggle an agent to set a target'; errEl.classList.remove('hidden'); } + if (errEl) { errEl.textContent = t('schedule.targetError'); errEl.classList.remove('hidden'); } if (submitBtn) { submitBtn.disabled = true; } } else { if (errEl) { errEl.classList.add('hidden'); errEl.textContent = ''; } @@ -3537,7 +3569,7 @@ async function submitSchedulePopover() { if (targets.size === 0) return; // button should be disabled anyway if (!prompt) { - if (errEl) { errEl.textContent = 'Type a message first'; errEl.classList.remove('hidden'); } + if (errEl) { errEl.textContent = t('schedule.messageError'); errEl.classList.remove('hidden'); } return; } @@ -3587,11 +3619,11 @@ async function submitSchedulePopover() { showScheduleConfirmation(); } else { const err = await resp.json().catch(() => ({})); - showSlashHint(err.error || 'Failed to schedule'); + showSlashHint(err.error || t('schedule.failed')); } } catch (e) { console.error('Failed to create schedule:', e); - showSlashHint('Failed to create schedule'); + showSlashHint(t('schedule.createFailed')); } } @@ -3651,90 +3683,88 @@ function _helpCardDefs() { anchor: '#agent-status', row: 'top', html: - '
Agents header pills
' + - '

The status pills in the header show who is here and what they\'re doing.

' + + `
${t('help.agents.title')} ${t('help.agents.loc')}
` + + `

${t('help.agents.desc')}

` + '
' + '
' + '' + 'claude' + '
' + - 'Online' + + `${t('help.agents.online')}` + '
' + '
' + '
' + '' + 'codex' + '
' + - 'Working — spinning border' + + `${t('help.agents.working')}` + '
' + '
' + '
' + '' + 'gemini' + '
' + - 'Offline' + + `${t('help.agents.offline')}` + '
' + - '

Click any pill to rename it, assign a role, or change its colour.

' + `

${t('help.agents.tip')}

` }, { id: 'hg-jobs', anchor: '#jobs-toggle', row: 'top', html: - '
Jobs hover any message + sidebar
' + - '

Jobs turn conversation into tracked work. Any message can become a job.

' + + `
${t('help.jobs.title')} ${t('help.jobs.loc')}
` + + `

${t('help.jobs.desc')}

` + '
' + '
' + '
' + 'claude' + - 'The auth module needs refactoring before we can add SSO support.' + + `${t('help.jobs.sample')}` + '
' + - 'convert to job' + + `${t('timeline.convertJob')}` + '
' + - '

Hover any message to see convert to job. This opens the Jobs sidebar.

' + + `

${t('help.jobs.tip1')}

` + '
' + '
' + - 'Jobs sidebar' + + `${t('help.jobs.sidebar')}` + '+' + '
' + '
' + '' + - 'Auth refactor' + + `${t('help.jobs.jobTitle')}` + '
' + - 'TO DO' + - 'ACTIVE' + - 'CLOSED' + + `${t('common.todo')}` + + `${t('common.active')}` + + `${t('common.closed')}` + '
' + '
' + '
' + - '

Jobs live in the sidebar. Each one opens its own thread, and you can track it from TO DO to ACTIVE to CLOSED.

' + `

${t('help.jobs.tip2')}

` }, { id: 'hg-rules', anchor: '#rules-toggle', row: 'top', html: - '
Rules top-right panel
' + - '

Rules tell agents how to work in this room. Active rules are followed automatically.

' + + `
${t('help.rules.title')} ${t('help.rules.loc')}
` + + `

${t('help.rules.desc')}

` + '
' + '
' + - 'Rules' + + `${t('rules.title')}` + '2' + '+' + '
' + '
' + '' + - 'Run tests before committing' + - 'draft' + + `${t('help.rules.sampleDraft')}` + + `${t('help.rules.draft')}` + '
' + '
' + '' + - 'Always explain your reasoning before making changes' + + `${t('help.rules.sampleActive')}` + '
' + '
' + - '

Active = agents follow this. ' + - ' Draft = not active yet. ' + - 'You or agents can add rules.

' + `

${t('help.rules.tip')}

` }, { id: 'hg-channels', @@ -3743,15 +3773,15 @@ function _helpCardDefs() { light: true, wide: true, html: - '
Channels top bar
' + - '

Split conversations by topic. Each channel has its own message history.

' + + `
${t('help.channels.title')} ${t('help.channels.loc')}
` + + `

${t('help.channels.desc')}

` + '
' + '# general' + '# design' + '# backend' + '+' + '
' + - '

Click + to create a new channel. Agents can be mentioned in any channel.

' + `

${t('help.channels.tip')}

` }, { id: 'hg-mentions', @@ -3759,62 +3789,62 @@ function _helpCardDefs() { row: 'bottom', light: true, html: - '
Mentions pills above composer
' + - '

Type @ in the composer to mention an agent. The pills above the input let you pre-select who to address. Selected agents are mentioned automatically when you send.

' + - '

Mentioned agents receive a trigger and will respond in the channel.

' + `
${t('help.mentions.title')} ${t('help.mentions.loc')}
` + + `

${t('help.mentions.desc')}

` + + `

${t('help.mentions.tip')}

` }, { id: 'hg-sessions', anchor: '#session-launch-btn', row: 'bottom', html: - '
Sessions play button
' + - '

Sessions are structured multi-agent workflows with phases and roles. Launch one to coordinate agents on a shared goal.

' + + `
${t('help.sessions.title')} ${t('help.sessions.loc')}
` + + `

${t('help.sessions.desc')}

` + '
' + '
' + '' + '
' + - 'Brainstorm' + - 'Free-form idea generation with all agents' + + `${t('help.sessions.brainstorm')}` + + `${t('help.sessions.brainstormDesc')}` + '
' + '
' + '
' + '' + '
' + - 'Code Review' + - 'Structured review with phases and roles' + + `${t('help.sessions.codeReview')}` + + `${t('help.sessions.codeReviewDesc')}` + '
' + '
' + '
' + '' + '
' + - 'Design Review' + - 'Critique and iterate on design decisions' + + `${t('help.sessions.designReview')}` + + `${t('help.sessions.designReviewDesc')}` + '
' + '
' + '
' + '
' + - 'Custom sessions — describe a goal and agents will organise themselves into a structured workflow.' + + `${t('help.sessions.custom')} -- ${t('help.sessions.customDesc')}` + '
' + - '

Click the ▶ play button next to the composer to start.

' + `

${t('help.sessions.tip')}

` }, { id: 'hg-scheduling', anchor: '#schedule-btn', row: 'bottom', html: - '
Scheduling clock button
' + - '

Schedule any message for later delivery, or set up recurring prompts on a timer.

' + + `
${t('help.scheduling.title')} ${t('help.scheduling.loc')}
` + + `

${t('help.scheduling.desc')}

` + '
' + '3:00 PM' + - '@codex check deployment status' + - 'every 1h' + + `${t('help.scheduling.sample1')}` + + `${t('help.scheduling.recur')}` + '
' + '
' + - 'Tomorrow 9 AM' + - '@claude summarise overnight changes' + + `${t('help.scheduling.tomorrow9')}` + + `${t('help.scheduling.sample2')}` + '
' + - '

Click the ⏱ clock button next to Send to schedule a one-time or recurring message.

' + `

${t('help.scheduling.tip')}

` } ]; } @@ -3924,7 +3954,7 @@ function _openHelpAnchored(cardDefs) { // Dismiss hint var hint = document.createElement('div'); hint.className = 'hg-dismiss-hint'; - hint.textContent = 'Press Esc or click outside to close'; + hint.textContent = t('help.dismissHint'); overlay.appendChild(hint); document.body.appendChild(overlay); @@ -4046,7 +4076,7 @@ function _openHelpStacked(cardDefs) { // Sticky header var header = document.createElement('div'); header.className = 'hg-modal-header'; - header.innerHTML = 'Guide'; + header.innerHTML = `${t('help.guide')}`; var closeBtn = document.createElement('button'); closeBtn.className = 'hg-modal-close'; closeBtn.innerHTML = '×'; diff --git a/static/i18n.js b/static/i18n.js new file mode 100644 index 00000000..fd09633c --- /dev/null +++ b/static/i18n.js @@ -0,0 +1,877 @@ +/** + * i18n.js -- lightweight runtime localization for agentchattr. + * + * The app is static JS without a build step, so translations live here and are + * applied directly to DOM nodes or requested by dynamic renderers via t(). + */ + +(function () { + 'use strict'; + + const STORAGE_KEY = 'agentchattr-language'; + + const I18N_STRINGS = { + en: { + 'lang.auto': 'Auto', + 'lang.en': 'English', + 'lang.zhCN': 'Simplified Chinese', + + 'settings.language': 'Language', + 'settings.theme': 'Theme', + 'settings.theme.light': 'Light', + 'settings.theme.dark': 'Dark', + 'settings.name': 'Name', + 'settings.name.placeholder': 'your name', + 'settings.font': 'Font', + 'settings.font.mono': 'Monospace', + 'settings.font.sans': 'Sans', + 'settings.loopGuard': 'Loop guard', + 'settings.loopGuard.title': 'Max agent-to-agent hops before pausing', + 'settings.history': 'History', + 'settings.history.all': 'All', + 'settings.contrast': 'Contrast', + 'settings.contrast.normal': 'Normal', + 'settings.contrast.high': 'High', + 'settings.channels': 'Channels', + 'settings.channels.title': 'Show channels in a sidebar (Discord/Slack-style) instead of a top bar', + 'settings.channels.top': 'Top bar', + 'settings.channels.sidebar': 'Sidebar', + 'settings.rulesRefresh': 'Rule refresh', + 'settings.rulesRefresh.title': 'How often to re-send active rules to agents', + 'settings.rulesRefresh.change': 'On rule changes', + 'settings.rulesRefresh.every': 'Every {count} triggers', + 'settings.rulesRefresh.every5': 'Every 5 triggers', + 'settings.rulesRefresh.every10': 'Every 10 triggers', + 'settings.rulesRefresh.every20': 'Every 20 triggers', + 'settings.rulesRefresh.every50': 'Every 50 triggers', + 'settings.projectHistory': 'Project History', + 'settings.exportHistory': 'Export History', + 'settings.importHistory': 'Import History', + 'settings.importing': 'Importing...', + 'settings.exportFailed': 'Export failed', + 'settings.exportSuccess': 'History exported', + 'settings.importFailed': 'Import failed', + 'settings.imported': 'Imported {items}', + 'settings.messages': 'messages', + 'settings.duplicateSkipped': '{count} duplicates skipped', + 'settings.warnings': '{count} warning(s)', + + 'header.discord.title': 'Join the Discord', + 'header.jobs.title': 'Jobs', + 'header.rules.title': 'Rules', + 'header.pins.title': 'Pinned messages', + 'header.settings.title': 'Settings', + 'header.help.title': 'Help', + 'header.support.title': 'Support development', + 'header.support': ' Support development', + 'header.support.short': ' Support', + + 'common.send': 'Send', + 'common.cancel': 'Cancel', + 'common.create': 'Create', + 'common.save': 'Save', + 'common.saved': 'Saved', + 'common.delete': 'Delete', + 'common.delete?': 'Delete?', + 'common.confirm': 'Confirm', + 'common.rename': 'Rename', + 'common.dismiss': 'Dismiss', + 'common.back': 'Back', + 'common.none': 'None', + 'common.reset': 'Reset', + 'common.active': 'ACTIVE', + 'common.closed': 'CLOSED', + 'common.todo': 'TO DO', + 'common.archive': 'ARCHIVE', + 'common.drafts': 'DRAFTS', + 'common.open': 'Open', + 'common.yes': 'Yes', + 'common.no': 'No', + 'common.error': 'Error: {message}', + + 'pins.title': 'Pinned', + 'pins.empty': 'No pinned messages', + 'pins.remove': 'Remove from todos', + 'pins.pin': 'pin', + 'pins.done?': 'done?', + 'pins.unpin': 'unpin', + + 'channel.add.title': 'Add channel', + 'channel.rename.title': 'Rename', + 'channel.delete.title': 'Delete', + 'channel.deleteConfirm': 'delete #{name}?', + + 'timeline.loading': 'Loading messages...', + 'timeline.thinking': 'is thinking...', + 'timeline.joined': 'joined', + 'timeline.left': 'left', + 'timeline.summary': 'Summary', + 'timeline.today': 'Today', + 'timeline.yesterday': 'Yesterday', + 'timeline.reply': 'reply', + 'timeline.deleteShort': 'del', + 'timeline.copy': 'Copy message', + 'timeline.copyButton': 'copy', + 'timeline.copied': 'copied!', + 'timeline.failed': 'failed', + 'timeline.openPath': 'Open in file manager', + 'timeline.convertJob': 'convert to job', + 'timeline.setRole': 'Set role', + 'timeline.chooseRole': 'choose a role', + 'timeline.youChose': 'You chose: {choice}', + 'timeline.replyingTo': 'replying to', + 'timeline.selected': '{count} selected', + 'timeline.selectMessages': 'Select messages', + 'timeline.deleteCount': 'Delete ({count})', + 'timeline.clearChat': 'Clear Chat', + 'timeline.clearChat.title': 'Clear chat in this channel', + 'timeline.clearChatConfirm': 'Clear Chat?', + 'timeline.confirmClearChat.title': 'Confirm clear chat', + + 'composer.startSession.title': 'Start a session', + 'composer.placeholder': 'Type a message... (use @name to mention agents)', + 'composer.voice.title': 'Voice typing (local speech-to-text)', + 'composer.voice.label': 'Voice typing', + 'composer.schedule.title': 'Schedule message', + 'composer.hint': 'Enter to send ยท Shift+Enter for newline ยท Paste/drop images ยท Mic for voice ยท Type / or @ for autocomplete', + 'composer.dropImage': 'Drop image here', + 'composer.speechUnsupported': 'Speech recognition not supported -- use Chrome or Edge.', + 'composer.micBlocked': 'Microphone access was blocked. Allow microphone access in Chrome and try again.', + + 'sound.default': 'Default sound', + 'sound.background': 'Background alerts', + 'sound.useDefault': 'Use default', + 'sound.softChime': 'Soft Chime', + 'sound.brightPing': 'Bright Ping', + 'sound.gentlePop': 'Gentle Pop', + 'sound.alertTone': 'Alert Tone', + 'sound.pluck': 'Pluck', + 'sound.click': 'Click', + 'sound.warmBell': 'Warm Bell', + 'sound.none': 'None', + + 'update.upstream': 'Upstream update available', + 'update.available': 'Update available', + 'update.dismiss': 'Dismiss', + + 'agent.nameThis': 'Name this agent', + 'agent.newInstance': 'A new {family} instance connected', + 'agent.rename': 'Rename agent', + 'agent.currentId': 'Current ID: @{name}', + 'agent.role': 'Role', + 'agent.customRole.placeholder': 'Custom role...', + 'agent.color': 'Color', + 'agent.pickColor.title': 'Pick custom color', + 'agent.resetColor.title': 'Reset to default', + 'agent.custom.placeholder': 'Custom...', + + 'role.planner': 'Planner', + 'role.designer': 'Designer', + 'role.architect': 'Architect', + 'role.builder': 'Builder', + 'role.reviewer': 'Reviewer', + 'role.researcher': 'Researcher', + 'role.redTeam': 'Red Team', + 'role.wry': 'Wry', + 'role.unhinged': 'Unhinged', + 'role.hype': 'Hype', + + 'jobs.title': 'Jobs', + 'jobs.create.title': 'Create Job', + 'jobs.openJob.title': 'Open job', + 'jobs.newJob': 'New job:', + 'jobs.proposal': 'Job Proposal', + 'jobs.accept': 'Accept', + 'jobs.requestChanges': 'Request Changes', + 'jobs.dismissed': 'Dismissed', + 'jobs.started': '{count} jobs were started', + 'jobs.none': 'none', + 'jobs.unread.title': 'Unread messages', + 'jobs.empty.title': 'No jobs yet', + 'jobs.empty.meta': 'Create one or convert a chat message', + 'jobs.dropDeleteOne': 'Drop to delete job', + 'jobs.dropDeleteMany': 'Drop to delete {count} jobs', + 'jobs.assign.placeholder': 'Send to assign ยท click to edit', + 'jobs.message.placeholder': 'Message...', + 'jobs.emptyMessages': 'No messages yet. Start the conversation!', + 'jobs.loadFailed': 'Failed to load messages.', + 'jobs.deleteShort': 'DEL', + 'jobs.title.placeholder': 'Job title', + 'jobs.body.placeholder': 'Description (optional)', + 'jobs.accepted': 'Accepted', + 'jobs.deleteSelected': 'Delete {count} selected', + 'jobs.dragDelete': 'Drag here to delete', + 'jobs.convertTitle': 'Convert to Job', + 'jobs.convertSubtitle': 'An agent will write a job proposal for you to accept', + 'jobs.convertLabel': 'Ask agent to write proposal', + 'jobs.convert': 'Convert', + 'jobs.askingAgent': 'Asking @{agent} to create a job card...', + 'jobs.deletePermanentTitle': 'Delete Job Permanently?', + 'jobs.deletePermanentSubtitle': 'This removes the job and its messages permanently. This cannot be undone.', + 'jobs.replyTarget.title': 'Reply target', + 'jobs.clearReplyTarget.title': 'Clear reply target', + 'jobs.replyTargetPrefix': 'To', + + 'rules.title': 'Rules', + 'rules.proposal': 'Rule Proposal', + 'rules.activate': 'Activate', + 'rules.addDrafts': 'Add to drafts', + 'rules.activated': 'Activated', + 'rules.addedDrafts': 'Added to drafts', + 'rules.add.title': 'Add Rule', + 'rules.remind': 'Remind agents', + 'rules.queued': 'Queued -- next trigger', + 'rules.empty.title': 'No rules yet', + 'rules.empty.meta': 'Tell your agents how to work', + 'rules.nextTrigger': 'New rules are sent on the next agent trigger', + 'rules.softWarning': 'Less than seven active rules tends to work better', + 'rules.dragDelete': 'Drag here to delete', + 'rules.create.placeholder': 'Write a short rule agents should follow', + 'rules.deleteConfirm': 'Delete?', + 'rules.rejectedPrefix': 'Rule rejected', + + 'sessions.goToChannel': 'Go to channel', + 'sessions.end': 'End Session', + 'sessions.endConfirm': 'End Session?', + 'sessions.confirmEnd.title': 'Confirm end session', + 'sessions.activeIn': 'Session active in #{channel}', + 'sessions.activeElsewhere': '{count} sessions active elsewhere', + 'sessions.session': 'Session', + 'sessions.customSession': 'Custom session', + 'sessions.waitingFor': 'Waiting for {agent}', + 'sessions.paused': 'Paused', + 'sessions.viewOutput': 'View output', + 'sessions.proposal': 'Session Proposal', + 'sessions.superseded': 'Superseded draft', + 'sessions.replacedByRevision': 'Replaced by revision {revision}.', + 'sessions.invalidDraft': 'Invalid session draft', + 'sessions.invalidDraftHelp': 'This draft needs changes before it can be run or saved.', + 'sessions.requestChanges': 'Request Changes', + 'sessions.run': 'Run', + 'sessions.saveTemplate': 'Save Template', + 'sessions.deleteTemplate.title': 'Delete custom template', + 'sessions.cast': 'Cast', + 'sessions.phaseNumber': 'Phase {number}', + 'sessions.goToChannelNamed': 'Go to #{channel}', + 'sessions.goToChannelExtra': 'Go to #{channel} (+{count})', + 'sessions.template.codeReview.name': 'Code Review', + 'sessions.template.codeReview.description': 'Structured code review with builder, reviewer, and red team.', + 'sessions.template.debate.name': 'Debate', + 'sessions.template.debate.description': 'Two agents argue opposing positions. A third moderates and delivers a verdict.', + 'sessions.template.designCritique.name': 'Design Critique', + 'sessions.template.designCritique.description': 'One agent presents work, one critiques it, one synthesises the outcome.', + 'sessions.template.planning.name': 'Planning', + 'sessions.template.planning.description': 'Turn a problem into an execution plan through framing, challenge, and synthesis.', + 'sessions.role.builder': 'builder', + 'sessions.role.reviewer': 'reviewer', + 'sessions.role.red_team': 'red_team', + 'sessions.role.synthesiser': 'synthesiser', + 'sessions.role.proposer': 'proposer', + 'sessions.role.for': 'for', + 'sessions.role.against': 'against', + 'sessions.role.moderator': 'moderator', + 'sessions.role.presenter': 'presenter', + 'sessions.role.critic': 'critic', + 'sessions.role.planner': 'planner', + 'sessions.role.challenger': 'challenger', + 'sessions.phase.submit': 'Submit', + 'sessions.phase.review': 'Review', + 'sessions.phase.respond': 'Respond', + 'sessions.phase.summary': 'Summary', + 'sessions.phase.frame': 'Frame', + 'sessions.phase.opening_arguments': 'Opening Arguments', + 'sessions.phase.rebuttals': 'Rebuttals', + 'sessions.phase.verdict': 'Verdict', + 'sessions.phase.present': 'Present', + 'sessions.phase.critique': 'Critique', + 'sessions.phase.synthesis': 'Synthesis', + 'sessions.phase.challenge': 'Challenge', + 'sessions.phase.plan': 'Plan', + 'sessions.design.placeholder': 'Describe the session you want...', + 'sessions.goal.placeholder': 'Goal (optional) -- what should this session achieve?', + 'sessions.goalShort.placeholder': 'Goal (optional)', + 'sessions.changes.placeholder': 'What changes do you want?', + 'sessions.sendDesignFailed': 'Failed to send design request (HTTP {status})', + 'sessions.startFailed': 'Failed to start session', + 'sessions.startError': 'Error starting session: {message}', + 'sessions.endFailed': 'Failed to end session', + 'sessions.endError': 'Error ending session: {message}', + 'sessions.startDraftFailed': 'Failed to start session from draft', + 'sessions.saveFailed': 'Failed to save template', + 'sessions.deleteFailed': 'Failed to delete template', + 'sessions.connectionLost': 'Connection lost. Reconnect and try again.', + 'sessions.dismissFailed': 'Failed to dismiss session proposal', + + 'schedule.seeAll': 'See all', + 'schedule.hide': 'Hide', + 'schedule.message': 'Schedule message', + 'schedule.when': 'When', + 'schedule.at': 'At', + 'schedule.recurring': 'Recurring', + 'schedule.every': 'Every', + 'schedule.minutes': 'minutes', + 'schedule.hours': 'hours', + 'schedule.days': 'days', + 'schedule.submit': 'Schedule', + 'schedule.today': 'Today', + 'schedule.tomorrow': 'Tomorrow', + 'schedule.sunday': 'Sunday', + 'schedule.monday': 'Monday', + 'schedule.tuesday': 'Tuesday', + 'schedule.wednesday': 'Wednesday', + 'schedule.thursday': 'Thursday', + 'schedule.friday': 'Friday', + 'schedule.saturday': 'Saturday', + 'schedule.nextIn': 'next in {time}', + 'schedule.paused': '{count} paused schedule{plural}', + 'schedule.activeCount': '{count} active', + 'schedule.pausedCount': '{count} paused', + 'schedule.pausedSuffix': ' (paused)', + 'schedule.dailyAt': 'daily at {time}', + 'schedule.everyShort': 'every {value}{unit}', + 'schedule.pause': 'Pause', + 'schedule.resume': 'Resume', + 'schedule.failed': 'Failed to schedule', + 'schedule.createFailed': 'Failed to create schedule', + 'schedule.targetError': 'Toggle an agent to set a target', + 'schedule.messageError': 'Type a message first', + + 'slash.artchallenge': 'SVG art challenge -- all agents create artwork (optional theme)', + 'slash.hatmaking': 'All agents design a hat to wear on their avatar', + 'slash.roastreview': 'Get all agents to review and roast each other\'s work', + 'slash.haiku': 'Agents write a haiku about the codebase', + 'slash.limerick': 'Agents write a limerick about the codebase', + 'slash.sonnet': 'Agents write a sonnet about the codebase', + 'slash.summary': 'Summarize recent messages -- tag an agent (e.g. /summary @claude)', + 'slash.summarise': 'Summarize recent messages -- tag an agent (e.g. /summarise @claude)', + 'slash.continue': 'Resume after loop guard pauses', + 'slash.clear': 'Clear messages in current channel', + + 'help.guide': 'Guide', + 'help.dismissHint': 'Press Esc or click outside to close', + 'help.agents.title': 'Agents', + 'help.agents.loc': 'header pills', + 'help.agents.desc': 'The status pills in the header show who is here and what they\'re doing.', + 'help.agents.online': 'Online', + 'help.agents.working': 'Working -- spinning border', + 'help.agents.offline': 'Offline', + 'help.agents.tip': 'Click any pill to rename it, assign a role, or change its color.', + 'help.jobs.title': 'Jobs', + 'help.jobs.loc': 'hover any message + sidebar', + 'help.jobs.desc': 'Jobs turn conversation into tracked work. Any message can become a job.', + 'help.jobs.sample': 'The auth module needs refactoring before we can add SSO support.', + 'help.jobs.tip1': 'Hover any message to see convert to job. This opens the Jobs sidebar.', + 'help.jobs.sidebar': 'Jobs sidebar', + 'help.jobs.jobTitle': 'Auth refactor', + 'help.jobs.tip2': 'Jobs live in the sidebar. Each one opens its own thread, and you can track it from TO DO to ACTIVE to CLOSED.', + 'help.rules.title': 'Rules', + 'help.rules.loc': 'top-right panel', + 'help.rules.desc': 'Rules tell agents how to work in this room. Active rules are followed automatically.', + 'help.rules.sampleDraft': 'Run tests before committing', + 'help.rules.sampleActive': 'Always explain your reasoning before making changes', + 'help.rules.draft': 'draft', + 'help.rules.tip': 'Active = agents follow this. Draft = not active yet. You or agents can add rules.', + 'help.channels.title': 'Channels', + 'help.channels.loc': 'top bar', + 'help.channels.desc': 'Split conversations by topic. Each channel has its own message history.', + 'help.channels.tip': 'Click + to create a new channel. Agents can be mentioned in any channel.', + 'help.mentions.title': 'Mentions', + 'help.mentions.loc': 'pills above composer', + 'help.mentions.desc': 'Type @ in the composer to mention an agent. The pills above the input let you pre-select who to address. Selected agents are mentioned automatically when you send.', + 'help.mentions.tip': 'Mentioned agents receive a trigger and will respond in the channel.', + 'help.sessions.title': 'Sessions', + 'help.sessions.loc': 'play button', + 'help.sessions.desc': 'Sessions are structured multi-agent workflows with phases and roles. Launch one to coordinate agents on a shared goal.', + 'help.sessions.brainstorm': 'Brainstorm', + 'help.sessions.brainstormDesc': 'Free-form idea generation with all agents', + 'help.sessions.codeReview': 'Code Review', + 'help.sessions.codeReviewDesc': 'Structured review with phases and roles', + 'help.sessions.designReview': 'Design Review', + 'help.sessions.designReviewDesc': 'Critique and iterate on design decisions', + 'help.sessions.custom': 'Custom sessions', + 'help.sessions.customDesc': 'describe a goal and agents will organize themselves into a structured workflow.', + 'help.sessions.tip': 'Click the play button next to the composer to start.', + 'help.scheduling.title': 'Scheduling', + 'help.scheduling.loc': 'clock button', + 'help.scheduling.desc': 'Schedule any message for later delivery, or set up recurring prompts on a timer.', + 'help.scheduling.sample1': '@codex check deployment status', + 'help.scheduling.sample2': '@claude summarize overnight changes', + 'help.scheduling.recur': 'every 1h', + 'help.scheduling.tomorrow9': 'Tomorrow 9 AM', + 'help.scheduling.tip': 'Click the clock button next to Send to schedule a one-time or recurring message.', + }, + zhCN: { + 'lang.auto': '่‡ชๅŠจ', + 'lang.en': 'English', + 'lang.zhCN': '็ฎ€ไฝ“ไธญๆ–‡', + + 'settings.language': '่ฏญ่จ€', + 'settings.theme': 'ไธป้ข˜', + 'settings.theme.light': '็™ฝๅคฉ', + 'settings.theme.dark': '้ป‘ๅคœ', + 'settings.name': 'ๅ็งฐ', + 'settings.name.placeholder': 'ไฝ ็š„ๅๅญ—', + 'settings.font': 'ๅญ—ไฝ“', + 'settings.font.mono': '็ญ‰ๅฎฝ', + 'settings.font.sans': 'ๆ— ่กฌ็บฟ', + 'settings.loopGuard': 'ๅพช็ŽฏไฟๆŠค', + 'settings.loopGuard.title': 'ไปฃ็†่ฟž็ปญไบ’็›ธ่งฆๅ‘ๅคšๅฐ‘ๆฌกๅŽๆš‚ๅœ', + 'settings.history': 'ๅކๅฒ่ฎฐๅฝ•', + 'settings.history.all': 'ๅ…จ้ƒจ', + 'settings.contrast': 'ๅฏนๆฏ”ๅบฆ', + 'settings.contrast.normal': 'ๆ™ฎ้€š', + 'settings.contrast.high': '้ซ˜', + 'settings.channels': '้ข‘้“', + 'settings.channels.title': '็”จไพง่พนๆ ๆ˜พ็คบ้ข‘้“๏ผŒ่€Œไธๆ˜ฏ้กถ้ƒจๆ ', + 'settings.channels.top': '้กถ้ƒจๆ ', + 'settings.channels.sidebar': 'ไพง่พนๆ ', + 'settings.rulesRefresh': '่ง„ๅˆ™ๅˆทๆ–ฐ', + 'settings.rulesRefresh.title': 'ๅคšไน…้‡ๆ–ฐๆŠŠๅฏ็”จ่ง„ๅˆ™ๅ‘้€็ป™ไปฃ็†', + 'settings.rulesRefresh.change': '่ง„ๅˆ™ๅ˜ๆ›ดๆ—ถ', + 'settings.rulesRefresh.every': 'ๆฏ {count} ๆฌก่งฆๅ‘', + 'settings.rulesRefresh.every5': 'ๆฏ 5 ๆฌก่งฆๅ‘', + 'settings.rulesRefresh.every10': 'ๆฏ 10 ๆฌก่งฆๅ‘', + 'settings.rulesRefresh.every20': 'ๆฏ 20 ๆฌก่งฆๅ‘', + 'settings.rulesRefresh.every50': 'ๆฏ 50 ๆฌก่งฆๅ‘', + 'settings.projectHistory': '้กน็›ฎๅކๅฒ', + 'settings.exportHistory': 'ๅฏผๅ‡บๅކๅฒ', + 'settings.importHistory': 'ๅฏผๅ…ฅๅކๅฒ', + 'settings.importing': 'ๆญฃๅœจๅฏผๅ…ฅ...', + 'settings.exportFailed': 'ๅฏผๅ‡บๅคฑ่ดฅ', + 'settings.exportSuccess': 'ๅކๅฒๅทฒๅฏผๅ‡บ', + 'settings.importFailed': 'ๅฏผๅ…ฅๅคฑ่ดฅ', + 'settings.imported': 'ๅทฒๅฏผๅ…ฅ {items}', + 'settings.messages': 'ๆกๆถˆๆฏ', + 'settings.duplicateSkipped': '่ทณ่ฟ‡ {count} ไธช้‡ๅค้กน', + 'settings.warnings': '{count} ไธช่ญฆๅ‘Š', + + 'header.discord.title': 'ๅŠ ๅ…ฅ Discord', + 'header.jobs.title': 'ไปปๅŠก', + 'header.rules.title': '่ง„ๅˆ™', + 'header.pins.title': 'ๅทฒ็ฝฎ้กถๆถˆๆฏ', + 'header.settings.title': '่ฎพ็ฝฎ', + 'header.help.title': 'ๅธฎๅŠฉ', + 'header.support.title': 'ๆ”ฏๆŒๅผ€ๅ‘', + 'header.support': ' ๆ”ฏๆŒๅผ€ๅ‘', + 'header.support.short': ' ๆ”ฏๆŒ', + + 'common.send': 'ๅ‘้€', + 'common.cancel': 'ๅ–ๆถˆ', + 'common.create': 'ๅˆ›ๅปบ', + 'common.save': 'ไฟๅญ˜', + 'common.saved': 'ๅทฒไฟๅญ˜', + 'common.delete': 'ๅˆ ้™ค', + 'common.delete?': 'ๅˆ ้™ค๏ผŸ', + 'common.confirm': '็กฎ่ฎค', + 'common.rename': '้‡ๅ‘ฝๅ', + 'common.dismiss': 'ๅฟฝ็•ฅ', + 'common.back': '่ฟ”ๅ›ž', + 'common.none': 'ๆ— ', + 'common.reset': '้‡็ฝฎ', + 'common.active': '่ฟ›่กŒไธญ', + 'common.closed': 'ๅทฒๅ…ณ้—ญ', + 'common.todo': 'ๅพ…ๅŠž', + 'common.archive': 'ๅฝ’ๆกฃ', + 'common.drafts': '่‰็จฟ', + 'common.open': 'ๆ‰“ๅผ€', + 'common.yes': 'ๆ˜ฏ', + 'common.no': 'ๅฆ', + 'common.error': '้”™่ฏฏ๏ผš{message}', + + 'pins.title': 'ๅทฒ็ฝฎ้กถ', + 'pins.empty': 'ๆฒกๆœ‰ๅทฒ็ฝฎ้กถๆถˆๆฏ', + 'pins.remove': 'ไปŽๅพ…ๅŠžไธญ็งป้™ค', + 'pins.pin': '็ฝฎ้กถ', + 'pins.done?': 'ๅฎŒๆˆ๏ผŸ', + 'pins.unpin': 'ๅ–ๆถˆ็ฝฎ้กถ', + + 'channel.add.title': 'ๆทปๅŠ ้ข‘้“', + 'channel.rename.title': '้‡ๅ‘ฝๅ', + 'channel.delete.title': 'ๅˆ ้™ค', + 'channel.deleteConfirm': 'ๅˆ ้™ค #{name}๏ผŸ', + + 'timeline.loading': 'ๆญฃๅœจๅŠ ่ฝฝๆถˆๆฏ...', + 'timeline.thinking': 'ๆญฃๅœจๆ€่€ƒ...', + 'timeline.joined': 'ๅŠ ๅ…ฅไบ†', + 'timeline.left': '็ฆปๅผ€ไบ†', + 'timeline.summary': 'ๆ‘˜่ฆ', + 'timeline.today': 'ไปŠๅคฉ', + 'timeline.yesterday': 'ๆ˜จๅคฉ', + 'timeline.reply': 'ๅ›žๅค', + 'timeline.deleteShort': 'ๅˆ ', + 'timeline.copy': 'ๅคๅˆถๆถˆๆฏ', + 'timeline.copyButton': 'ๅคๅˆถ', + 'timeline.copied': 'ๅทฒๅคๅˆถ', + 'timeline.failed': 'ๅคฑ่ดฅ', + 'timeline.openPath': 'ๅœจๆ–‡ไปถ็ฎก็†ๅ™จไธญๆ‰“ๅผ€', + 'timeline.convertJob': '่ฝฌไธบไปปๅŠก', + 'timeline.setRole': '่ฎพ็ฝฎ่ง’่‰ฒ', + 'timeline.chooseRole': '้€‰ๆ‹ฉ่ง’่‰ฒ', + 'timeline.youChose': 'ไฝ ้€‰ๆ‹ฉไบ†๏ผš{choice}', + 'timeline.replyingTo': 'ๆญฃๅœจๅ›žๅค', + 'timeline.selected': 'ๅทฒ้€‰ๆ‹ฉ {count} ๆก', + 'timeline.selectMessages': '้€‰ๆ‹ฉๆถˆๆฏ', + 'timeline.deleteCount': 'ๅˆ ้™ค ({count})', + 'timeline.clearChat': 'ๆธ…็ฉบ่Šๅคฉ', + 'timeline.clearChat.title': 'ๆธ…็ฉบๅฝ“ๅ‰้ข‘้“่Šๅคฉ', + 'timeline.clearChatConfirm': '็กฎ่ฎคๆธ…็ฉบ๏ผŸ', + 'timeline.confirmClearChat.title': '็กฎ่ฎคๆธ…็ฉบ่Šๅคฉ', + + 'composer.startSession.title': 'ๅฏๅŠจไผš่ฏ', + 'composer.placeholder': '่พ“ๅ…ฅๆถˆๆฏ...๏ผˆ็”จ @name ๆๅŠไปฃ็†๏ผ‰', + 'composer.voice.title': '่ฏญ้Ÿณ่พ“ๅ…ฅ๏ผˆๆœฌๅœฐ่ฏญ้Ÿณ่ฝฌๆ–‡ๅญ—๏ผ‰', + 'composer.voice.label': '่ฏญ้Ÿณ่พ“ๅ…ฅ', + 'composer.schedule.title': 'ๅฎšๆ—ถๆถˆๆฏ', + 'composer.hint': 'Enter ๅ‘้€ ยท Shift+Enter ๆข่กŒ ยท ็ฒ˜่ดด/ๆ‹–ๆ”พๅ›พ็‰‡ ยท ้บฆๅ…‹้ฃŽ่ฏญ้Ÿณ่พ“ๅ…ฅ ยท ่พ“ๅ…ฅ / ๆˆ– @ ่‡ชๅŠจ่กฅๅ…จ', + 'composer.dropImage': 'ๆŠŠๅ›พ็‰‡ๆ‹–ๅˆฐ่ฟ™้‡Œ', + 'composer.speechUnsupported': 'ๅฝ“ๅ‰ๆต่งˆๅ™จไธๆ”ฏๆŒ่ฏญ้Ÿณ่ฏ†ๅˆซ๏ผŒ่ฏทไฝฟ็”จ Chrome ๆˆ– Edgeใ€‚', + 'composer.micBlocked': '้บฆๅ…‹้ฃŽๆƒ้™่ขซ้˜ปๆญขใ€‚่ฏทๅœจ Chrome ไธญๅ…่ฎธ้บฆๅ…‹้ฃŽๆƒ้™ๅŽ้‡่ฏ•ใ€‚', + + 'sound.default': '้ป˜่ฎคๆ็คบ้Ÿณ', + 'sound.background': 'ๅŽๅฐๆ้†’', + 'sound.useDefault': 'ไฝฟ็”จ้ป˜่ฎค', + 'sound.softChime': 'ๆŸ”ๅ’Œ้“ƒๅฃฐ', + 'sound.brightPing': 'ๆ˜Žไบฎๆ็คบ', + 'sound.gentlePop': '่ฝปๆŸ”ๅผน้Ÿณ', + 'sound.alertTone': '่ญฆ็คบ้Ÿณ', + 'sound.pluck': 'ๆ‹จๅผฆ', + 'sound.click': '็‚นๅ‡ปๅฃฐ', + 'sound.warmBell': 'ๆธฉๆš–้“ƒๅฃฐ', + 'sound.none': 'ๆ— ', + + 'update.upstream': 'ไธŠๆธธๆœ‰ๅฏ็”จๆ›ดๆ–ฐ', + 'update.available': 'ๆœ‰ๅฏ็”จๆ›ดๆ–ฐ', + 'update.dismiss': 'ๅฟฝ็•ฅ', + + 'agent.nameThis': '็ป™่ฟ™ไธชไปฃ็†ๅ‘ฝๅ', + 'agent.newInstance': 'ๆ–ฐ็š„ {family} ๅฎžไพ‹ๅทฒ่ฟžๆŽฅ', + 'agent.rename': '้‡ๅ‘ฝๅไปฃ็†', + 'agent.currentId': 'ๅฝ“ๅ‰ ID๏ผš@{name}', + 'agent.role': '่ง’่‰ฒ', + 'agent.customRole.placeholder': '่‡ชๅฎšไน‰่ง’่‰ฒ...', + 'agent.color': '้ขœ่‰ฒ', + 'agent.pickColor.title': '้€‰ๆ‹ฉ่‡ชๅฎšไน‰้ขœ่‰ฒ', + 'agent.resetColor.title': 'ๆขๅค้ป˜่ฎค้ขœ่‰ฒ', + 'agent.custom.placeholder': '่‡ชๅฎšไน‰...', + + 'role.planner': '่ง„ๅˆ’่€…', + 'role.designer': '่ฎพ่ฎกๅธˆ', + 'role.architect': 'ๆžถๆž„ๅธˆ', + 'role.builder': 'ๆž„ๅปบ่€…', + 'role.reviewer': 'ๅฎกๆŸฅ่€…', + 'role.researcher': '็ ”็ฉถๅ‘˜', + 'role.redTeam': '็บข้˜Ÿ', + 'role.wry': 'ๅๆงฝๆ‹…ๅฝ“', + 'role.unhinged': 'ๆ”พ้ฃžๅž‹', + 'role.hype': 'ๆฐ”ๆฐ›็ป„', + + 'jobs.title': 'ไปปๅŠก', + 'jobs.create.title': 'ๅˆ›ๅปบไปปๅŠก', + 'jobs.openJob.title': 'ๆ‰“ๅผ€ไปปๅŠก', + 'jobs.newJob': 'ๆ–ฐไปปๅŠก๏ผš', + 'jobs.proposal': 'ไปปๅŠกๆๆกˆ', + 'jobs.accept': 'ๆŽฅๅ—', + 'jobs.requestChanges': '่ฏทๆฑ‚ไฟฎๆ”น', + 'jobs.dismissed': 'ๅทฒๅฟฝ็•ฅ', + 'jobs.started': 'ๅทฒๅˆ›ๅปบ {count} ไธชไปปๅŠก', + 'jobs.none': 'ๆ— ', + 'jobs.unread.title': 'ๆœช่ฏปๆถˆๆฏ', + 'jobs.empty.title': '่ฟ˜ๆฒกๆœ‰ไปปๅŠก', + 'jobs.empty.meta': 'ๅˆ›ๅปบไธ€ไธชไปปๅŠก๏ผŒๆˆ–ๆŠŠ่Šๅคฉๆถˆๆฏ่ฝฌๆˆไปปๅŠก', + 'jobs.dropDeleteOne': 'ๆพๆ‰‹ๅˆ ้™คไปปๅŠก', + 'jobs.dropDeleteMany': 'ๆพๆ‰‹ๅˆ ้™ค {count} ไธชไปปๅŠก', + 'jobs.assign.placeholder': 'ๅ‘้€ไปฅๅˆ†้… ยท ็‚นๅ‡ป็ผ–่พ‘', + 'jobs.message.placeholder': 'ๆถˆๆฏ...', + 'jobs.emptyMessages': '่ฟ˜ๆฒกๆœ‰ๆถˆๆฏใ€‚ๅผ€ๅง‹ๅฏน่ฏๅง๏ผ', + 'jobs.loadFailed': 'ๅŠ ่ฝฝๆถˆๆฏๅคฑ่ดฅใ€‚', + 'jobs.deleteShort': 'ๅˆ ', + 'jobs.title.placeholder': 'ไปปๅŠกๆ ‡้ข˜', + 'jobs.body.placeholder': 'ๆ่ฟฐ๏ผˆๅฏ้€‰๏ผ‰', + 'jobs.accepted': 'ๅทฒๆŽฅๅ—', + 'jobs.deleteSelected': 'ๅˆ ้™คๅทฒ้€‰ๆ‹ฉ็š„ {count} ้กน', + 'jobs.dragDelete': 'ๆ‹–ๅˆฐ่ฟ™้‡Œๅˆ ้™ค', + 'jobs.convertTitle': '่ฝฌไธบไปปๅŠก', + 'jobs.convertSubtitle': 'ไปฃ็†ไผšๅ†™ไธ€ไธชไปปๅŠกๆๆกˆ๏ผŒไพ›ไฝ ๆŽฅๅ—', + 'jobs.convertLabel': '่ฎฉไปฃ็†็ผ–ๅ†™ๆๆกˆ', + 'jobs.convert': '่ฝฌๆข', + 'jobs.askingAgent': 'ๆญฃๅœจ่ฏทๆฑ‚ @{agent} ๅˆ›ๅปบไปปๅŠกๅก็‰‡...', + 'jobs.deletePermanentTitle': 'ๆฐธไน…ๅˆ ้™คไปปๅŠก๏ผŸ', + 'jobs.deletePermanentSubtitle': '่ฟ™ไผšๆฐธไน…ๅˆ ้™ค่ฏฅไปปๅŠกๅŠๅ…ถๆถˆๆฏ๏ผŒๆ— ๆณ•ๆ’ค้”€ใ€‚', + 'jobs.replyTarget.title': 'ๅ›žๅคๅฏน่ฑก', + 'jobs.clearReplyTarget.title': 'ๆธ…้™คๅ›žๅคๅฏน่ฑก', + 'jobs.replyTargetPrefix': 'ๅ‘็ป™', + + 'rules.title': '่ง„ๅˆ™', + 'rules.proposal': '่ง„ๅˆ™ๆๆกˆ', + 'rules.activate': 'ๅฏ็”จ', + 'rules.addDrafts': 'ๅŠ ๅ…ฅ่‰็จฟ', + 'rules.activated': 'ๅทฒๅฏ็”จ', + 'rules.addedDrafts': 'ๅทฒๅŠ ๅ…ฅ่‰็จฟ', + 'rules.add.title': 'ๆทปๅŠ ่ง„ๅˆ™', + 'rules.remind': 'ๆ้†’ไปฃ็†', + 'rules.queued': 'ๅทฒๆŽ’้˜Ÿ๏ผŒไธ‹ๆฌก่งฆๅ‘ๆ—ถๅ‘้€', + 'rules.empty.title': '่ฟ˜ๆฒกๆœ‰่ง„ๅˆ™', + 'rules.empty.meta': 'ๅ‘Š่ฏ‰ไปฃ็†ๅบ”่ฏฅๆ€Žไนˆๅทฅไฝœ', + 'rules.nextTrigger': 'ๆ–ฐ่ง„ๅˆ™ไผšๅœจไธ‹ไธ€ๆฌกไปฃ็†่งฆๅ‘ๆ—ถๅ‘้€', + 'rules.softWarning': 'ๅฏ็”จ่ง„ๅˆ™ๅฐ‘ไบŽ 7 ๆก้€šๅธธๆ•ˆๆžœๆ›ดๅฅฝ', + 'rules.dragDelete': 'ๆ‹–ๅˆฐ่ฟ™้‡Œๅˆ ้™ค', + 'rules.create.placeholder': 'ๅ†™ไธ€ๆกไปฃ็†ๅบ”่ฏฅ้ตๅฎˆ็š„็ฎ€็Ÿญ่ง„ๅˆ™', + 'rules.deleteConfirm': 'ๅˆ ้™ค๏ผŸ', + 'rules.rejectedPrefix': '่ง„ๅˆ™ๅทฒๆ‹’็ป', + + 'sessions.goToChannel': 'ๅ‰ๅพ€้ข‘้“', + 'sessions.end': '็ป“ๆŸไผš่ฏ', + 'sessions.endConfirm': '็ป“ๆŸไผš่ฏ๏ผŸ', + 'sessions.confirmEnd.title': '็กฎ่ฎค็ป“ๆŸไผš่ฏ', + 'sessions.activeIn': 'ไผš่ฏๆญฃๅœจ #{channel} ไธญ่ฟ›่กŒ', + 'sessions.activeElsewhere': 'ๅ…ถไป–้ข‘้“ๆœ‰ {count} ไธชไผš่ฏๆญฃๅœจ่ฟ›่กŒ', + 'sessions.session': 'ไผš่ฏ', + 'sessions.customSession': '่‡ชๅฎšไน‰ไผš่ฏ', + 'sessions.waitingFor': 'ๆญฃๅœจ็ญ‰ๅพ… {agent}', + 'sessions.paused': 'ๅทฒๆš‚ๅœ', + 'sessions.viewOutput': 'ๆŸฅ็œ‹่พ“ๅ‡บ', + 'sessions.proposal': 'ไผš่ฏๆๆกˆ', + 'sessions.superseded': 'ๅทฒ่ขซๆ›ฟๆข็š„่‰็จฟ', + 'sessions.replacedByRevision': 'ๅทฒ็”ฑไฟฎ่ฎข็‰ˆ {revision} ๆ›ฟๆขใ€‚', + 'sessions.invalidDraft': 'ๆ— ๆ•ˆ็š„ไผš่ฏ่‰็จฟ', + 'sessions.invalidDraftHelp': '่ฟ™ไธช่‰็จฟ้œ€่ฆไฟฎๆ”นๅŽๆ‰่ƒฝ่ฟ่กŒๆˆ–ไฟๅญ˜ใ€‚', + 'sessions.requestChanges': '่ฏทๆฑ‚ไฟฎๆ”น', + 'sessions.run': '่ฟ่กŒ', + 'sessions.saveTemplate': 'ไฟๅญ˜ๆจกๆฟ', + 'sessions.deleteTemplate.title': 'ๅˆ ้™ค่‡ชๅฎšไน‰ๆจกๆฟ', + 'sessions.cast': 'ๆˆๅ‘˜ๅˆ†้…', + 'sessions.phaseNumber': '้˜ถๆฎต {number}', + 'sessions.goToChannelNamed': 'ๅ‰ๅพ€ #{channel}', + 'sessions.goToChannelExtra': 'ๅ‰ๅพ€ #{channel}๏ผˆ+{count}๏ผ‰', + 'sessions.template.codeReview.name': 'ไปฃ็ ๅฎกๆŸฅ', + 'sessions.template.codeReview.description': '็”ฑๆž„ๅปบ่€…ใ€ๅฎกๆŸฅ่€…ๅ’Œ็บข้˜Ÿ่ฟ›่กŒ็ป“ๆž„ๅŒ–ไปฃ็ ๅฎกๆŸฅใ€‚', + 'sessions.template.debate.name': '่พฉ่ฎบ', + 'sessions.template.debate.description': 'ไธคไธชไปฃ็†ๅˆ†ๅˆซๆ”ฏๆŒ็›ธๅ็ซ‹ๅœบ๏ผŒ็ฌฌไธ‰ไธชไปฃ็†ไธปๆŒๅนถ็ป™ๅ‡บ็ป“่ฎบใ€‚', + 'sessions.template.designCritique.name': '่ฎพ่ฎก่ฏ„ๅฎก', + 'sessions.template.designCritique.description': 'ไธ€ไธชไปฃ็†ๅฑ•็คบๆ–นๆกˆ๏ผŒไธ€ไธชไปฃ็†ๆๅ‡บ่ฏ„ๅฎกๆ„่ง๏ผŒๅฆไธ€ไธชไปฃ็†ๆฑ‡ๆ€ป็ป“่ฎบใ€‚', + 'sessions.template.planning.name': '่ง„ๅˆ’', + 'sessions.template.planning.description': '้€š่ฟ‡็•Œๅฎš้—ฎ้ข˜ใ€ๆŒ‘ๆˆ˜ๅ‡่ฎพๅ’Œ็ปผๅˆ็ป“่ฎบ๏ผŒๆŠŠ้—ฎ้ข˜่ฝฌๆˆๆ‰ง่กŒ่ฎกๅˆ’ใ€‚', + 'sessions.role.builder': 'ๆž„ๅปบ่€…', + 'sessions.role.reviewer': 'ๅฎกๆŸฅ่€…', + 'sessions.role.red_team': '็บข้˜Ÿ', + 'sessions.role.synthesiser': 'ๆ€ป็ป“่€…', + 'sessions.role.proposer': 'ๆ่ฎฎ่€…', + 'sessions.role.for': 'ๆญฃๆ–น', + 'sessions.role.against': 'ๅๆ–น', + 'sessions.role.moderator': 'ไธปๆŒไบบ', + 'sessions.role.presenter': 'ๅฑ•็คบ่€…', + 'sessions.role.critic': '่ฏ„ๅฎก่€…', + 'sessions.role.planner': '่ง„ๅˆ’่€…', + 'sessions.role.challenger': 'ๆŒ‘ๆˆ˜่€…', + 'sessions.phase.submit': 'ๆไบค', + 'sessions.phase.review': 'ๅฎกๆŸฅ', + 'sessions.phase.respond': 'ๅ›žๅบ”', + 'sessions.phase.summary': 'ๆ€ป็ป“', + 'sessions.phase.frame': '็•Œๅฎš้—ฎ้ข˜', + 'sessions.phase.opening_arguments': 'ๅผ€ๅœบ้™ˆ่ฟฐ', + 'sessions.phase.rebuttals': 'ๅ้ฉณ', + 'sessions.phase.verdict': '็ป“่ฎบ', + 'sessions.phase.present': 'ๅฑ•็คบ', + 'sessions.phase.critique': '่ฏ„ๅฎก', + 'sessions.phase.synthesis': '็ปผๅˆ', + 'sessions.phase.challenge': 'ๆŒ‘ๆˆ˜', + 'sessions.phase.plan': '่ฎกๅˆ’', + 'sessions.design.placeholder': 'ๆ่ฟฐไฝ ๆƒณ่ฆ็š„ไผš่ฏ...', + 'sessions.goal.placeholder': '็›ฎๆ ‡๏ผˆๅฏ้€‰๏ผ‰-- ่ฟ™ไธชไผš่ฏๅบ”่ฏฅ่พพๆˆไป€ไนˆ๏ผŸ', + 'sessions.goalShort.placeholder': '็›ฎๆ ‡๏ผˆๅฏ้€‰๏ผ‰', + 'sessions.changes.placeholder': 'ไฝ ๅธŒๆœ›ไฟฎๆ”นไป€ไนˆ๏ผŸ', + 'sessions.sendDesignFailed': 'ๅ‘้€่ฎพ่ฎก่ฏทๆฑ‚ๅคฑ่ดฅ๏ผˆHTTP {status}๏ผ‰', + 'sessions.startFailed': 'ๅฏๅŠจไผš่ฏๅคฑ่ดฅ', + 'sessions.startError': 'ๅฏๅŠจไผš่ฏๅ‡บ้”™๏ผš{message}', + 'sessions.endFailed': '็ป“ๆŸไผš่ฏๅคฑ่ดฅ', + 'sessions.endError': '็ป“ๆŸไผš่ฏๅ‡บ้”™๏ผš{message}', + 'sessions.startDraftFailed': 'ไปŽ่‰็จฟๅฏๅŠจไผš่ฏๅคฑ่ดฅ', + 'sessions.saveFailed': 'ไฟๅญ˜ๆจกๆฟๅคฑ่ดฅ', + 'sessions.deleteFailed': 'ๅˆ ้™คๆจกๆฟๅคฑ่ดฅ', + 'sessions.connectionLost': '่ฟžๆŽฅๅทฒๆ–ญๅผ€ใ€‚่ฏท้‡ๆ–ฐ่ฟžๆŽฅๅŽ้‡่ฏ•ใ€‚', + 'sessions.dismissFailed': 'ๅฟฝ็•ฅไผš่ฏๆๆกˆๅคฑ่ดฅ', + + 'schedule.seeAll': 'ๆŸฅ็œ‹ๅ…จ้ƒจ', + 'schedule.hide': 'ๆ”ถ่ตท', + 'schedule.message': 'ๅฎšๆ—ถๆถˆๆฏ', + 'schedule.when': 'ๆ—ฅๆœŸ', + 'schedule.at': 'ๆ—ถ้—ด', + 'schedule.recurring': '้‡ๅค', + 'schedule.every': 'ๆฏ', + 'schedule.minutes': 'ๅˆ†้’Ÿ', + 'schedule.hours': 'ๅฐๆ—ถ', + 'schedule.days': 'ๅคฉ', + 'schedule.submit': 'ๅฎšๆ—ถๅ‘้€', + 'schedule.today': 'ไปŠๅคฉ', + 'schedule.tomorrow': 'ๆ˜Žๅคฉ', + 'schedule.sunday': 'ๆ˜ŸๆœŸๆ—ฅ', + 'schedule.monday': 'ๆ˜ŸๆœŸไธ€', + 'schedule.tuesday': 'ๆ˜ŸๆœŸไบŒ', + 'schedule.wednesday': 'ๆ˜ŸๆœŸไธ‰', + 'schedule.thursday': 'ๆ˜ŸๆœŸๅ››', + 'schedule.friday': 'ๆ˜ŸๆœŸไบ”', + 'schedule.saturday': 'ๆ˜ŸๆœŸๅ…ญ', + 'schedule.nextIn': '{time} ๅŽๆ‰ง่กŒ', + 'schedule.paused': '{count} ไธชๅฎšๆ—ถๅทฒๆš‚ๅœ', + 'schedule.activeCount': '{count} ไธชๅฏ็”จ', + 'schedule.pausedCount': '{count} ไธชๆš‚ๅœ', + 'schedule.pausedSuffix': '๏ผˆๅทฒๆš‚ๅœ๏ผ‰', + 'schedule.dailyAt': 'ๆฏๅคฉ {time}', + 'schedule.everyShort': 'ๆฏ {value}{unit}', + 'schedule.pause': 'ๆš‚ๅœ', + 'schedule.resume': '็ปง็ปญ', + 'schedule.failed': 'ๅฎšๆ—ถๅคฑ่ดฅ', + 'schedule.createFailed': 'ๅˆ›ๅปบๅฎšๆ—ถๅคฑ่ดฅ', + 'schedule.targetError': 'ๅ…ˆๆ‰“ๅผ€ไธ€ไธชไปฃ็†ๅผ€ๅ…ณไฝœไธบ็›ฎๆ ‡', + 'schedule.messageError': '่ฏทๅ…ˆ่พ“ๅ…ฅๆถˆๆฏ', + + 'slash.artchallenge': 'SVG ่‰บๆœฏๆŒ‘ๆˆ˜๏ผšๆ‰€ๆœ‰ไปฃ็†ๅˆ›ไฝœไฝœๅ“๏ผˆๅฏ้€‰ไธป้ข˜๏ผ‰', + 'slash.hatmaking': 'ๆ‰€ๆœ‰ไปฃ็†ไธบ่‡ชๅทฑ็š„ๅคดๅƒ่ฎพ่ฎกไธ€้กถๅธฝๅญ', + 'slash.roastreview': '่ฎฉๆ‰€ๆœ‰ไปฃ็†ไบ’็›ธๅฎกๆŸฅๅนถ็Š€ๅˆฉ็‚น่ฏ„ๅทฅไฝœ', + 'slash.haiku': '่ฎฉไปฃ็†ๅ†™ไธ€้ฆ–ๅ…ณไบŽไปฃ็ ๅบ“็š„ไฟณๅฅ', + 'slash.limerick': '่ฎฉไปฃ็†ๅ†™ไธ€้ฆ–ๅ…ณไบŽไปฃ็ ๅบ“็š„ไบ”่กŒๆ‰“ๆฒน่ฏ—', + 'slash.sonnet': '่ฎฉไปฃ็†ๅ†™ไธ€้ฆ–ๅ…ณไบŽไปฃ็ ๅบ“็š„ๅๅ››่กŒ่ฏ—', + 'slash.summary': 'ๆ€ป็ป“ๆœ€่ฟ‘ๆถˆๆฏ๏ผš่ฏทๆๅŠไธ€ไธชไปฃ็†๏ผˆไพ‹ๅฆ‚ /summary @claude๏ผ‰', + 'slash.summarise': 'ๆ€ป็ป“ๆœ€่ฟ‘ๆถˆๆฏ๏ผš่ฏทๆๅŠไธ€ไธชไปฃ็†๏ผˆไพ‹ๅฆ‚ /summarise @claude๏ผ‰', + 'slash.continue': 'ๅพช็ŽฏไฟๆŠคๆš‚ๅœๅŽ็ปง็ปญ', + 'slash.clear': 'ๆธ…็ฉบๅฝ“ๅ‰้ข‘้“ๆถˆๆฏ', + + 'help.guide': 'ๆŒ‡ๅ—', + 'help.dismissHint': 'ๆŒ‰ Esc ๆˆ–็‚นๅ‡ปๅค–้ƒจๅ…ณ้—ญ', + 'help.agents.title': 'ไปฃ็†', + 'help.agents.loc': '้กถ้ƒจ็Šถๆ€่ƒถๅ›Š', + 'help.agents.desc': '้กถ้ƒจ็Šถๆ€่ƒถๅ›Šๆ˜พ็คบ่ฐๅœจ็บฟ๏ผŒไปฅๅŠไป–ไปฌๆญฃๅœจๅšไป€ไนˆใ€‚', + 'help.agents.online': 'ๅœจ็บฟ', + 'help.agents.working': 'ๅทฅไฝœไธญ๏ผš่พนๆก†ไผšๆ—‹่ฝฌ', + 'help.agents.offline': '็ฆป็บฟ', + 'help.agents.tip': '็‚นๅ‡ปไปปๆ„่ƒถๅ›Šๅฏไปฅ้‡ๅ‘ฝๅใ€ๅˆ†้…่ง’่‰ฒๆˆ–ไฟฎๆ”น้ขœ่‰ฒใ€‚', + 'help.jobs.title': 'ไปปๅŠก', + 'help.jobs.loc': 'ๆ‚ฌๅœๆถˆๆฏ + ไพง่พนๆ ', + 'help.jobs.desc': 'ไปปๅŠกๅฏไปฅๆŠŠ่Šๅคฉๅ†…ๅฎนๅ˜ๆˆๅฏ่ฟฝ่ธช็š„ๅทฅไฝœใ€‚ไปปไฝ•ๆถˆๆฏ้ƒฝๅฏไปฅ่ฝฌๆˆไปปๅŠกใ€‚', + 'help.jobs.sample': '่ฎค่ฏๆจกๅ—้œ€่ฆๅ…ˆ้‡ๆž„๏ผŒๆ‰่ƒฝๅŠ ๅ…ฅ SSO ๆ”ฏๆŒใ€‚', + 'help.jobs.tip1': 'ๆ‚ฌๅœไปปๆ„ๆถˆๆฏๅฏ็œ‹ๅˆฐโ€œ่ฝฌไธบไปปๅŠกโ€๏ผŒๅนถๆ‰“ๅผ€ไปปๅŠกไพง่พนๆ ใ€‚', + 'help.jobs.sidebar': 'ไปปๅŠกไพง่พนๆ ', + 'help.jobs.jobTitle': '่ฎค่ฏ้‡ๆž„', + 'help.jobs.tip2': 'ไปปๅŠกไฝไบŽไพง่พนๆ ใ€‚ๆฏไธชไปปๅŠก้ƒฝๆœ‰่‡ชๅทฑ็š„็บฟ็จ‹๏ผŒๅนถๅฏไปŽๅพ…ๅŠž่ฟฝ่ธชๅˆฐ่ฟ›่กŒไธญใ€ๅทฒๅ…ณ้—ญใ€‚', + 'help.rules.title': '่ง„ๅˆ™', + 'help.rules.loc': 'ๅณไธŠ่ง’้ขๆฟ', + 'help.rules.desc': '่ง„ๅˆ™ๅ‘Š่ฏ‰ไปฃ็†ๅœจ่ฟ™ไธชๆˆฟ้—ด้‡Œๅบ”่ฏฅๅฆ‚ไฝ•ๅทฅไฝœใ€‚ๅฏ็”จ็š„่ง„ๅˆ™ไผš่‡ชๅŠจ็”Ÿๆ•ˆใ€‚', + 'help.rules.sampleDraft': 'ๆไบคๅ‰่ฟ่กŒๆต‹่ฏ•', + 'help.rules.sampleActive': 'ๆ”นๅŠจๅ‰ๆ€ปๆ˜ฏๅ…ˆ่ฏดๆ˜Žไฝ ็š„ๆŽจ็†', + 'help.rules.draft': '่‰็จฟ', + 'help.rules.tip': 'ๅฏ็”จ = ไปฃ็†ไผš้ตๅฎˆใ€‚่‰็จฟ = ๆš‚ๆœชๅฏ็”จใ€‚ไฝ ๅ’Œไปฃ็†้ƒฝๅฏไปฅๆทปๅŠ ่ง„ๅˆ™ใ€‚', + 'help.channels.title': '้ข‘้“', + 'help.channels.loc': '้กถ้ƒจๆ ', + 'help.channels.desc': 'ๆŒ‰ไธป้ข˜ๆ‹†ๅˆ†ๅฏน่ฏใ€‚ๆฏไธช้ข‘้“้ƒฝๆœ‰่‡ชๅทฑ็š„ๆถˆๆฏๅކๅฒใ€‚', + 'help.channels.tip': '็‚นๅ‡ป + ๅˆ›ๅปบๆ–ฐ้ข‘้“ใ€‚ไปปไฝ•้ข‘้“้‡Œ้ƒฝๅฏไปฅๆๅŠไปฃ็†ใ€‚', + 'help.mentions.title': 'ๆๅŠ', + 'help.mentions.loc': '่พ“ๅ…ฅๆก†ไธŠๆ–น่ƒถๅ›Š', + 'help.mentions.desc': 'ๅœจ่พ“ๅ…ฅๆก†้‡Œ่พ“ๅ…ฅ @ ๅฏไปฅๆๅŠไปฃ็†ใ€‚่พ“ๅ…ฅๆก†ไธŠๆ–น็š„่ƒถๅ›Šๅฏ้ข„ๅ…ˆ้€‰ๆ‹ฉ่ฆๅ‘้€็ป™่ฐ๏ผŒๅ‘้€ๆ—ถไผš่‡ชๅŠจๆๅŠใ€‚', + 'help.mentions.tip': '่ขซๆๅŠ็š„ไปฃ็†ไผšๆ”ถๅˆฐ่งฆๅ‘๏ผŒๅนถๅœจ้ข‘้“ไธญๅ›žๅคใ€‚', + 'help.sessions.title': 'ไผš่ฏ', + 'help.sessions.loc': 'ๆ’ญๆ”พๆŒ‰้’ฎ', + 'help.sessions.desc': 'ไผš่ฏๆ˜ฏๅธฆ้˜ถๆฎตๅ’Œ่ง’่‰ฒ็š„ๅคšไปฃ็†็ป“ๆž„ๅŒ–ๅทฅไฝœๆตใ€‚ๅฏๅŠจไธ€ไธชไผš่ฏๅณๅฏ่ฎฉไปฃ็†ๅ›ด็ป•ๅ…ฑๅŒ็›ฎๆ ‡ๅไฝœใ€‚', + 'help.sessions.brainstorm': 'ๅคด่„‘้ฃŽๆšด', + 'help.sessions.brainstormDesc': 'ๆ‰€ๆœ‰ไปฃ็†ๅ‚ไธŽ็š„่‡ช็”ฑๅ‘ๆ•ฃ', + 'help.sessions.codeReview': 'ไปฃ็ ๅฎกๆŸฅ', + 'help.sessions.codeReviewDesc': 'ๅธฆ้˜ถๆฎตๅ’Œ่ง’่‰ฒ็š„็ป“ๆž„ๅŒ–ๅฎกๆŸฅ', + 'help.sessions.designReview': '่ฎพ่ฎกๅฎกๆŸฅ', + 'help.sessions.designReviewDesc': '่ฏ„ๅฎกๅนถ่ฟญไปฃ่ฎพ่ฎกๅ†ณ็ญ–', + 'help.sessions.custom': '่‡ชๅฎšไน‰ไผš่ฏ', + 'help.sessions.customDesc': 'ๆ่ฟฐไธ€ไธช็›ฎๆ ‡๏ผŒไปฃ็†ไผš่‡ช่กŒ็ป„็ป‡ๆˆ็ป“ๆž„ๅŒ–ๅทฅไฝœๆตใ€‚', + 'help.sessions.tip': '็‚นๅ‡ป่พ“ๅ…ฅๆก†ๆ—่พน็š„ๆ’ญๆ”พๆŒ‰้’ฎๅผ€ๅง‹ใ€‚', + 'help.scheduling.title': 'ๅฎšๆ—ถ', + 'help.scheduling.loc': 'ๆ—ถ้’ŸๆŒ‰้’ฎ', + 'help.scheduling.desc': 'ๆŠŠไปปๆ„ๆถˆๆฏๅฎ‰ๆŽ’ๅˆฐ็จๅŽๅ‘้€๏ผŒๆˆ–่ฎพ็ฝฎๆŒ‰ๆ—ถ้—ด้‡ๅค็š„ๆ็คบใ€‚', + 'help.scheduling.sample1': '@codex ๆฃ€ๆŸฅ้ƒจ็ฝฒ็Šถๆ€', + 'help.scheduling.sample2': '@claude ๆ€ป็ป“ๅคœ้—ดๅ˜ๆ›ด', + 'help.scheduling.recur': 'ๆฏ 1 ๅฐๆ—ถ', + 'help.scheduling.tomorrow9': 'ๆ˜ŽๅคฉไธŠๅˆ 9 ็‚น', + 'help.scheduling.tip': '็‚นๅ‡ปๅ‘้€ๆŒ‰้’ฎๆ—่พน็š„ๆ—ถ้’Ÿ๏ผŒๅฏไปฅ่ฎพ็ฝฎไธ€ๆฌกๆ€งๆˆ–้‡ๅคๅฎšๆ—ถๆถˆๆฏใ€‚', + }, + }; + + function normalizeLang(value) { + if (value === 'zh-CN' || value === 'zhCN' || value === 'zh') return 'zh-CN'; + if (value === 'en') return 'en'; + return 'auto'; + } + + function resolveLanguage(pref) { + const normalized = normalizeLang(pref); + if (normalized === 'zh-CN' || normalized === 'en') return normalized; + const nav = (navigator.languages && navigator.languages[0]) || navigator.language || ''; + return /^zh\b/i.test(nav) ? 'zh-CN' : 'en'; + } + + function getLanguagePreference() { + return normalizeLang(localStorage.getItem(STORAGE_KEY) || 'auto'); + } + + function getLanguage() { + return resolveLanguage(getLanguagePreference()); + } + + function interpolate(str, vars) { + if (!vars) return str; + return str.replace(/\{(\w+)\}/g, (m, key) => + Object.prototype.hasOwnProperty.call(vars, key) ? String(vars[key]) : m + ); + } + + function t(key, vars) { + const lang = getLanguage(); + const table = lang === 'zh-CN' ? I18N_STRINGS.zhCN : I18N_STRINGS.en; + const fallback = I18N_STRINGS.en; + return interpolate(table[key] || fallback[key] || key, vars); + } + + function applyTranslations(root) { + const scope = root || document; + document.documentElement.lang = getLanguage(); + + scope.querySelectorAll('[data-i18n]').forEach(el => { + el.textContent = t(el.dataset.i18n); + }); + scope.querySelectorAll('[data-i18n-html]').forEach(el => { + el.innerHTML = t(el.dataset.i18nHtml); + }); + scope.querySelectorAll('[data-i18n-title]').forEach(el => { + el.title = t(el.dataset.i18nTitle); + }); + scope.querySelectorAll('[data-i18n-placeholder]').forEach(el => { + el.placeholder = t(el.dataset.i18nPlaceholder); + }); + scope.querySelectorAll('[data-i18n-aria-label]').forEach(el => { + el.setAttribute('aria-label', t(el.dataset.i18nAriaLabel)); + }); + + const selector = document.getElementById('setting-language'); + if (selector) selector.value = getLanguagePreference(); + } + + function setLanguage(lang) { + const pref = normalizeLang(lang); + localStorage.setItem(STORAGE_KEY, pref); + applyTranslations(document); + document.dispatchEvent(new CustomEvent('i18n:change', { + detail: { language: getLanguage(), preference: pref }, + })); + // Existing messages and sidebar cards are rendered HTML. Reloading keeps + // the switch reliable without introducing a client-side virtual DOM. + setTimeout(() => window.location.reload(), 50); + } + + window.I18N_STRINGS = I18N_STRINGS; + window.t = t; + window.setLanguage = setLanguage; + window.getLanguage = getLanguage; + window.getLanguagePreference = getLanguagePreference; + window.applyTranslations = applyTranslations; + + document.addEventListener('DOMContentLoaded', () => { + applyTranslations(document); + const selector = document.getElementById('setting-language'); + if (selector) selector.addEventListener('change', () => setLanguage(selector.value)); + }); +})(); diff --git a/static/index.html b/static/index.html index 955feb87..a8d9856a 100644 --- a/static/index.html +++ b/static/index.html @@ -4,9 +4,12 @@ agentchattr - - - + + + + @@ -15,7 +18,7 @@

agentchattr

- + Discord @@ -23,33 +26,33 @@

agentchattr

- - - - - @@ -81,9 +99,9 @@

agentchattr

- +
- +
- - + +
- - + + + + +
@@ -121,10 +139,10 @@

agentchattr

- +
- - + +
@@ -132,7 +150,7 @@

agentchattr

@@ -141,11 +159,11 @@

agentchattr

- +
- Support development + Support development
@@ -156,8 +174,8 @@

agentchattr

- - + +
@@ -165,8 +183,8 @@

agentchattr