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

@ -2,6 +2,7 @@
import { tick } from 'svelte';
import ChatInput from './ChatInput.svelte';
import type { AgentMessage, AgentStatus } from './agent-store.svelte.ts';
import { t } from './i18n.svelte.ts';
interface Props {
messages: AgentMessage[];
@ -68,10 +69,10 @@
}
function statusLabel(s: AgentStatus) {
if (s === 'running') return 'Running';
if (s === 'error') return 'Error';
if (s === 'done') return 'Done';
return 'Idle';
if (s === 'running') return t('agent.status.running');
if (s === 'error') return t('agent.status.error');
if (s === 'done') return t('agent.status.done');
return t('agent.status.idle');
}
function onResizeMouseDown(e: MouseEvent) {
@ -106,7 +107,7 @@
<span class="strip-sep" aria-hidden="true"></span>
<span class="strip-cost">{fmtCost(costUsd)}</span>
{#if status === 'running' && onStop}
<button class="strip-stop-btn" onclick={onStop} title="Stop agent" aria-label="Stop agent">
<button class="strip-stop-btn" onclick={onStop} title={t('agent.prompt.stop')} aria-label={t('agent.prompt.stop')}>
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<rect x="3" y="3" width="10" height="10" rx="1" />
</svg>
@ -206,6 +207,7 @@
{model}
{provider}
{contextPct}
placeholder={t('agent.prompt.placeholder')}
onSend={handleSend}
onInput={(v) => (promptText = v)}
/>