feat(web): add i18n support with FR/EN locales and keyboard layout awareness
All checks were successful
Beta Release / beta (push) Successful in 36s
All checks were successful
Beta Release / beta (push) Successful in 36s
Add full internationalization system with React context, French/English translations, and AZERTY/QWERTY keyboard layout support. Dashboard now uses a tabbed layout (Tools, Notifications, Workflows). Config page exposes language and keyboard preferences persisted via new /api/preferences endpoint. 💕 Generated with Crush Assisted-by: GLM-5-Turbo via Crush <crush@charm.land>
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { useI18n } from '../i18n'
|
||||
|
||||
export default function Studio({ api }) {
|
||||
const { t, layout } = useI18n()
|
||||
const [messages, setMessages] = useState([
|
||||
{ role: 'ai', content: 'Welcome to Studio! Chat with your AI assistant here.' },
|
||||
{ role: 'ai', content: 'Configure agents and workflows from the sidebar.' },
|
||||
{ role: 'ai', content: t('studio.welcome') },
|
||||
{ role: 'ai', content: t('studio.configureHint') },
|
||||
])
|
||||
const [input, setInput] = useState('')
|
||||
const [sidebarPanel, setSidebarPanel] = useState('chat')
|
||||
@@ -23,10 +25,10 @@ export default function Studio({ api }) {
|
||||
|
||||
api.runCommand(`echo "AI response simulation for: ${text}"`, '')
|
||||
.then(res => {
|
||||
setMessages(prev => [...prev, { role: 'ai', content: res.output || res.error || 'No response' }])
|
||||
setMessages(prev => [...prev, { role: 'ai', content: res.output || res.error || t('studio.noResponse') }])
|
||||
})
|
||||
.catch(err => {
|
||||
setMessages(prev => [...prev, { role: 'ai', content: `Error: ${err.message}` }])
|
||||
setMessages(prev => [...prev, { role: 'ai', content: `${t('studio.error')}: ${err.message}` }])
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
@@ -39,9 +41,9 @@ export default function Studio({ api }) {
|
||||
}
|
||||
|
||||
const sidebarItems = [
|
||||
{ id: 'chat', label: 'Chat', icon: '#' },
|
||||
{ id: 'agents', label: 'Agents', icon: '*' },
|
||||
{ id: 'workflows', label: 'Workflows', icon: '~' },
|
||||
{ id: 'chat', label: t('studio.chat'), icon: '#' },
|
||||
{ id: 'agents', label: t('studio.agents'), icon: '*' },
|
||||
{ id: 'workflows', label: t('studio.workflows'), icon: '~' },
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -49,7 +51,7 @@ export default function Studio({ api }) {
|
||||
<div className="chat-layout" style={{ flex: 1, borderRight: '1px solid var(--border)' }}>
|
||||
<div className="panel-header">
|
||||
<span className="panel-title">
|
||||
Chat
|
||||
{t('studio.chat')}
|
||||
{loading && <span className="spinner" />}
|
||||
</span>
|
||||
</div>
|
||||
@@ -68,11 +70,11 @@ export default function Studio({ api }) {
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type a message... (Enter to send)"
|
||||
placeholder={t('studio.placeholder')}
|
||||
disabled={loading}
|
||||
/>
|
||||
<button className="primary" onClick={handleSend} disabled={loading || !input.trim()}>
|
||||
Send
|
||||
{t('studio.send')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,42 +95,42 @@ export default function Studio({ api }) {
|
||||
|
||||
{sidebarPanel === 'chat' && (
|
||||
<div>
|
||||
<div className="section-title">Commands</div>
|
||||
<div className="section-title">{t('studio.commands')}</div>
|
||||
<div style={{ fontSize: 12, fontFamily: 'var(--font-mono)', color: 'var(--text-tertiary)' }}>
|
||||
/plan <goal><br />
|
||||
/help
|
||||
{t('studio.planGoal')}<br />
|
||||
{t('studio.help')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sidebarPanel === 'agents' && (
|
||||
<div>
|
||||
<div className="section-title">Active Agents</div>
|
||||
<div className="section-title">{t('studio.activeAgents')}</div>
|
||||
<div className="agent-card">
|
||||
<div className="agent-avatar">C</div>
|
||||
<div>
|
||||
<div className="agent-name">Crush</div>
|
||||
<div className="agent-status">Stopped</div>
|
||||
<div className="agent-name">{t('studio.crush')}</div>
|
||||
<div className="agent-status">{t('studio.stopped')}</div>
|
||||
</div>
|
||||
<span className="badge neutral" style={{ marginLeft: 'auto' }}>Inactive</span>
|
||||
<span className="badge neutral" style={{ marginLeft: 'auto' }}>{t('studio.inactive')}</span>
|
||||
</div>
|
||||
<div className="agent-card">
|
||||
<div className="agent-avatar">CC</div>
|
||||
<div>
|
||||
<div className="agent-name">Claude Code</div>
|
||||
<div className="agent-status">Stopped</div>
|
||||
<div className="agent-name">{t('studio.claudeCode')}</div>
|
||||
<div className="agent-status">{t('studio.stopped')}</div>
|
||||
</div>
|
||||
<span className="badge neutral" style={{ marginLeft: 'auto' }}>Inactive</span>
|
||||
<span className="badge neutral" style={{ marginLeft: 'auto' }}>{t('studio.inactive')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sidebarPanel === 'workflows' && (
|
||||
<div>
|
||||
<div className="section-title">Workflows</div>
|
||||
<div className="section-title">{t('studio.workflows')}</div>
|
||||
<div className="empty-state">
|
||||
No active workflow.
|
||||
<span style={{ fontFamily: 'var(--font-mono)' }}>Use /plan <goal> in chat to start.</span>
|
||||
{t('studio.noWorkflow')}
|
||||
<span style={{ fontFamily: 'var(--font-mono)' }}>{t('studio.usePlan')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user