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,24 +1,26 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
import api from '../api/client'
|
||||
import { getTheme, getThemeNames, applyTheme } from '../themes'
|
||||
import { useI18n } from '../i18n'
|
||||
import Dashboard from './Dashboard'
|
||||
import Studio from './Studio'
|
||||
import Shell from './Shell'
|
||||
import Config from './Config'
|
||||
|
||||
const TABS = [
|
||||
{ id: 'dash', label: 'Dashboard', icon: '\u25A0' },
|
||||
{ id: 'studio', label: 'Studio', icon: '\u27E8\u27E9' },
|
||||
{ id: 'shell', label: 'Shell', icon: '$' },
|
||||
{ id: 'config', label: 'Config', icon: '\u2699' },
|
||||
]
|
||||
|
||||
export default function App() {
|
||||
const [activeTab, setActiveTab] = useState('dash')
|
||||
const [info, setInfo] = useState({})
|
||||
const [clock, setClock] = useState(new Date())
|
||||
const [updates, setUpdates] = useState([])
|
||||
const [tools, setTools] = useState([])
|
||||
const { t, layout } = useI18n()
|
||||
|
||||
const TABS = useMemo(() => [
|
||||
{ id: 'dash', label: t('tabs.dashboard'), icon: '\u25A0' },
|
||||
{ id: 'studio', label: t('tabs.studio'), icon: '\u27E8\u27E9' },
|
||||
{ id: 'shell', label: t('tabs.shell'), icon: '$' },
|
||||
{ id: 'config', label: t('tabs.config'), icon: '\u2699' },
|
||||
], [t])
|
||||
|
||||
useEffect(() => {
|
||||
api.getInfo().then(setInfo).catch(() => {})
|
||||
@@ -35,10 +37,16 @@ export default function App() {
|
||||
useEffect(() => {
|
||||
const onKey = (e) => {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
|
||||
const map = { '1': 'dash', '2': 'studio', '3': 'shell', '4': 'config' }
|
||||
if (map[e.key]) {
|
||||
if (!e.ctrlKey && !e.metaKey) return
|
||||
const map = {
|
||||
Digit1: 'dash',
|
||||
Digit2: 'studio',
|
||||
Digit3: 'shell',
|
||||
Digit4: 'config',
|
||||
}
|
||||
if (map[e.code]) {
|
||||
e.preventDefault()
|
||||
setActiveTab(map[e.key])
|
||||
setActiveTab(map[e.code])
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', onKey)
|
||||
@@ -50,6 +58,25 @@ export default function App() {
|
||||
const hasUpdates = updates.some(u => u.needsUpdate)
|
||||
const installed = tools.filter(t => t.installed).length
|
||||
|
||||
const WINDOW_SHORTCUTS = useMemo(() => ({
|
||||
dash: [
|
||||
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
||||
],
|
||||
studio: [
|
||||
{ keys: layout.keys.enter, desc: t('statusbar.sendMessage') },
|
||||
{ keys: `${layout.keys.shift}+${layout.keys.enter}`, desc: t('statusbar.newLine') },
|
||||
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
||||
],
|
||||
shell: [
|
||||
{ keys: layout.keys.enter, desc: t('statusbar.runCommand') },
|
||||
{ keys: `${layout.keys.up}/${layout.keys.down}`, desc: t('statusbar.commandHistory') },
|
||||
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
||||
],
|
||||
config: [
|
||||
{ keys: `${layout.keys.ctrl}+${layout.keys.range}`, desc: t('statusbar.switchWindow') },
|
||||
],
|
||||
}), [layout, t])
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeTab) {
|
||||
case 'dash': return <Dashboard tools={tools} updates={updates} api={api} onRescan={t => setTools(t)} />
|
||||
@@ -86,28 +113,43 @@ export default function App() {
|
||||
<div className="header-spacer" />
|
||||
|
||||
<div className="header-indicators">
|
||||
<span className={`indicator ${installed > 0 ? 'ok' : 'off'}`} title={`${installed} tools installed`} />
|
||||
<span className={`indicator ${hasUpdates ? 'warn' : 'ok'}`} title={hasUpdates ? 'Updates available' : 'Up to date'} />
|
||||
<span
|
||||
className={`indicator ${installed > 0 ? 'ok' : 'off'}`}
|
||||
title={t('header.toolsInstalled', { count: installed })}
|
||||
/>
|
||||
<span
|
||||
className={`indicator ${hasUpdates ? 'warn' : 'ok'}`}
|
||||
title={hasUpdates ? t('header.updatesAvailable') : t('header.upToDate')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="header-clock">
|
||||
{clock.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
|
||||
{clock.toLocaleTimeString(layout.locale, { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<main className="content fade-in" key={activeTab}>
|
||||
<main className="content fade-in" key={`${activeTab}-${TABS.length}`}>
|
||||
{renderContent()}
|
||||
</main>
|
||||
|
||||
<footer className="statusbar">
|
||||
<div className="statusbar-left">
|
||||
<span>Press 1-4 to switch tabs</span>
|
||||
<FooterShortcuts shortcuts={WINDOW_SHORTCUTS[activeTab] || []} />
|
||||
</div>
|
||||
<div className="statusbar-right">
|
||||
{hasUpdates && <span style={{ color: 'var(--warning)' }}>Updates available</span>}
|
||||
<span>v{info.version || '...'}</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)' }}>
|
||||
{layout.keys.ctrl}+{layout.keys.range} {t('statusbar.switchWindow')}
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FooterShortcuts({ shortcuts }) {
|
||||
return shortcuts.map((s, i) => (
|
||||
<span key={i} className="statusbar-shortcut">
|
||||
<kbd>{s.keys}</kbd> {s.desc}
|
||||
</span>
|
||||
))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user