feat(electrobun): i18n system — @formatjs/intl + Svelte 5 runes + 3 locales

- i18n.svelte.ts: store with $state locale + createIntl(), t() function,
  formatDate/Number/RelativeTime, getDir() for RTL, async setLocale()
- i18n.types.ts: TranslationKey union (codegen from en.json)
- locales/en.json: 200+ strings in ICU MessageFormat
- locales/pl.json: full Polish translation
- locales/ar.json: partial Arabic (validates 6-form plural + RTL)
- scripts/i18n-types.ts: codegen script for type-safe keys
- 6 components wired: StatusBar, AgentPane, CommandPalette,
  SettingsDrawer, SplashScreen, ChatInput
- Language selector in AppearanceSettings
- App.svelte: document.dir reactive for RTL
- CONTRIBUTING_I18N.md: guide for adding languages

Note: currently Electrobun-only. Will extract to @agor/i18n shared
package for both Tauri and Electrobun.
This commit is contained in:
Hibryda 2026-03-22 10:28:13 +01:00
parent eee65070a8
commit aae86a4001
16 changed files with 947 additions and 64 deletions

View file

@ -4,6 +4,7 @@
getAttentionQueue,
type ProjectHealth,
} from './health-store.svelte.ts';
import { t } from './i18n.svelte.ts';
interface Props {
projectCount: number;
@ -60,21 +61,21 @@
<span class="status-segment">
<span class="dot green pulse-dot" aria-hidden="true"></span>
<span class="val">{health.running}</span>
<span>running</span>
<span>{t('statusbar.running')}</span>
</span>
{/if}
{#if health.idle > 0}
<span class="status-segment">
<span class="dot gray" aria-hidden="true"></span>
<span class="val">{health.idle}</span>
<span>idle</span>
<span>{t('statusbar.idle')}</span>
</span>
{/if}
{#if health.stalled > 0}
<span class="status-segment stalled">
<span class="dot orange" aria-hidden="true"></span>
<span class="val">{health.stalled}</span>
<span>stalled</span>
<span>{t('statusbar.stalled')}</span>
</span>
{/if}
@ -84,11 +85,11 @@
class="status-segment attn-btn"
class:attn-open={showAttention}
onclick={() => showAttention = !showAttention}
title="Needs attention"
title={t('statusbar.needsAttention')}
>
<span class="dot orange pulse-dot" aria-hidden="true"></span>
<span class="val">{attentionQueue.length}</span>
<span>attention</span>
<span>{t('statusbar.attention')}</span>
</button>
{/if}
@ -96,32 +97,32 @@
<!-- Right: aggregates -->
{#if health.totalBurnRatePerHour > 0}
<span class="status-segment burn" title="Burn rate">
<span class="status-segment burn" title={t('statusbar.burnRate')}>
<span class="val">{formatRate(health.totalBurnRatePerHour)}</span>
</span>
{/if}
<span class="status-segment" title="Active group">
<span class="status-segment" title={t('statusbar.activeGroup')}>
<span class="val">{groupName}</span>
</span>
<span class="status-segment" title="Projects">
<span class="status-segment" title={t('statusbar.projects')}>
<span class="val">{projectCount}</span>
<span>projects</span>
<span>{t('statusbar.projects')}</span>
</span>
<span class="status-segment" title="Session duration">
<span>session</span>
<span class="status-segment" title={t('statusbar.session')}>
<span>{t('statusbar.session')}</span>
<span class="val">{sessionDuration}</span>
</span>
<span class="status-segment" title="Total tokens">
<span>tokens</span>
<span class="status-segment" title={t('statusbar.tokens')}>
<span>{t('statusbar.tokens')}</span>
<span class="val">{fmtTokens(totalTokens)}</span>
</span>
<span class="status-segment" title="Total cost">
<span>cost</span>
<span class="status-segment" title={t('statusbar.cost')}>
<span>{t('statusbar.cost')}</span>
<span class="val cost">{fmtCost(totalCost)}</span>
</span>
<kbd class="palette-hint" title="Search (Ctrl+Shift+F)">Ctrl+Shift+F</kbd>
<kbd class="palette-hint" title={t('statusbar.search')}>Ctrl+Shift+F</kbd>
</footer>
<!-- Attention dropdown -->