feat(web): add i18n support with FR/EN locales and keyboard layout awareness
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:
Augustin
2026-04-21 21:48:36 +02:00
parent 3dc24ae22c
commit 11417d3ea7
15 changed files with 713 additions and 186 deletions

View File

@@ -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 &lt;goal&gt;<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 &lt;goal&gt; in chat to start.</span>
{t('studio.noWorkflow')}
<span style={{ fontFamily: 'var(--font-mono)' }}>{t('studio.usePlan')}</span>
</div>
</div>
)}