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
104
ui-electrobun/CONTRIBUTING_I18N.md
Normal file
104
ui-electrobun/CONTRIBUTING_I18N.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# i18n Contributing Guide
|
||||
|
||||
This project uses ICU MessageFormat via `@formatjs/intl` with a custom Svelte 5 store.
|
||||
|
||||
## Adding a New Language
|
||||
|
||||
1. Create `locales/<tag>.json` (e.g. `locales/de.json`) with all keys from `locales/en.json`.
|
||||
2. Register the locale in `src/mainview/i18n.svelte.ts`:
|
||||
- Add an entry to `AVAILABLE_LOCALES` with tag, label, nativeLabel, and dir.
|
||||
- Add a dynamic import entry to the `loaders` map.
|
||||
3. The language will automatically appear in Settings > Appearance > Language.
|
||||
|
||||
## Key Naming Convention
|
||||
|
||||
Use dot-notation: `component.element.state`
|
||||
|
||||
```
|
||||
sidebar.settings -- component.element
|
||||
agent.status.running -- component.element.state
|
||||
settings.appearance -- component.element
|
||||
common.cancel -- shared across components
|
||||
errors.connectionFailed -- error messages
|
||||
```
|
||||
|
||||
Group prefixes:
|
||||
- `sidebar.*` -- left sidebar
|
||||
- `agent.*` -- agent pane
|
||||
- `terminal.*` -- terminal tabs
|
||||
- `settings.*` -- settings drawer
|
||||
- `statusbar.*` -- bottom status bar
|
||||
- `notifications.*` -- notification drawer
|
||||
- `files.*` -- file browser
|
||||
- `search.*` -- search overlay
|
||||
- `comms.*` -- communications tab
|
||||
- `tasks.*` -- task board
|
||||
- `errors.*` -- error messages
|
||||
- `splash.*` -- splash screen
|
||||
- `common.*` -- shared labels (cancel, save, delete)
|
||||
- `project.*` -- project management
|
||||
- `palette.*` -- command palette
|
||||
|
||||
## ICU MessageFormat Syntax
|
||||
|
||||
### Plain text
|
||||
```json
|
||||
"common.save": "Save"
|
||||
```
|
||||
|
||||
### Plurals (English: one/other)
|
||||
```json
|
||||
"search.resultsCount": "{count, plural, one {{count} result} other {{count} results}}"
|
||||
```
|
||||
|
||||
### Plurals (Polish: one/few/many/other)
|
||||
```json
|
||||
"search.resultsCount": "{count, plural, one {{count} wynik} few {{count} wyniki} many {{count} wynikow} other {{count} wynikow}}"
|
||||
```
|
||||
|
||||
### Plurals (Arabic: zero/one/two/few/many/other)
|
||||
```json
|
||||
"tasks.taskCount": "{count, plural, =0 {no tasks} one {one task} two {two tasks} few {{count} tasks} many {{count} tasks} other {{count} tasks}}"
|
||||
```
|
||||
|
||||
### Interpolation
|
||||
```json
|
||||
"errors.unhandled": "Unhandled error: {message}"
|
||||
```
|
||||
|
||||
### Select
|
||||
```json
|
||||
"agent.status": "{status, select, running {Running} idle {Idle} other {Unknown}}"
|
||||
```
|
||||
|
||||
## Using Translations in Components
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { t } from './i18n.svelte.ts';
|
||||
</script>
|
||||
|
||||
<button>{t('common.save')}</button>
|
||||
<span>{t('sidebar.notifCount', { count: 5 })}</span>
|
||||
```
|
||||
|
||||
## Testing Translations
|
||||
|
||||
1. Switch language in Settings > Appearance > Language.
|
||||
2. All `t()` calls update reactively -- no page reload needed.
|
||||
3. Missing keys fall back to the key name itself (e.g. `"agent.status.running"`).
|
||||
4. Check the browser console for `[i18n]` warnings about missing or malformed messages.
|
||||
|
||||
## Regenerating Types
|
||||
|
||||
After adding or removing keys in `locales/en.json`:
|
||||
|
||||
```bash
|
||||
bun scripts/i18n-types.ts
|
||||
```
|
||||
|
||||
This regenerates `src/mainview/i18n.types.ts` with the updated `TranslationKey` union type. TypeScript will then flag any `t()` calls using keys that no longer exist.
|
||||
|
||||
## RTL Support
|
||||
|
||||
Arabic (`ar`) sets `document.dir = 'rtl'` automatically. CSS should use logical properties (`inline-start`/`inline-end`) instead of `left`/`right` where possible.
|
||||
75
ui-electrobun/locales/ar.json
Normal file
75
ui-electrobun/locales/ar.json
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"sidebar.settings": "\u0625\u0639\u062f\u0627\u062f\u0627\u062a",
|
||||
"sidebar.addGroup": "\u0625\u0636\u0627\u0641\u0629 \u0645\u062c\u0645\u0648\u0639\u0629",
|
||||
"sidebar.addProject": "\u0625\u0636\u0627\u0641\u0629 \u0645\u0634\u0631\u0648\u0639",
|
||||
"sidebar.groupName": "\u0627\u0633\u0645 \u0627\u0644\u0645\u062c\u0645\u0648\u0639\u0629",
|
||||
"sidebar.notifications": "\u0627\u0644\u0625\u0634\u0639\u0627\u0631\u0627\u062a",
|
||||
"sidebar.notifCount": "{count, plural, =0 {\u0627\u0644\u0625\u0634\u0639\u0627\u0631\u0627\u062a} one {\u0625\u0634\u0639\u0627\u0631 \u0648\u0627\u062d\u062f} two {\u0625\u0634\u0639\u0627\u0631\u0627\u0646} few {{count} \u0625\u0634\u0639\u0627\u0631\u0627\u062a} many {{count} \u0625\u0634\u0639\u0627\u0631\u064b\u0627} other {{count} \u0625\u0634\u0639\u0627\u0631}}",
|
||||
"sidebar.close": "\u0625\u063a\u0644\u0627\u0642 \u0627\u0644\u0646\u0627\u0641\u0630\u0629",
|
||||
"sidebar.maximize": "\u062a\u0643\u0628\u064a\u0631 \u0627\u0644\u0646\u0627\u0641\u0630\u0629",
|
||||
"sidebar.minimize": "\u062a\u0635\u063a\u064a\u0631 \u0627\u0644\u0646\u0627\u0641\u0630\u0629",
|
||||
|
||||
"agent.prompt.placeholder": "\u0627\u0637\u0631\u062d \u0633\u0624\u0627\u0644\u0627\u064b \u0623\u0648 \u0635\u0641 \u0645\u0647\u0645\u0629...",
|
||||
"agent.prompt.send": "\u0625\u0631\u0633\u0627\u0644",
|
||||
"agent.prompt.stop": "\u0625\u064a\u0642\u0627\u0641 \u0627\u0644\u0648\u0643\u064a\u0644",
|
||||
"agent.status.running": "\u064a\u0639\u0645\u0644",
|
||||
"agent.status.idle": "\u062e\u0627\u0645\u0644",
|
||||
"agent.status.done": "\u0645\u0643\u062a\u0645\u0644",
|
||||
"agent.status.error": "\u062e\u0637\u0623",
|
||||
"agent.status.stalled": "\u0645\u062a\u0648\u0642\u0641",
|
||||
"agent.status.thinking": "\u064a\u0641\u0643\u0631",
|
||||
"agent.tokens": "{count} \u0631\u0645\u0632",
|
||||
|
||||
"settings.title": "\u0627\u0644\u0625\u0639\u062f\u0627\u062f\u0627\u062a",
|
||||
"settings.close": "\u0625\u063a\u0644\u0627\u0642 \u0627\u0644\u0625\u0639\u062f\u0627\u062f\u0627\u062a",
|
||||
"settings.appearance": "\u0627\u0644\u0645\u0638\u0647\u0631",
|
||||
"settings.agents": "\u0627\u0644\u0648\u0643\u0644\u0627\u0621",
|
||||
"settings.security": "\u0627\u0644\u0623\u0645\u0627\u0646",
|
||||
"settings.projects": "\u0627\u0644\u0645\u0634\u0627\u0631\u064a\u0639",
|
||||
"settings.orchestration": "\u0627\u0644\u062a\u0646\u0633\u064a\u0642",
|
||||
"settings.machines": "\u0627\u0644\u0623\u062c\u0647\u0632\u0629",
|
||||
"settings.keyboard": "\u0644\u0648\u062d\u0629 \u0627\u0644\u0645\u0641\u0627\u062a\u064a\u062d",
|
||||
"settings.advanced": "\u0645\u062a\u0642\u062f\u0645",
|
||||
"settings.marketplace": "\u0627\u0644\u0633\u0648\u0642",
|
||||
"settings.diagnostics": "\u0627\u0644\u062a\u0634\u062e\u064a\u0635",
|
||||
"settings.language": "\u0627\u0644\u0644\u063a\u0629",
|
||||
|
||||
"statusbar.running": "\u064a\u0639\u0645\u0644",
|
||||
"statusbar.idle": "\u062e\u0627\u0645\u0644",
|
||||
"statusbar.stalled": "\u0645\u062a\u0648\u0642\u0641",
|
||||
"statusbar.attention": "\u0627\u0646\u062a\u0628\u0627\u0647",
|
||||
"statusbar.projects": "\u0645\u0634\u0627\u0631\u064a\u0639",
|
||||
"statusbar.session": "\u062c\u0644\u0633\u0629",
|
||||
"statusbar.tokens": "\u0631\u0645\u0648\u0632",
|
||||
"statusbar.cost": "\u0627\u0644\u062a\u0643\u0644\u0641\u0629",
|
||||
|
||||
"splash.loading": "\u062c\u0627\u0631\u064d \u0627\u0644\u062a\u062d\u0645\u064a\u0644...",
|
||||
|
||||
"common.cancel": "\u0625\u0644\u063a\u0627\u0621",
|
||||
"common.confirm": "\u062a\u0623\u0643\u064a\u062f",
|
||||
"common.close": "\u0625\u063a\u0644\u0627\u0642",
|
||||
"common.save": "\u062d\u0641\u0638",
|
||||
"common.delete": "\u062d\u0630\u0641",
|
||||
"common.edit": "\u062a\u0639\u062f\u064a\u0644",
|
||||
"common.refresh": "\u062a\u062d\u062f\u064a\u062b",
|
||||
"common.add": "\u0625\u0636\u0627\u0641\u0629",
|
||||
"common.back": "\u0631\u062c\u0648\u0639",
|
||||
"common.noItems": "\u0644\u0627 \u062a\u0648\u062c\u062f \u0639\u0646\u0627\u0635\u0631",
|
||||
|
||||
"tasks.todo": "\u0644\u0644\u062a\u0646\u0641\u064a\u0630",
|
||||
"tasks.inProgress": "\u0642\u064a\u062f \u0627\u0644\u062a\u0646\u0641\u064a\u0630",
|
||||
"tasks.review": "\u0645\u0631\u0627\u062c\u0639\u0629",
|
||||
"tasks.done": "\u0645\u0643\u062a\u0645\u0644",
|
||||
"tasks.blocked": "\u0645\u062d\u0638\u0648\u0631",
|
||||
"tasks.taskCount": "{count, plural, =0 {\u0644\u0627 \u0645\u0647\u0627\u0645} one {\u0645\u0647\u0645\u0629 \u0648\u0627\u062d\u062f\u0629} two {\u0645\u0647\u0645\u062a\u0627\u0646} few {{count} \u0645\u0647\u0627\u0645} many {{count} \u0645\u0647\u0645\u0629} other {{count} \u0645\u0647\u0645\u0629}}",
|
||||
|
||||
"errors.connectionFailed": "\u0641\u0634\u0644 \u0627\u0644\u0627\u062a\u0635\u0627\u0644",
|
||||
"errors.sessionExpired": "\u0627\u0646\u062a\u0647\u062a \u0627\u0644\u062c\u0644\u0633\u0629",
|
||||
"errors.fileNotFound": "\u0627\u0644\u0645\u0644\u0641 \u063a\u064a\u0631 \u0645\u0648\u062c\u0648\u062f",
|
||||
"errors.generic": "\u062d\u062f\u062b \u062e\u0637\u0623 \u0645\u0627",
|
||||
|
||||
"palette.placeholder": "\u0627\u0643\u062a\u0628 \u0623\u0645\u0631\u064b\u0627...",
|
||||
"palette.newTerminal": "\u0639\u0644\u0627\u0645\u0629 \u062a\u0628\u0648\u064a\u0628 \u0637\u0631\u0641\u064a\u0629 \u062c\u062f\u064a\u062f\u0629",
|
||||
"palette.openSettings": "\u0641\u062a\u062d \u0627\u0644\u0625\u0639\u062f\u0627\u062f\u0627\u062a",
|
||||
"palette.searchMessages": "\u0628\u062d\u062b \u0641\u064a \u0627\u0644\u0631\u0633\u0627\u0626\u0644"
|
||||
}
|
||||
150
ui-electrobun/locales/en.json
Normal file
150
ui-electrobun/locales/en.json
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
{
|
||||
"sidebar.settings": "Settings",
|
||||
"sidebar.addGroup": "Add group",
|
||||
"sidebar.addProject": "Add project",
|
||||
"sidebar.groupName": "Group name",
|
||||
"sidebar.notifications": "Notifications",
|
||||
"sidebar.notifCount": "{count, plural, =0 {Notifications} one {{count} notification} other {{count} notifications}}",
|
||||
"sidebar.close": "Close window",
|
||||
"sidebar.maximize": "Maximize window",
|
||||
"sidebar.minimize": "Minimize window",
|
||||
|
||||
"agent.prompt.placeholder": "Ask a question or describe a task...",
|
||||
"agent.prompt.send": "Send",
|
||||
"agent.prompt.stop": "Stop agent",
|
||||
"agent.status.running": "Running",
|
||||
"agent.status.idle": "Idle",
|
||||
"agent.status.done": "Done",
|
||||
"agent.status.error": "Error",
|
||||
"agent.status.stalled": "Stalled",
|
||||
"agent.status.thinking": "Thinking",
|
||||
"agent.tokens": "{count} tok",
|
||||
"agent.toolCall": "Tool call",
|
||||
"agent.toolResult": "Tool result",
|
||||
"agent.contextMeter": "Context: {pct}%",
|
||||
|
||||
"terminal.shell": "Shell",
|
||||
"terminal.addTab": "New tab",
|
||||
"terminal.closeTab": "Close tab",
|
||||
"terminal.collapse": "Collapse",
|
||||
"terminal.expand": "Expand",
|
||||
|
||||
"settings.title": "Settings",
|
||||
"settings.close": "Close settings",
|
||||
"settings.appearance": "Appearance",
|
||||
"settings.agents": "Agents",
|
||||
"settings.security": "Security",
|
||||
"settings.projects": "Projects",
|
||||
"settings.orchestration": "Orchestration",
|
||||
"settings.machines": "Machines",
|
||||
"settings.keyboard": "Keyboard",
|
||||
"settings.advanced": "Advanced",
|
||||
"settings.marketplace": "Marketplace",
|
||||
"settings.diagnostics": "Diagnostics",
|
||||
"settings.theme": "Theme",
|
||||
"settings.uiFont": "UI Font",
|
||||
"settings.termFont": "Terminal Font",
|
||||
"settings.termCursor": "Terminal Cursor",
|
||||
"settings.scrollback": "Scrollback",
|
||||
"settings.language": "Language",
|
||||
"settings.editTheme": "Edit Theme",
|
||||
"settings.customTheme": "+ Custom",
|
||||
"settings.deleteTheme": "Delete theme",
|
||||
"settings.cursorBlink": "Blink",
|
||||
"settings.cursorOn": "On",
|
||||
"settings.cursorOff": "Off",
|
||||
"settings.scrollbackHint": "lines (100-100k)",
|
||||
|
||||
"statusbar.running": "running",
|
||||
"statusbar.idle": "idle",
|
||||
"statusbar.stalled": "stalled",
|
||||
"statusbar.attention": "attention",
|
||||
"statusbar.needsAttention": "Needs attention",
|
||||
"statusbar.burnRate": "Burn rate",
|
||||
"statusbar.activeGroup": "Active group",
|
||||
"statusbar.projects": "projects",
|
||||
"statusbar.session": "session",
|
||||
"statusbar.tokens": "tokens",
|
||||
"statusbar.cost": "cost",
|
||||
"statusbar.search": "Search (Ctrl+Shift+F)",
|
||||
|
||||
"notifications.title": "Notifications",
|
||||
"notifications.clearAll": "Clear all",
|
||||
"notifications.noNotifications": "No notifications",
|
||||
|
||||
"files.open": "Open",
|
||||
"files.save": "Save",
|
||||
"files.saving": "Saving...",
|
||||
"files.modified": "Modified",
|
||||
"files.tooLarge": "File too large to display",
|
||||
"files.empty": "Empty file",
|
||||
|
||||
"search.placeholder": "Search messages, tasks, comms...",
|
||||
"search.noResults": "No results found",
|
||||
"search.searching": "Searching...",
|
||||
"search.resultsCount": "{count, plural, one {{count} result} other {{count} results}}",
|
||||
|
||||
"comms.channels": "Channels",
|
||||
"comms.directMessages": "Direct Messages",
|
||||
"comms.sendMessage": "Send message",
|
||||
"comms.placeholder": "Type a message...",
|
||||
|
||||
"tasks.todo": "To Do",
|
||||
"tasks.inProgress": "In Progress",
|
||||
"tasks.review": "Review",
|
||||
"tasks.done": "Done",
|
||||
"tasks.blocked": "Blocked",
|
||||
"tasks.addTask": "Add task",
|
||||
"tasks.deleteTask": "Delete task",
|
||||
"tasks.taskCount": "{count, plural, =0 {No tasks} one {{count} task} other {{count} tasks}}",
|
||||
|
||||
"errors.connectionFailed": "Connection failed",
|
||||
"errors.sessionExpired": "Session expired",
|
||||
"errors.fileNotFound": "File not found",
|
||||
"errors.unhandled": "Unhandled error: {message}",
|
||||
"errors.generic": "Something went wrong",
|
||||
|
||||
"splash.loading": "Loading...",
|
||||
|
||||
"common.cancel": "Cancel",
|
||||
"common.confirm": "Confirm",
|
||||
"common.close": "Close",
|
||||
"common.save": "Save",
|
||||
"common.delete": "Delete",
|
||||
"common.edit": "Edit",
|
||||
"common.refresh": "Refresh",
|
||||
"common.add": "Add",
|
||||
"common.back": "Back",
|
||||
"common.noItems": "No items",
|
||||
|
||||
"project.name": "Project name",
|
||||
"project.cwd": "Working directory (e.g. ~/code/myproject)",
|
||||
"project.deleteConfirm": "Delete project \"{name}\"?",
|
||||
"project.emptyGroup": "No projects in {group}",
|
||||
"project.clone": "Clone",
|
||||
"project.cloneBranch": "Branch name",
|
||||
|
||||
"palette.title": "Command Palette",
|
||||
"palette.placeholder": "Type a command...",
|
||||
"palette.newTerminal": "New Terminal Tab",
|
||||
"palette.openSettings": "Open Settings",
|
||||
"palette.searchMessages": "Search Messages",
|
||||
"palette.addProject": "Add Project",
|
||||
"palette.clearAgent": "Clear Agent Context",
|
||||
"palette.copyCost": "Copy Session Cost",
|
||||
"palette.openDocs": "Open Documentation",
|
||||
"palette.changeTheme": "Change Theme",
|
||||
"palette.splitH": "Split Horizontally",
|
||||
"palette.splitV": "Split Vertically",
|
||||
"palette.focusNext": "Focus Next Project",
|
||||
"palette.focusPrev": "Focus Previous Project",
|
||||
"palette.closeTab": "Close Tab",
|
||||
"palette.toggleTerminal": "Toggle Terminal",
|
||||
"palette.reloadPlugins": "Reload Plugins",
|
||||
"palette.toggleSidebar": "Toggle Sidebar",
|
||||
"palette.zoomIn": "Zoom In",
|
||||
"palette.zoomOut": "Zoom Out",
|
||||
"palette.addProjectDesc": "Open a project directory",
|
||||
"palette.clearAgentDesc": "Reset agent session",
|
||||
"palette.changeThemeDesc": "Switch between 17 themes"
|
||||
}
|
||||
150
ui-electrobun/locales/pl.json
Normal file
150
ui-electrobun/locales/pl.json
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
{
|
||||
"sidebar.settings": "Ustawienia",
|
||||
"sidebar.addGroup": "Dodaj grupe",
|
||||
"sidebar.addProject": "Dodaj projekt",
|
||||
"sidebar.groupName": "Nazwa grupy",
|
||||
"sidebar.notifications": "Powiadomienia",
|
||||
"sidebar.notifCount": "{count, plural, =0 {Powiadomienia} one {{count} powiadomienie} few {{count} powiadomienia} many {{count} powiadomien} other {{count} powiadomien}}",
|
||||
"sidebar.close": "Zamknij okno",
|
||||
"sidebar.maximize": "Maksymalizuj okno",
|
||||
"sidebar.minimize": "Minimalizuj okno",
|
||||
|
||||
"agent.prompt.placeholder": "Zadaj pytanie lub opisz zadanie...",
|
||||
"agent.prompt.send": "Wyslij",
|
||||
"agent.prompt.stop": "Zatrzymaj agenta",
|
||||
"agent.status.running": "Dziala",
|
||||
"agent.status.idle": "Bezczynny",
|
||||
"agent.status.done": "Gotowe",
|
||||
"agent.status.error": "Blad",
|
||||
"agent.status.stalled": "Zawieszony",
|
||||
"agent.status.thinking": "Mysli",
|
||||
"agent.tokens": "{count} tok",
|
||||
"agent.toolCall": "Wywolanie narzedzia",
|
||||
"agent.toolResult": "Wynik narzedzia",
|
||||
"agent.contextMeter": "Kontekst: {pct}%",
|
||||
|
||||
"terminal.shell": "Powloka",
|
||||
"terminal.addTab": "Nowa karta",
|
||||
"terminal.closeTab": "Zamknij karte",
|
||||
"terminal.collapse": "Zwin",
|
||||
"terminal.expand": "Rozwin",
|
||||
|
||||
"settings.title": "Ustawienia",
|
||||
"settings.close": "Zamknij ustawienia",
|
||||
"settings.appearance": "Wyglad",
|
||||
"settings.agents": "Agenci",
|
||||
"settings.security": "Bezpieczenstwo",
|
||||
"settings.projects": "Projekty",
|
||||
"settings.orchestration": "Orkiestracja",
|
||||
"settings.machines": "Maszyny",
|
||||
"settings.keyboard": "Klawiatura",
|
||||
"settings.advanced": "Zaawansowane",
|
||||
"settings.marketplace": "Sklep",
|
||||
"settings.diagnostics": "Diagnostyka",
|
||||
"settings.theme": "Motyw",
|
||||
"settings.uiFont": "Czcionka UI",
|
||||
"settings.termFont": "Czcionka terminala",
|
||||
"settings.termCursor": "Kursor terminala",
|
||||
"settings.scrollback": "Bufor przewijania",
|
||||
"settings.language": "Jezyk",
|
||||
"settings.editTheme": "Edytuj motyw",
|
||||
"settings.customTheme": "+ Wlasny",
|
||||
"settings.deleteTheme": "Usun motyw",
|
||||
"settings.cursorBlink": "Mruganie",
|
||||
"settings.cursorOn": "Wl.",
|
||||
"settings.cursorOff": "Wyl.",
|
||||
"settings.scrollbackHint": "linii (100-100k)",
|
||||
|
||||
"statusbar.running": "dziala",
|
||||
"statusbar.idle": "bezczynny",
|
||||
"statusbar.stalled": "zawieszony",
|
||||
"statusbar.attention": "uwaga",
|
||||
"statusbar.needsAttention": "Wymaga uwagi",
|
||||
"statusbar.burnRate": "Zuzycie",
|
||||
"statusbar.activeGroup": "Aktywna grupa",
|
||||
"statusbar.projects": "projekty",
|
||||
"statusbar.session": "sesja",
|
||||
"statusbar.tokens": "tokeny",
|
||||
"statusbar.cost": "koszt",
|
||||
"statusbar.search": "Szukaj (Ctrl+Shift+F)",
|
||||
|
||||
"notifications.title": "Powiadomienia",
|
||||
"notifications.clearAll": "Wyczysc wszystko",
|
||||
"notifications.noNotifications": "Brak powiadomien",
|
||||
|
||||
"files.open": "Otworz",
|
||||
"files.save": "Zapisz",
|
||||
"files.saving": "Zapisywanie...",
|
||||
"files.modified": "Zmodyfikowany",
|
||||
"files.tooLarge": "Plik za duzy do wyswietlenia",
|
||||
"files.empty": "Pusty plik",
|
||||
|
||||
"search.placeholder": "Szukaj wiadomosci, zadan, komunikacji...",
|
||||
"search.noResults": "Brak wynikow",
|
||||
"search.searching": "Szukanie...",
|
||||
"search.resultsCount": "{count, plural, one {{count} wynik} few {{count} wyniki} many {{count} wynikow} other {{count} wynikow}}",
|
||||
|
||||
"comms.channels": "Kanaly",
|
||||
"comms.directMessages": "Wiadomosci prywatne",
|
||||
"comms.sendMessage": "Wyslij wiadomosc",
|
||||
"comms.placeholder": "Napisz wiadomosc...",
|
||||
|
||||
"tasks.todo": "Do zrobienia",
|
||||
"tasks.inProgress": "W toku",
|
||||
"tasks.review": "Przeglad",
|
||||
"tasks.done": "Gotowe",
|
||||
"tasks.blocked": "Zablokowane",
|
||||
"tasks.addTask": "Dodaj zadanie",
|
||||
"tasks.deleteTask": "Usun zadanie",
|
||||
"tasks.taskCount": "{count, plural, =0 {Brak zadan} one {{count} zadanie} few {{count} zadania} many {{count} zadan} other {{count} zadan}}",
|
||||
|
||||
"errors.connectionFailed": "Polaczenie nieudane",
|
||||
"errors.sessionExpired": "Sesja wygasla",
|
||||
"errors.fileNotFound": "Nie znaleziono pliku",
|
||||
"errors.unhandled": "Nieobsluzony blad: {message}",
|
||||
"errors.generic": "Cos poszlo nie tak",
|
||||
|
||||
"splash.loading": "Ladowanie...",
|
||||
|
||||
"common.cancel": "Anuluj",
|
||||
"common.confirm": "Potwierdz",
|
||||
"common.close": "Zamknij",
|
||||
"common.save": "Zapisz",
|
||||
"common.delete": "Usun",
|
||||
"common.edit": "Edytuj",
|
||||
"common.refresh": "Odswiez",
|
||||
"common.add": "Dodaj",
|
||||
"common.back": "Wstecz",
|
||||
"common.noItems": "Brak elementow",
|
||||
|
||||
"project.name": "Nazwa projektu",
|
||||
"project.cwd": "Katalog roboczy (np. ~/code/myproject)",
|
||||
"project.deleteConfirm": "Usunac projekt \"{name}\"?",
|
||||
"project.emptyGroup": "Brak projektow w {group}",
|
||||
"project.clone": "Klonuj",
|
||||
"project.cloneBranch": "Nazwa galezi",
|
||||
|
||||
"palette.title": "Paleta polecen",
|
||||
"palette.placeholder": "Wpisz polecenie...",
|
||||
"palette.newTerminal": "Nowa karta terminala",
|
||||
"palette.openSettings": "Otworz ustawienia",
|
||||
"palette.searchMessages": "Szukaj wiadomosci",
|
||||
"palette.addProject": "Dodaj projekt",
|
||||
"palette.clearAgent": "Wyczysc kontekst agenta",
|
||||
"palette.copyCost": "Kopiuj koszt sesji",
|
||||
"palette.openDocs": "Otworz dokumentacje",
|
||||
"palette.changeTheme": "Zmien motyw",
|
||||
"palette.splitH": "Podziel poziomo",
|
||||
"palette.splitV": "Podziel pionowo",
|
||||
"palette.focusNext": "Nastepny projekt",
|
||||
"palette.focusPrev": "Poprzedni projekt",
|
||||
"palette.closeTab": "Zamknij karte",
|
||||
"palette.toggleTerminal": "Przelacz terminal",
|
||||
"palette.reloadPlugins": "Przeladuj wtyczki",
|
||||
"palette.toggleSidebar": "Przelacz pasek boczny",
|
||||
"palette.zoomIn": "Przybliz",
|
||||
"palette.zoomOut": "Oddal",
|
||||
"palette.addProjectDesc": "Otworz katalog projektu",
|
||||
"palette.clearAgentDesc": "Zresetuj sesje agenta",
|
||||
"palette.changeThemeDesc": "Wybierz sposrod 17 motywow"
|
||||
}
|
||||
|
|
@ -10,7 +10,8 @@
|
|||
"hmr": "vite --port 9760",
|
||||
"build:canary": "vite build && electrobun build --env=canary",
|
||||
"test": "bun test src/bun/__tests__/",
|
||||
"test:e2e": "wdio run tests/e2e/wdio.conf.js"
|
||||
"test:e2e": "wdio run tests/e2e/wdio.conf.js",
|
||||
"i18n:types": "bun scripts/i18n-types.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.20.1",
|
||||
|
|
|
|||
37
ui-electrobun/scripts/i18n-types.ts
Normal file
37
ui-electrobun/scripts/i18n-types.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Reads locales/en.json and generates src/mainview/i18n.types.ts
|
||||
* with a TranslationKey union type covering all keys.
|
||||
*
|
||||
* Usage: bun scripts/i18n-types.ts
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const ROOT = resolve(import.meta.dir, '..');
|
||||
const EN_PATH = resolve(ROOT, 'locales/en.json');
|
||||
const OUT_PATH = resolve(ROOT, 'src/mainview/i18n.types.ts');
|
||||
|
||||
const en: Record<string, string> = JSON.parse(readFileSync(EN_PATH, 'utf-8'));
|
||||
const keys = Object.keys(en).sort();
|
||||
|
||||
const lines: string[] = [
|
||||
'/**',
|
||||
' * Auto-generated by scripts/i18n-types.ts — do not edit manually.',
|
||||
' * Run: bun scripts/i18n-types.ts',
|
||||
' */',
|
||||
'',
|
||||
'export type TranslationKey =',
|
||||
];
|
||||
|
||||
keys.forEach((key, i) => {
|
||||
const prefix = i === 0 ? ' | ' : ' | ';
|
||||
const suffix = i === keys.length - 1 ? ';' : '';
|
||||
lines.push(`${prefix}'${key}'${suffix}`);
|
||||
});
|
||||
|
||||
lines.push('');
|
||||
|
||||
writeFileSync(OUT_PATH, lines.join('\n'), 'utf-8');
|
||||
console.log(`[i18n-types] Generated ${keys.length} keys -> ${OUT_PATH}`);
|
||||
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
import { trackProject } from './health-store.svelte.ts';
|
||||
import { setAgentToastFn } from './agent-store.svelte.ts';
|
||||
import { appRpc } from './rpc.ts';
|
||||
import { initI18n, getDir, getLocale, t } from './i18n.svelte.ts';
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────
|
||||
type AgentStatus = 'running' | 'idle' | 'stalled';
|
||||
|
|
@ -285,6 +286,12 @@
|
|||
});
|
||||
}
|
||||
|
||||
// ── i18n: keep <html> lang and dir in sync ─────────────────────
|
||||
$effect(() => {
|
||||
document.documentElement.lang = getLocale();
|
||||
document.documentElement.dir = getDir();
|
||||
});
|
||||
|
||||
// ── Init ───────────────────────────────────────────────────────
|
||||
onMount(() => {
|
||||
// Wire agent toast callback
|
||||
|
|
@ -296,6 +303,7 @@
|
|||
// Fix #8: Load groups FIRST, then apply saved active_group.
|
||||
// Other init tasks run in parallel.
|
||||
const initTasks = [
|
||||
initI18n().catch(console.error),
|
||||
themeStore.initTheme(appRpc).catch(console.error),
|
||||
fontStore.initFonts(appRpc).catch(console.error),
|
||||
keybindingStore.init(appRpc).catch(console.error),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
provider?: string;
|
||||
contextPct?: number;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
onSend?: () => void;
|
||||
onInput?: (v: string) => void;
|
||||
}
|
||||
|
|
@ -17,6 +18,7 @@
|
|||
provider = 'claude',
|
||||
contextPct = 78,
|
||||
disabled = false,
|
||||
placeholder = 'Ask Claude anything...',
|
||||
onSend,
|
||||
onInput,
|
||||
}: Props = $props();
|
||||
|
|
@ -94,7 +96,7 @@
|
|||
<textarea
|
||||
bind:this={textareaEl}
|
||||
class="chat-textarea"
|
||||
placeholder="Ask Claude anything..."
|
||||
{placeholder}
|
||||
rows="1"
|
||||
{value}
|
||||
oninput={handleInput}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { tick } from 'svelte';
|
||||
import { t } from './i18n.svelte.ts';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
|
|
@ -21,27 +22,46 @@
|
|||
window.dispatchEvent(new CustomEvent('palette-command', { detail: name }));
|
||||
}
|
||||
|
||||
const COMMANDS: Command[] = [
|
||||
{ id: 'new-terminal', label: 'New Terminal Tab', shortcut: 'Ctrl+`', action: () => dispatch('new-terminal') },
|
||||
{ id: 'settings', label: 'Open Settings', shortcut: 'Ctrl+,', action: () => dispatch('settings') },
|
||||
{ id: 'search', label: 'Search Messages', shortcut: 'Ctrl+Shift+F', action: () => dispatch('search') },
|
||||
{ id: 'new-project', label: 'Add Project', description: 'Open a project directory', action: () => dispatch('new-project') },
|
||||
{ id: 'clear-agent', label: 'Clear Agent Context', description: 'Reset agent session', action: () => dispatch('clear-agent') },
|
||||
{ id: 'copy-cost', label: 'Copy Session Cost', action: () => dispatch('copy-cost') },
|
||||
{ id: 'docs', label: 'Open Documentation', shortcut: 'F1', action: () => dispatch('docs') },
|
||||
{ id: 'theme', label: 'Change Theme', description: 'Switch between 17 themes', action: () => dispatch('theme') },
|
||||
{ id: 'split-h', label: 'Split Horizontally', shortcut: 'Ctrl+\\', action: () => dispatch('split-h') },
|
||||
{ id: 'split-v', label: 'Split Vertically', shortcut: 'Ctrl+Shift+\\', action: () => dispatch('split-v') },
|
||||
{ id: 'focus-next', label: 'Focus Next Project', shortcut: 'Ctrl+Tab', action: () => dispatch('focus-next') },
|
||||
{ id: 'focus-prev', label: 'Focus Previous Project', shortcut: 'Ctrl+Shift+Tab', action: () => dispatch('focus-prev') },
|
||||
{ id: 'close-tab', label: 'Close Tab', shortcut: 'Ctrl+W', action: () => dispatch('close-tab') },
|
||||
{ id: 'toggle-terminal', label: 'Toggle Terminal', shortcut: 'Ctrl+J', action: () => dispatch('toggle-terminal') },
|
||||
{ id: 'reload-plugins', label: 'Reload Plugins', action: () => dispatch('reload-plugins') },
|
||||
{ id: 'toggle-sidebar', label: 'Toggle Sidebar', shortcut: 'Ctrl+B', action: () => dispatch('toggle-sidebar') },
|
||||
{ id: 'zoom-in', label: 'Zoom In', shortcut: 'Ctrl+=', action: () => dispatch('zoom-in') },
|
||||
{ id: 'zoom-out', label: 'Zoom Out', shortcut: 'Ctrl+-', action: () => dispatch('zoom-out') },
|
||||
// Command definitions — labels resolved reactively via t()
|
||||
interface CommandDef {
|
||||
id: string;
|
||||
labelKey: string;
|
||||
descKey?: string;
|
||||
shortcut?: string;
|
||||
action: () => void;
|
||||
}
|
||||
|
||||
const COMMAND_DEFS: CommandDef[] = [
|
||||
{ id: 'new-terminal', labelKey: 'palette.newTerminal', shortcut: 'Ctrl+`', action: () => dispatch('new-terminal') },
|
||||
{ id: 'settings', labelKey: 'palette.openSettings', shortcut: 'Ctrl+,', action: () => dispatch('settings') },
|
||||
{ id: 'search', labelKey: 'palette.searchMessages', shortcut: 'Ctrl+Shift+F', action: () => dispatch('search') },
|
||||
{ id: 'new-project', labelKey: 'palette.addProject', descKey: 'palette.addProjectDesc', action: () => dispatch('new-project') },
|
||||
{ id: 'clear-agent', labelKey: 'palette.clearAgent', descKey: 'palette.clearAgentDesc', action: () => dispatch('clear-agent') },
|
||||
{ id: 'copy-cost', labelKey: 'palette.copyCost', action: () => dispatch('copy-cost') },
|
||||
{ id: 'docs', labelKey: 'palette.openDocs', shortcut: 'F1', action: () => dispatch('docs') },
|
||||
{ id: 'theme', labelKey: 'palette.changeTheme', descKey: 'palette.changeThemeDesc', action: () => dispatch('theme') },
|
||||
{ id: 'split-h', labelKey: 'palette.splitH', shortcut: 'Ctrl+\\', action: () => dispatch('split-h') },
|
||||
{ id: 'split-v', labelKey: 'palette.splitV', shortcut: 'Ctrl+Shift+\\', action: () => dispatch('split-v') },
|
||||
{ id: 'focus-next', labelKey: 'palette.focusNext', shortcut: 'Ctrl+Tab', action: () => dispatch('focus-next') },
|
||||
{ id: 'focus-prev', labelKey: 'palette.focusPrev', shortcut: 'Ctrl+Shift+Tab', action: () => dispatch('focus-prev') },
|
||||
{ id: 'close-tab', labelKey: 'palette.closeTab', shortcut: 'Ctrl+W', action: () => dispatch('close-tab') },
|
||||
{ id: 'toggle-terminal', labelKey: 'palette.toggleTerminal', shortcut: 'Ctrl+J', action: () => dispatch('toggle-terminal') },
|
||||
{ id: 'reload-plugins', labelKey: 'palette.reloadPlugins', action: () => dispatch('reload-plugins') },
|
||||
{ id: 'toggle-sidebar', labelKey: 'palette.toggleSidebar', shortcut: 'Ctrl+B', action: () => dispatch('toggle-sidebar') },
|
||||
{ id: 'zoom-in', labelKey: 'palette.zoomIn', shortcut: 'Ctrl+=', action: () => dispatch('zoom-in') },
|
||||
{ id: 'zoom-out', labelKey: 'palette.zoomOut', shortcut: 'Ctrl+-', action: () => dispatch('zoom-out') },
|
||||
];
|
||||
|
||||
let COMMANDS = $derived<Command[]>(
|
||||
COMMAND_DEFS.map(d => ({
|
||||
id: d.id,
|
||||
label: t(d.labelKey as any),
|
||||
description: d.descKey ? t(d.descKey as any) : undefined,
|
||||
shortcut: d.shortcut,
|
||||
action: d.action,
|
||||
}))
|
||||
);
|
||||
|
||||
let query = $state('');
|
||||
let selectedIdx = $state(0);
|
||||
let inputEl = $state<HTMLInputElement | undefined>(undefined);
|
||||
|
|
@ -110,7 +130,7 @@
|
|||
class="palette-input"
|
||||
type="text"
|
||||
role="combobox"
|
||||
placeholder="Type a command..."
|
||||
placeholder={t('palette.placeholder')}
|
||||
bind:this={inputEl}
|
||||
bind:value={query}
|
||||
onkeydown={handleKeydown}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import KeyboardSettings from './settings/KeyboardSettings.svelte';
|
||||
import RemoteMachinesSettings from './settings/RemoteMachinesSettings.svelte';
|
||||
import DiagnosticsTab from './settings/DiagnosticsTab.svelte';
|
||||
import { t } from './i18n.svelte.ts';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
|
|
@ -25,19 +26,23 @@
|
|||
icon: string;
|
||||
}
|
||||
|
||||
const CATEGORIES: Category[] = [
|
||||
{ id: 'appearance', label: 'Appearance', icon: '🎨' },
|
||||
{ id: 'agents', label: 'Agents', icon: '🤖' },
|
||||
{ id: 'security', label: 'Security', icon: '🔒' },
|
||||
{ id: 'projects', label: 'Projects', icon: '📁' },
|
||||
{ id: 'orchestration', label: 'Orchestration', icon: '⚙' },
|
||||
{ id: 'machines', label: 'Machines', icon: '🖥' },
|
||||
{ id: 'keyboard', label: 'Keyboard', icon: '⌨' },
|
||||
{ id: 'advanced', label: 'Advanced', icon: '🔧' },
|
||||
{ id: 'marketplace', label: 'Marketplace', icon: '🛒' },
|
||||
{ id: 'diagnostics', label: 'Diagnostics', icon: '📊' },
|
||||
const CATEGORY_DEFS: Array<{ id: CategoryId; icon: string; key: string }> = [
|
||||
{ id: 'appearance', icon: '🎨', key: 'settings.appearance' },
|
||||
{ id: 'agents', icon: '🤖', key: 'settings.agents' },
|
||||
{ id: 'security', icon: '🔒', key: 'settings.security' },
|
||||
{ id: 'projects', icon: '📁', key: 'settings.projects' },
|
||||
{ id: 'orchestration', icon: '⚙', key: 'settings.orchestration' },
|
||||
{ id: 'machines', icon: '🖥', key: 'settings.machines' },
|
||||
{ id: 'keyboard', icon: '⌨', key: 'settings.keyboard' },
|
||||
{ id: 'advanced', icon: '🔧', key: 'settings.advanced' },
|
||||
{ id: 'marketplace', icon: '🛒', key: 'settings.marketplace' },
|
||||
{ id: 'diagnostics', icon: '📊', key: 'settings.diagnostics' },
|
||||
];
|
||||
|
||||
let CATEGORIES = $derived<Category[]>(
|
||||
CATEGORY_DEFS.map(d => ({ id: d.id, label: t(d.key as any), icon: d.icon }))
|
||||
);
|
||||
|
||||
let activeCategory = $state<CategoryId>('appearance');
|
||||
|
||||
function handleBackdropClick(e: MouseEvent) {
|
||||
|
|
@ -66,8 +71,8 @@
|
|||
|
||||
<!-- Header -->
|
||||
<header class="drawer-header">
|
||||
<h2 class="drawer-title">Settings</h2>
|
||||
<button class="drawer-close" onclick={onClose} aria-label="Close settings">×</button>
|
||||
<h2 class="drawer-title">{t('settings.title')}</h2>
|
||||
<button class="drawer-close" onclick={onClose} aria-label={t('settings.close')}>×</button>
|
||||
</header>
|
||||
|
||||
<!-- Body: sidebar + content -->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
* Auto-dismisses when the `ready` prop becomes true.
|
||||
* Fade-out transition: 300ms opacity.
|
||||
*/
|
||||
import { t } from './i18n.svelte.ts';
|
||||
|
||||
interface Props {
|
||||
/** Set to true when app initialization is complete. */
|
||||
|
|
@ -41,7 +42,7 @@
|
|||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
</div>
|
||||
<div class="loading-label">Loading...</div>
|
||||
<div class="loading-label">{t('splash.loading')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
153
ui-electrobun/src/mainview/i18n.svelte.ts
Normal file
153
ui-electrobun/src/mainview/i18n.svelte.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/**
|
||||
* i18n store — Svelte 5 runes + @formatjs/intl.
|
||||
*
|
||||
* Usage:
|
||||
* import { t, setLocale, getLocale, getDir, initI18n } from './i18n.svelte.ts';
|
||||
* const label = t('agent.status.running');
|
||||
* const msg = t('sidebar.notifCount', { count: 3 });
|
||||
*/
|
||||
|
||||
import { createIntl, createIntlCache, type IntlShape } from '@formatjs/intl';
|
||||
import type { TranslationKey } from './i18n.types';
|
||||
import { appRpc } from './rpc.ts';
|
||||
|
||||
// ── Locale metadata ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface LocaleMeta {
|
||||
tag: string;
|
||||
label: string;
|
||||
nativeLabel: string;
|
||||
dir: 'ltr' | 'rtl';
|
||||
}
|
||||
|
||||
export const AVAILABLE_LOCALES: LocaleMeta[] = [
|
||||
{ tag: 'en', label: 'English', nativeLabel: 'English', dir: 'ltr' },
|
||||
{ tag: 'pl', label: 'Polish', nativeLabel: 'Polski', dir: 'ltr' },
|
||||
{ tag: 'ar', label: 'Arabic', nativeLabel: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629', dir: 'rtl' },
|
||||
];
|
||||
|
||||
// ── Reactive state ───────────────────────────────────────────────────────────
|
||||
|
||||
let locale = $state<string>('en');
|
||||
/** Version counter — incremented on every setLocale() so $derived consumers re-evaluate. */
|
||||
let _v = $state(0);
|
||||
|
||||
// ── @formatjs/intl instance ──────────────────────────────────────────────────
|
||||
|
||||
const cache = createIntlCache();
|
||||
type Messages = Record<string, string>;
|
||||
|
||||
let _messages: Messages = {};
|
||||
let _intl: IntlShape<string> = createIntl({ locale: 'en', messages: {} }, cache);
|
||||
|
||||
// ── Locale loaders (dynamic import) ──────────────────────────────────────────
|
||||
|
||||
const loaders: Record<string, () => Promise<Messages>> = {
|
||||
en: () => import('../../locales/en.json').then(m => m.default as Messages),
|
||||
pl: () => import('../../locales/pl.json').then(m => m.default as Messages),
|
||||
ar: () => import('../../locales/ar.json').then(m => m.default as Messages),
|
||||
};
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Change the active locale. Dynamically loads the JSON message file,
|
||||
* recreates the intl instance, and bumps the reactivity version counter.
|
||||
*/
|
||||
export async function setLocale(tag: string): Promise<void> {
|
||||
const loader = loaders[tag];
|
||||
if (!loader) {
|
||||
console.warn(`[i18n] unknown locale: ${tag}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_messages = await loader();
|
||||
} catch (err) {
|
||||
console.error(`[i18n] failed to load locale "${tag}":`, err);
|
||||
return;
|
||||
}
|
||||
|
||||
locale = tag;
|
||||
_intl = createIntl({ locale: tag, messages: _messages }, cache);
|
||||
_v++;
|
||||
|
||||
// Persist preference
|
||||
try {
|
||||
await appRpc?.request['settings.set']({ key: 'locale', value: tag });
|
||||
} catch { /* non-critical */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate a message key, optionally with ICU values.
|
||||
* Reads `_v` to trigger Svelte 5 reactivity on locale change.
|
||||
*/
|
||||
export function t(key: TranslationKey, values?: Record<string, any>): string {
|
||||
// Touch reactive version so $derived consumers re-run.
|
||||
void _v;
|
||||
|
||||
const msg = _messages[key];
|
||||
if (!msg) return key;
|
||||
|
||||
try {
|
||||
return _intl.formatMessage({ id: key, defaultMessage: msg }, values);
|
||||
} catch (err) {
|
||||
console.warn(`[i18n] format error for "${key}":`, err);
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
/** Format a Date using the current locale. */
|
||||
export function formatDate(date: Date | number, options?: Intl.DateTimeFormatOptions): string {
|
||||
void _v;
|
||||
return _intl.formatDate(date, options);
|
||||
}
|
||||
|
||||
/** Format a number using the current locale. */
|
||||
export function formatNumber(num: number, options?: Intl.NumberFormatOptions): string {
|
||||
void _v;
|
||||
return _intl.formatNumber(num, options);
|
||||
}
|
||||
|
||||
/** Format a relative time (e.g. -5, 'minute' -> "5 minutes ago"). */
|
||||
export function formatRelativeTime(
|
||||
value: number,
|
||||
unit: Intl.RelativeTimeFormatUnit,
|
||||
options?: Parameters<IntlShape['formatRelativeTime']>[2],
|
||||
): string {
|
||||
void _v;
|
||||
return _intl.formatRelativeTime(value, unit, options);
|
||||
}
|
||||
|
||||
/** Current locale tag (e.g. 'en', 'pl', 'ar'). */
|
||||
export function getLocale(): string {
|
||||
void _v;
|
||||
return locale;
|
||||
}
|
||||
|
||||
/** Text direction for the current locale. */
|
||||
export function getDir(): 'ltr' | 'rtl' {
|
||||
void _v;
|
||||
const meta = AVAILABLE_LOCALES.find(l => l.tag === locale);
|
||||
return meta?.dir ?? 'ltr';
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize i18n on app startup.
|
||||
* Loads saved locale from settings, falls back to 'en'.
|
||||
*/
|
||||
export async function initI18n(): Promise<void> {
|
||||
// Load PluralRules polyfill for WebKitGTK (needed for Arabic 6-form plurals)
|
||||
await import('@formatjs/intl-pluralrules/polyfill-force.js').catch(() => {});
|
||||
|
||||
let savedLocale = 'en';
|
||||
|
||||
try {
|
||||
const result = await appRpc?.request['settings.get']({ key: 'locale' });
|
||||
if (result?.value && loaders[result.value]) {
|
||||
savedLocale = result.value;
|
||||
}
|
||||
} catch { /* use default */ }
|
||||
|
||||
await setLocale(savedLocale);
|
||||
}
|
||||
140
ui-electrobun/src/mainview/i18n.types.ts
Normal file
140
ui-electrobun/src/mainview/i18n.types.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/**
|
||||
* Auto-generated by scripts/i18n-types.ts — do not edit manually.
|
||||
* Run: bun scripts/i18n-types.ts
|
||||
*/
|
||||
|
||||
export type TranslationKey =
|
||||
| 'agent.contextMeter'
|
||||
| 'agent.prompt.placeholder'
|
||||
| 'agent.prompt.send'
|
||||
| 'agent.prompt.stop'
|
||||
| 'agent.status.done'
|
||||
| 'agent.status.error'
|
||||
| 'agent.status.idle'
|
||||
| 'agent.status.running'
|
||||
| 'agent.status.stalled'
|
||||
| 'agent.status.thinking'
|
||||
| 'agent.tokens'
|
||||
| 'agent.toolCall'
|
||||
| 'agent.toolResult'
|
||||
| 'common.add'
|
||||
| 'common.back'
|
||||
| 'common.cancel'
|
||||
| 'common.close'
|
||||
| 'common.confirm'
|
||||
| 'common.delete'
|
||||
| 'common.edit'
|
||||
| 'common.noItems'
|
||||
| 'common.refresh'
|
||||
| 'common.save'
|
||||
| 'comms.channels'
|
||||
| 'comms.directMessages'
|
||||
| 'comms.placeholder'
|
||||
| 'comms.sendMessage'
|
||||
| 'errors.connectionFailed'
|
||||
| 'errors.fileNotFound'
|
||||
| 'errors.generic'
|
||||
| 'errors.sessionExpired'
|
||||
| 'errors.unhandled'
|
||||
| 'files.empty'
|
||||
| 'files.modified'
|
||||
| 'files.open'
|
||||
| 'files.save'
|
||||
| 'files.saving'
|
||||
| 'files.tooLarge'
|
||||
| 'notifications.clearAll'
|
||||
| 'notifications.noNotifications'
|
||||
| 'notifications.title'
|
||||
| 'palette.addProject'
|
||||
| 'palette.addProjectDesc'
|
||||
| 'palette.changeTheme'
|
||||
| 'palette.changeThemeDesc'
|
||||
| 'palette.clearAgent'
|
||||
| 'palette.clearAgentDesc'
|
||||
| 'palette.closeTab'
|
||||
| 'palette.copyCost'
|
||||
| 'palette.focusNext'
|
||||
| 'palette.focusPrev'
|
||||
| 'palette.newTerminal'
|
||||
| 'palette.openDocs'
|
||||
| 'palette.openSettings'
|
||||
| 'palette.placeholder'
|
||||
| 'palette.reloadPlugins'
|
||||
| 'palette.searchMessages'
|
||||
| 'palette.splitH'
|
||||
| 'palette.splitV'
|
||||
| 'palette.title'
|
||||
| 'palette.toggleSidebar'
|
||||
| 'palette.toggleTerminal'
|
||||
| 'palette.zoomIn'
|
||||
| 'palette.zoomOut'
|
||||
| 'project.clone'
|
||||
| 'project.cloneBranch'
|
||||
| 'project.cwd'
|
||||
| 'project.deleteConfirm'
|
||||
| 'project.emptyGroup'
|
||||
| 'project.name'
|
||||
| 'search.noResults'
|
||||
| 'search.placeholder'
|
||||
| 'search.resultsCount'
|
||||
| 'search.searching'
|
||||
| 'settings.advanced'
|
||||
| 'settings.agents'
|
||||
| 'settings.appearance'
|
||||
| 'settings.close'
|
||||
| 'settings.cursorBlink'
|
||||
| 'settings.cursorOff'
|
||||
| 'settings.cursorOn'
|
||||
| 'settings.customTheme'
|
||||
| 'settings.deleteTheme'
|
||||
| 'settings.diagnostics'
|
||||
| 'settings.editTheme'
|
||||
| 'settings.keyboard'
|
||||
| 'settings.language'
|
||||
| 'settings.machines'
|
||||
| 'settings.marketplace'
|
||||
| 'settings.orchestration'
|
||||
| 'settings.projects'
|
||||
| 'settings.scrollback'
|
||||
| 'settings.scrollbackHint'
|
||||
| 'settings.security'
|
||||
| 'settings.termCursor'
|
||||
| 'settings.termFont'
|
||||
| 'settings.theme'
|
||||
| 'settings.title'
|
||||
| 'settings.uiFont'
|
||||
| 'sidebar.addGroup'
|
||||
| 'sidebar.addProject'
|
||||
| 'sidebar.close'
|
||||
| 'sidebar.groupName'
|
||||
| 'sidebar.maximize'
|
||||
| 'sidebar.minimize'
|
||||
| 'sidebar.notifCount'
|
||||
| 'sidebar.notifications'
|
||||
| 'sidebar.settings'
|
||||
| 'splash.loading'
|
||||
| 'statusbar.activeGroup'
|
||||
| 'statusbar.attention'
|
||||
| 'statusbar.burnRate'
|
||||
| 'statusbar.cost'
|
||||
| 'statusbar.idle'
|
||||
| 'statusbar.needsAttention'
|
||||
| 'statusbar.projects'
|
||||
| 'statusbar.running'
|
||||
| 'statusbar.search'
|
||||
| 'statusbar.session'
|
||||
| 'statusbar.stalled'
|
||||
| 'statusbar.tokens'
|
||||
| 'tasks.addTask'
|
||||
| 'tasks.blocked'
|
||||
| 'tasks.deleteTask'
|
||||
| 'tasks.done'
|
||||
| 'tasks.inProgress'
|
||||
| 'tasks.review'
|
||||
| 'tasks.taskCount'
|
||||
| 'tasks.todo'
|
||||
| 'terminal.addTab'
|
||||
| 'terminal.closeTab'
|
||||
| 'terminal.collapse'
|
||||
| 'terminal.expand'
|
||||
| 'terminal.shell';
|
||||
|
|
@ -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