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:
parent
eee65070a8
commit
aae86a4001
16 changed files with 947 additions and 64 deletions
|
|
@ -4,6 +4,7 @@
|
|||
import { THEMES, THEME_GROUPS, getPalette, type ThemeId, type ThemeMeta } from '../themes.ts';
|
||||
import { themeStore } from '../theme-store.svelte.ts';
|
||||
import { fontStore } from '../font-store.svelte.ts';
|
||||
import { t, getLocale, setLocale, AVAILABLE_LOCALES } from '../i18n.svelte.ts';
|
||||
import ThemeEditor from './ThemeEditor.svelte';
|
||||
|
||||
const UI_FONTS = [
|
||||
|
|
@ -50,6 +51,16 @@
|
|||
let themeOpen = $state(false);
|
||||
let uiFontOpen = $state(false);
|
||||
let termFontOpen = $state(false);
|
||||
let langOpen = $state(false);
|
||||
|
||||
// ── Language ──────────────────────────────────────────────────────────────
|
||||
let currentLocale = $derived(getLocale());
|
||||
let langLabel = $derived(AVAILABLE_LOCALES.find(l => l.tag === currentLocale)?.nativeLabel ?? 'English');
|
||||
|
||||
function selectLang(tag: string): void {
|
||||
langOpen = false;
|
||||
setLocale(tag);
|
||||
}
|
||||
|
||||
// ── All themes (built-in + custom) ────────────────────────────────────────
|
||||
let allThemes = $derived<ThemeMeta[]>([
|
||||
|
|
@ -106,7 +117,7 @@
|
|||
appRpc?.request['settings.set']({ key: 'scrollback', value: String(v) }).catch(console.error);
|
||||
}
|
||||
|
||||
function closeAll(): void { themeOpen = false; uiFontOpen = false; termFontOpen = false; }
|
||||
function closeAll(): void { themeOpen = false; uiFontOpen = false; termFontOpen = false; langOpen = false; }
|
||||
function handleOutsideClick(e: MouseEvent): void {
|
||||
if (!(e.target as HTMLElement).closest('.dd-wrap')) closeAll();
|
||||
}
|
||||
|
|
@ -149,7 +160,7 @@
|
|||
/>
|
||||
{:else}
|
||||
|
||||
<h3 class="sh">Theme</h3>
|
||||
<h3 class="sh">{t('settings.theme')}</h3>
|
||||
<div class="field">
|
||||
<div class="dd-wrap">
|
||||
<button class="dd-btn" onclick={() => { themeOpen = !themeOpen; uiFontOpen = false; termFontOpen = false; }}>
|
||||
|
|
@ -186,7 +197,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="sh">UI Font</h3>
|
||||
<h3 class="sh">{t('settings.uiFont')}</h3>
|
||||
<div class="field row">
|
||||
<div class="dd-wrap flex1">
|
||||
<button class="dd-btn" onclick={() => { uiFontOpen = !uiFontOpen; themeOpen = false; termFontOpen = false; }}>
|
||||
|
|
@ -211,7 +222,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="sh">Terminal Font</h3>
|
||||
<h3 class="sh">{t('settings.termFont')}</h3>
|
||||
<div class="field row">
|
||||
<div class="dd-wrap flex1">
|
||||
<button class="dd-btn" onclick={() => { termFontOpen = !termFontOpen; themeOpen = false; uiFontOpen = false; }}>
|
||||
|
|
@ -236,7 +247,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="sh">Terminal Cursor</h3>
|
||||
<h3 class="sh">{t('settings.termCursor')}</h3>
|
||||
<div class="field row">
|
||||
<div class="seg">
|
||||
{#each ['block', 'line', 'underline'] as s}
|
||||
|
|
@ -249,12 +260,35 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<h3 class="sh">Scrollback</h3>
|
||||
<h3 class="sh">{t('settings.scrollback')}</h3>
|
||||
<div class="field row">
|
||||
<input type="number" class="num-in" min="100" max="100000" step="100" value={scrollback}
|
||||
onchange={e => persistScrollback(parseInt((e.target as HTMLInputElement).value, 10) || 1000)}
|
||||
aria-label="Scrollback lines" />
|
||||
<span class="hint">lines (100–100k)</span>
|
||||
<span class="hint">{t('settings.scrollbackHint')}</span>
|
||||
</div>
|
||||
|
||||
<h3 class="sh">{t('settings.language')}</h3>
|
||||
<div class="field">
|
||||
<div class="dd-wrap">
|
||||
<button class="dd-btn" onclick={() => { langOpen = !langOpen; themeOpen = false; uiFontOpen = false; termFontOpen = false; }}>
|
||||
{langLabel}
|
||||
<svg class="chev" class:open={langOpen} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
{#if langOpen}
|
||||
<ul class="dd-list" role="listbox">
|
||||
{#each AVAILABLE_LOCALES as loc}
|
||||
<li class="dd-item" class:sel={currentLocale === loc.tag} role="option" aria-selected={currentLocale === loc.tag}
|
||||
tabindex="0" onclick={() => selectLang(loc.tag)}
|
||||
onkeydown={e => (e.key === 'Enter' || e.key === ' ') && selectLang(loc.tag)}
|
||||
>
|
||||
<span class="dd-item-label">{loc.nativeLabel}</span>
|
||||
<span style="color: var(--ctp-overlay0); font-size: 0.6875rem;">{loc.label}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue