feat: Implement internationalization (i18n) with English and Portuguese (pt-BR) locales and integrate into UI components.
This commit is contained in:
parent
9688454a30
commit
54900d6358
5
ui/src/i18n/index.ts
Normal file
5
ui/src/i18n/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './lib/types';
|
||||||
|
export * from './lib/translate';
|
||||||
|
export * from './lib/lit-controller';
|
||||||
|
export * from './locales/en';
|
||||||
|
export * from './locales/pt-BR';
|
||||||
22
ui/src/i18n/lib/lit-controller.ts
Normal file
22
ui/src/i18n/lib/lit-controller.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
||||||
|
import { i18n } from './translate';
|
||||||
|
|
||||||
|
export class I18nController implements ReactiveController {
|
||||||
|
host: ReactiveControllerHost;
|
||||||
|
private unsubscribe?: () => void;
|
||||||
|
|
||||||
|
constructor(host: ReactiveControllerHost) {
|
||||||
|
this.host = host;
|
||||||
|
this.host.addController(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
hostConnected() {
|
||||||
|
this.unsubscribe = i18n.subscribe(() => {
|
||||||
|
this.host.requestUpdate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hostDisconnected() {
|
||||||
|
this.unsubscribe?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
81
ui/src/i18n/lib/translate.ts
Normal file
81
ui/src/i18n/lib/translate.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import type { TranslationMap } from './types';
|
||||||
|
|
||||||
|
class Translator {
|
||||||
|
private locale: string = 'en';
|
||||||
|
private translations: Record<string, TranslationMap> = {};
|
||||||
|
private listeners: Set<() => void> = new Set();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Try to load saved locale
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('moltbot-locale');
|
||||||
|
if (saved) {
|
||||||
|
this.locale = saved;
|
||||||
|
} else if (navigator.language.startsWith('pt')) {
|
||||||
|
this.locale = 'pt-BR';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentLocale() {
|
||||||
|
return this.locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(cb: () => void) {
|
||||||
|
this.listeners.add(cb);
|
||||||
|
return () => this.listeners.delete(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
private notify() {
|
||||||
|
this.listeners.forEach(cb => cb());
|
||||||
|
}
|
||||||
|
|
||||||
|
setResources(locale: string, resources: TranslationMap) {
|
||||||
|
this.translations[locale] = resources;
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocale(locale: string) {
|
||||||
|
if (this.locale === locale) return;
|
||||||
|
this.locale = locale;
|
||||||
|
try {
|
||||||
|
localStorage.setItem('moltbot-locale', locale);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
this.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
t(key: string, params?: Record<string, string | number>): string {
|
||||||
|
const table = this.translations[this.locale];
|
||||||
|
|
||||||
|
let val = this.resolve(table, key);
|
||||||
|
|
||||||
|
if (val === undefined || val === null) {
|
||||||
|
// Fallback to EN if key missing in current
|
||||||
|
if (this.locale !== 'en') {
|
||||||
|
val = this.resolve(this.translations['en'], key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val === undefined || val === null) return key;
|
||||||
|
if (typeof val !== 'string') return key;
|
||||||
|
|
||||||
|
return this.interpolate(val, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolve(table: TranslationMap | undefined, key: string): any {
|
||||||
|
if (!table) return undefined;
|
||||||
|
return key.split('.').reduce((obj, i) => (obj ? obj[i] : null), table);
|
||||||
|
}
|
||||||
|
|
||||||
|
private interpolate(text: string, params?: Record<string, string | number>) {
|
||||||
|
if (!params) return text;
|
||||||
|
return text.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? `{${k}}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const i18n = new Translator();
|
||||||
|
export const t = i18n.t.bind(i18n);
|
||||||
1
ui/src/i18n/lib/types.ts
Normal file
1
ui/src/i18n/lib/types.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type TranslationMap = Record<string, any>; // Relaxed type for nested objects
|
||||||
98
ui/src/i18n/locales/en.ts
Normal file
98
ui/src/i18n/locales/en.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
export const en = {
|
||||||
|
brand: {
|
||||||
|
title: "MOLTBOT",
|
||||||
|
subtitle: "Gateway Dashboard"
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
expand: "Expand sidebar",
|
||||||
|
collapse: "Collapse sidebar"
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
health: "Health",
|
||||||
|
ok: "OK",
|
||||||
|
offline: "Offline",
|
||||||
|
connected: "Connected",
|
||||||
|
disconnected: "Disconnected"
|
||||||
|
},
|
||||||
|
nav: {
|
||||||
|
resources: "Resources",
|
||||||
|
docs: "Docs",
|
||||||
|
group: {
|
||||||
|
chat: "Chat",
|
||||||
|
control: "Control",
|
||||||
|
agent: "Agent",
|
||||||
|
settings: "Settings"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
title: {
|
||||||
|
overview: "Overview",
|
||||||
|
channels: "Channels",
|
||||||
|
instances: "Instances",
|
||||||
|
sessions: "Sessions",
|
||||||
|
cron: "Cron Jobs",
|
||||||
|
skills: "Skills",
|
||||||
|
nodes: "Nodes",
|
||||||
|
chat: "Chat",
|
||||||
|
config: "Config",
|
||||||
|
debug: "Debug",
|
||||||
|
logs: "Logs",
|
||||||
|
control: "Control"
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
overview: "Gateway status, entry points, and a fast health read.",
|
||||||
|
channels: "Manage channels and settings.",
|
||||||
|
instances: "Presence beacons from connected clients and nodes.",
|
||||||
|
sessions: "Inspect active sessions and adjust per-session defaults.",
|
||||||
|
cron: "Schedule wakeups and recurring agent runs.",
|
||||||
|
skills: "Manage skill availability and API key injection.",
|
||||||
|
nodes: "Paired devices, capabilities, and command exposure.",
|
||||||
|
chat: "Direct gateway chat session for quick interventions.",
|
||||||
|
config: "Edit ~/.clawdbot/moltbot.json safely.",
|
||||||
|
debug: "Gateway snapshots, events, and manual RPC calls.",
|
||||||
|
logs: "Live tail of the gateway file logs."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
overview: {
|
||||||
|
gateway_access: {
|
||||||
|
title: "Gateway Access",
|
||||||
|
sub: "Where the dashboard connects and how it authenticates."
|
||||||
|
},
|
||||||
|
snapshot: {
|
||||||
|
title: "Snapshot",
|
||||||
|
sub: "Latest gateway handshake information."
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
title: "Notes",
|
||||||
|
sub: "Quick reminders for remote control setups.",
|
||||||
|
tailscale: "Tailscale serve",
|
||||||
|
tailscale_sub: "Prefer serve mode to keep the gateway on loopback with tailnet auth.",
|
||||||
|
session: "Session hygiene",
|
||||||
|
session_sub: "Use /new or sessions.patch to reset context.",
|
||||||
|
cron: "Cron reminders",
|
||||||
|
cron_sub: "Use isolated sessions for recurring runs."
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
websocket: "WebSocket URL",
|
||||||
|
token: "Gateway Token",
|
||||||
|
password: "Password (not stored)",
|
||||||
|
session: "Default Session Key",
|
||||||
|
language: "Language"
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
uptime: "Uptime",
|
||||||
|
tick: "Tick Interval",
|
||||||
|
last_refresh: "Last Channels Refresh",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
connect: "Connect",
|
||||||
|
refresh: "Refresh"
|
||||||
|
},
|
||||||
|
hint: {
|
||||||
|
connect_apply: "Click Connect to apply connection changes.",
|
||||||
|
use_channels: "Use Channels to link WhatsApp, Telegram, Discord, Signal, or iMessage.",
|
||||||
|
auth_failed: "Auth failed. Re-copy a tokenized URL with `moltbot dashboard --no-open`, or update the token, then click Connect.",
|
||||||
|
https_required: "This page is HTTP, so the browser blocks device identity. Use HTTPS (Tailscale Serve) or open http://127.0.0.1:18789 on the gateway host."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
98
ui/src/i18n/locales/pt-BR.ts
Normal file
98
ui/src/i18n/locales/pt-BR.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
export const ptBR = {
|
||||||
|
brand: {
|
||||||
|
title: "MOLTBOT",
|
||||||
|
subtitle: "Painel do Gateway"
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
expand: "Expandir barra lateral",
|
||||||
|
collapse: "Recolher barra lateral"
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
health: "Saúde",
|
||||||
|
ok: "OK",
|
||||||
|
offline: "Offline",
|
||||||
|
connected: "Conectado",
|
||||||
|
disconnected: "Desconectado"
|
||||||
|
},
|
||||||
|
nav: {
|
||||||
|
resources: "Recursos",
|
||||||
|
docs: "Documentação",
|
||||||
|
group: {
|
||||||
|
chat: "Chat",
|
||||||
|
control: "Controle",
|
||||||
|
agent: "Agente",
|
||||||
|
settings: "Configurações"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
title: {
|
||||||
|
overview: "Visão Geral",
|
||||||
|
channels: "Canais",
|
||||||
|
instances: "Instâncias",
|
||||||
|
sessions: "Sessões",
|
||||||
|
cron: "Tarefas Cron",
|
||||||
|
skills: "Skills",
|
||||||
|
nodes: "Nós",
|
||||||
|
chat: "Chat",
|
||||||
|
config: "Configuração",
|
||||||
|
debug: "Depuração",
|
||||||
|
logs: "Logs",
|
||||||
|
control: "Controle"
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
overview: "Status do gateway e leitura rápida de saúde.",
|
||||||
|
channels: "Gerenciar canais e configurações.",
|
||||||
|
instances: "Beacons de presença de clientes e nós conectados.",
|
||||||
|
sessions: "Inspecionar sessões ativas e ajustar padrões.",
|
||||||
|
cron: "Agendar despertares e execuções recorrentes de agentes.",
|
||||||
|
skills: "Gerenciar disponibilidade de skills e chaves de API.",
|
||||||
|
nodes: "Dispositivos pareados, capacidades e exposição de comandos.",
|
||||||
|
chat: "Sessão de chat direta com o gateway para intervenções rápidas.",
|
||||||
|
config: "Editar ~/.clawdbot/moltbot.json com segurança.",
|
||||||
|
debug: "Snapshots do gateway, eventos e chamadas RPC manuais.",
|
||||||
|
logs: "Acompanhamento em tempo real dos logs do gateway."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
overview: {
|
||||||
|
gateway_access: {
|
||||||
|
title: "Acesso ao Gateway",
|
||||||
|
sub: "Onde o painel se conecta e como se autentica."
|
||||||
|
},
|
||||||
|
snapshot: {
|
||||||
|
title: "Snapshot",
|
||||||
|
sub: "Informações mais recentes do handshake do gateway."
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
title: "Notas",
|
||||||
|
sub: "Lembretes rápidos para configurações de controle remoto.",
|
||||||
|
tailscale: "Tailscale serve",
|
||||||
|
tailscale_sub: "Prefira o modo serve para manter o gateway em loopback com autenticação tailnet.",
|
||||||
|
session: "Higiene de sessão",
|
||||||
|
session_sub: "Use /new ou sessions.patch para redefinir o contexto.",
|
||||||
|
cron: "Lembretes do Cron",
|
||||||
|
cron_sub: "Use sessões isoladas para execuções recorrentes."
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
websocket: "URL do WebSocket",
|
||||||
|
token: "Token do Gateway",
|
||||||
|
password: "Senha (não armazenada)",
|
||||||
|
session: "Chave de Sessão Padrão",
|
||||||
|
language: "Idioma"
|
||||||
|
},
|
||||||
|
stats: {
|
||||||
|
uptime: "Tempo de Atividade",
|
||||||
|
tick: "Intervalo de Tick",
|
||||||
|
last_refresh: "Última Atualização de Canais",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
connect: "Conectar",
|
||||||
|
refresh: "Atualizar"
|
||||||
|
},
|
||||||
|
hint: {
|
||||||
|
connect_apply: "Clique em Conectar para aplicar as alterações.",
|
||||||
|
use_channels: "Use Canais para vincular WhatsApp, Telegram, Discord, Signal ou iMessage.",
|
||||||
|
auth_failed: "Falha na autenticação. Copie novamente uma URL tokenizada com `moltbot dashboard --no-open`, ou atualize o token, depois clique em Conectar.",
|
||||||
|
https_required: "Esta página é HTTP, então o navegador bloqueia a identidade do dispositivo. Use HTTPS (Tailscale Serve) ou abra http://127.0.0.1:18789 no host do gateway."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
46
ui/src/i18n/test/translate.test.ts
Normal file
46
ui/src/i18n/test/translate.test.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { i18n } from '../lib/translate';
|
||||||
|
|
||||||
|
describe('i18n', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
i18n.setResources('en', {
|
||||||
|
hello: 'Hello World',
|
||||||
|
greeting: 'Hello {name}',
|
||||||
|
nested: {
|
||||||
|
key: 'Nested Value'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
i18n.setResources('pt-BR', {
|
||||||
|
hello: 'Olá Mundo',
|
||||||
|
greeting: 'Olá {name}'
|
||||||
|
});
|
||||||
|
i18n.setLocale('en');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('translates simple keys', () => {
|
||||||
|
expect(i18n.t('hello')).toBe('Hello World');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('translates with interpolation', () => {
|
||||||
|
expect(i18n.t('greeting', { name: 'Alice' })).toBe('Hello Alice');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles nested keys', () => {
|
||||||
|
expect(i18n.t('nested.key')).toBe('Nested Value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('switches locales', () => {
|
||||||
|
i18n.setLocale('pt-BR');
|
||||||
|
expect(i18n.t('hello')).toBe('Olá Mundo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fallbacks to en', () => {
|
||||||
|
i18n.setLocale('pt-BR');
|
||||||
|
// 'nested.key' is not in pt-BR
|
||||||
|
expect(i18n.t('nested.key')).toBe('Nested Value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns key if missing', () => {
|
||||||
|
expect(i18n.t('missing.key')).toBe('missing.key');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -81,6 +81,7 @@ import {
|
|||||||
import { loadCronRuns, toggleCronJob, runCronJob, removeCronJob, addCronJob } from "./controllers/cron";
|
import { loadCronRuns, toggleCronJob, runCronJob, removeCronJob, addCronJob } from "./controllers/cron";
|
||||||
import { loadDebug, callDebugMethod } from "./controllers/debug";
|
import { loadDebug, callDebugMethod } from "./controllers/debug";
|
||||||
import { loadLogs } from "./controllers/logs";
|
import { loadLogs } from "./controllers/logs";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
|
||||||
const AVATAR_DATA_RE = /^data:/i;
|
const AVATAR_DATA_RE = /^data:/i;
|
||||||
const AVATAR_HTTP_RE = /^https?:\/\//i;
|
const AVATAR_HTTP_RE = /^https?:\/\//i;
|
||||||
@ -104,7 +105,7 @@ export function renderApp(state: AppViewState) {
|
|||||||
const presenceCount = state.presenceEntries.length;
|
const presenceCount = state.presenceEntries.length;
|
||||||
const sessionsCount = state.sessionsResult?.count ?? null;
|
const sessionsCount = state.sessionsResult?.count ?? null;
|
||||||
const cronNext = state.cronStatus?.nextWakeAtMs ?? null;
|
const cronNext = state.cronStatus?.nextWakeAtMs ?? null;
|
||||||
const chatDisabledReason = state.connected ? null : "Disconnected from gateway.";
|
const chatDisabledReason = state.connected ? null : t("status.disconnected") + " from gateway.";
|
||||||
const isChat = state.tab === "chat";
|
const isChat = state.tab === "chat";
|
||||||
const chatFocus = isChat && (state.settings.chatFocusMode || state.onboarding);
|
const chatFocus = isChat && (state.settings.chatFocusMode || state.onboarding);
|
||||||
const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
|
const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
|
||||||
@ -122,8 +123,15 @@ export function renderApp(state: AppViewState) {
|
|||||||
...state.settings,
|
...state.settings,
|
||||||
navCollapsed: !state.settings.navCollapsed,
|
navCollapsed: !state.settings.navCollapsed,
|
||||||
})}
|
})}
|
||||||
title="${state.settings.navCollapsed ? "Expand sidebar" : "Collapse sidebar"}"
|
@click=${() =>
|
||||||
aria-label="${state.settings.navCollapsed ? "Expand sidebar" : "Collapse sidebar"}"
|
state.applySettings({
|
||||||
|
...state.settings,
|
||||||
|
navCollapsed: !state.settings.navCollapsed,
|
||||||
|
})}
|
||||||
|
title="${state.settings.navCollapsed ? t("sidebar.expand") : t("sidebar.collapse")}"
|
||||||
|
aria-label="${state.settings.navCollapsed ? t("sidebar.expand") : t("sidebar.collapse")}"
|
||||||
|
>
|
||||||
|
<span class="nav-collapse-toggle__icon">${icons.menu}</span>
|
||||||
>
|
>
|
||||||
<span class="nav-collapse-toggle__icon">${icons.menu}</span>
|
<span class="nav-collapse-toggle__icon">${icons.menu}</span>
|
||||||
</button>
|
</button>
|
||||||
@ -132,16 +140,21 @@ export function renderApp(state: AppViewState) {
|
|||||||
<img src="https://mintcdn.com/clawdhub/4rYvG-uuZrMK_URE/assets/pixel-lobster.svg?fit=max&auto=format&n=4rYvG-uuZrMK_URE&q=85&s=da2032e9eac3b5d9bfe7eb96ca6a8a26" alt="Moltbot" />
|
<img src="https://mintcdn.com/clawdhub/4rYvG-uuZrMK_URE/assets/pixel-lobster.svg?fit=max&auto=format&n=4rYvG-uuZrMK_URE&q=85&s=da2032e9eac3b5d9bfe7eb96ca6a8a26" alt="Moltbot" />
|
||||||
</div>
|
</div>
|
||||||
<div class="brand-text">
|
<div class="brand-text">
|
||||||
<div class="brand-title">MOLTBOT</div>
|
<div class="brand-text">
|
||||||
<div class="brand-sub">Gateway Dashboard</div>
|
<div class="brand-title">${t("brand.title")}</div>
|
||||||
|
<div class="brand-sub">${t("brand.subtitle")}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="topbar-status">
|
<div class="topbar-status">
|
||||||
<div class="pill">
|
<div class="pill">
|
||||||
<span class="statusDot ${state.connected ? "ok" : ""}"></span>
|
<span class="statusDot ${state.connected ? "ok" : ""}"></span>
|
||||||
<span>Health</span>
|
<div class="pill">
|
||||||
<span class="mono">${state.connected ? "OK" : "Offline"}</span>
|
<span class="statusDot ${state.connected ? "ok" : ""}"></span>
|
||||||
|
<span>${t("status.health")}</span>
|
||||||
|
<span class="mono">${state.connected ? t("status.ok") : t("status.offline")}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${renderThemeToggle(state)}
|
${renderThemeToggle(state)}
|
||||||
</div>
|
</div>
|
||||||
@ -175,7 +188,11 @@ export function renderApp(state: AppViewState) {
|
|||||||
})}
|
})}
|
||||||
<div class="nav-group nav-group--links">
|
<div class="nav-group nav-group--links">
|
||||||
<div class="nav-label nav-label--static">
|
<div class="nav-label nav-label--static">
|
||||||
<span class="nav-label__text">Resources</span>
|
<div class="nav-group nav-group--links">
|
||||||
|
<div class="nav-label nav-label--static">
|
||||||
|
<span class="nav-label__text">${t("nav.resources")}</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-group__items">
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-group__items">
|
<div class="nav-group__items">
|
||||||
<a
|
<a
|
||||||
@ -186,7 +203,11 @@ export function renderApp(state: AppViewState) {
|
|||||||
title="Docs (opens in new tab)"
|
title="Docs (opens in new tab)"
|
||||||
>
|
>
|
||||||
<span class="nav-item__icon" aria-hidden="true">${icons.book}</span>
|
<span class="nav-item__icon" aria-hidden="true">${icons.book}</span>
|
||||||
<span class="nav-item__text">Docs</span>
|
title="${t("nav.docs")} (opens in new tab)"
|
||||||
|
>
|
||||||
|
<span class="nav-item__icon" aria-hidden="true">${icons.book}</span>
|
||||||
|
<span class="nav-item__text">${t("nav.docs")}</span>
|
||||||
|
</a>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -78,6 +78,7 @@ import {
|
|||||||
} from "./app-channels";
|
} from "./app-channels";
|
||||||
import type { NostrProfileFormState } from "./views/channels.nostr-profile-form";
|
import type { NostrProfileFormState } from "./views/channels.nostr-profile-form";
|
||||||
import { loadAssistantIdentity as loadAssistantIdentityInternal } from "./controllers/assistant-identity";
|
import { loadAssistantIdentity as loadAssistantIdentityInternal } from "./controllers/assistant-identity";
|
||||||
|
import { i18n, en, ptBR, I18nController } from "../i18n";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -111,6 +112,7 @@ export class MoltbotApp extends LitElement {
|
|||||||
private eventLogBuffer: EventLogEntry[] = [];
|
private eventLogBuffer: EventLogEntry[] = [];
|
||||||
private toolStreamSyncTimer: number | null = null;
|
private toolStreamSyncTimer: number | null = null;
|
||||||
private sidebarCloseTimer: number | null = null;
|
private sidebarCloseTimer: number | null = null;
|
||||||
|
private i18n = new I18nController(this);
|
||||||
|
|
||||||
@state() assistantName = injectedAssistantIdentity.name;
|
@state() assistantName = injectedAssistantIdentity.name;
|
||||||
@state() assistantAvatar = injectedAssistantIdentity.avatar;
|
@state() assistantAvatar = injectedAssistantIdentity.avatar;
|
||||||
@ -272,6 +274,9 @@ export class MoltbotApp extends LitElement {
|
|||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
i18n.setResources('en', en);
|
||||||
|
i18n.setResources('pt-BR', ptBR);
|
||||||
|
i18n.setLocale(this.settings.locale || 'en');
|
||||||
handleConnected(this as unknown as Parameters<typeof handleConnected>[0]);
|
handleConnected(this as unknown as Parameters<typeof handleConnected>[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,6 +337,9 @@ export class MoltbotApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
applySettings(next: UiSettings) {
|
applySettings(next: UiSettings) {
|
||||||
|
if (next.locale !== this.settings.locale) {
|
||||||
|
i18n.setLocale(next.locale);
|
||||||
|
}
|
||||||
applySettingsInternal(
|
applySettingsInternal(
|
||||||
this as unknown as Parameters<typeof applySettingsInternal>[0],
|
this as unknown as Parameters<typeof applySettingsInternal>[0],
|
||||||
next,
|
next,
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import type { IconName } from "./icons.js";
|
import type { IconName } from "./icons.js";
|
||||||
|
import { t } from "../i18n";
|
||||||
|
|
||||||
export const TAB_GROUPS = [
|
export const TAB_GROUPS = [
|
||||||
{ label: "Chat", tabs: ["chat"] },
|
{ label: "chat", tabs: ["chat"] },
|
||||||
{
|
{
|
||||||
label: "Control",
|
label: "control",
|
||||||
tabs: ["overview", "channels", "instances", "sessions", "cron"],
|
tabs: ["overview", "channels", "instances", "sessions", "cron"],
|
||||||
},
|
},
|
||||||
{ label: "Agent", tabs: ["skills", "nodes"] },
|
{ label: "agent", tabs: ["skills", "nodes"] },
|
||||||
{ label: "Settings", tabs: ["config", "debug", "logs"] },
|
{ label: "settings", tabs: ["config", "debug", "logs"] },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type Tab =
|
export type Tab =
|
||||||
@ -132,56 +133,56 @@ export function iconForTab(tab: Tab): IconName {
|
|||||||
export function titleForTab(tab: Tab) {
|
export function titleForTab(tab: Tab) {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case "overview":
|
case "overview":
|
||||||
return "Overview";
|
return t("tab.title.overview");
|
||||||
case "channels":
|
case "channels":
|
||||||
return "Channels";
|
return t("tab.title.channels");
|
||||||
case "instances":
|
case "instances":
|
||||||
return "Instances";
|
return t("tab.title.instances");
|
||||||
case "sessions":
|
case "sessions":
|
||||||
return "Sessions";
|
return t("tab.title.sessions");
|
||||||
case "cron":
|
case "cron":
|
||||||
return "Cron Jobs";
|
return t("tab.title.cron");
|
||||||
case "skills":
|
case "skills":
|
||||||
return "Skills";
|
return t("tab.title.skills");
|
||||||
case "nodes":
|
case "nodes":
|
||||||
return "Nodes";
|
return t("tab.title.nodes");
|
||||||
case "chat":
|
case "chat":
|
||||||
return "Chat";
|
return t("tab.title.chat");
|
||||||
case "config":
|
case "config":
|
||||||
return "Config";
|
return t("tab.title.config");
|
||||||
case "debug":
|
case "debug":
|
||||||
return "Debug";
|
return t("tab.title.debug");
|
||||||
case "logs":
|
case "logs":
|
||||||
return "Logs";
|
return t("tab.title.logs");
|
||||||
default:
|
default:
|
||||||
return "Control";
|
return t("tab.title.control");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subtitleForTab(tab: Tab) {
|
export function subtitleForTab(tab: Tab) {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case "overview":
|
case "overview":
|
||||||
return "Gateway status, entry points, and a fast health read.";
|
return t("tab.subtitle.overview");
|
||||||
case "channels":
|
case "channels":
|
||||||
return "Manage channels and settings.";
|
return t("tab.subtitle.channels");
|
||||||
case "instances":
|
case "instances":
|
||||||
return "Presence beacons from connected clients and nodes.";
|
return t("tab.subtitle.instances");
|
||||||
case "sessions":
|
case "sessions":
|
||||||
return "Inspect active sessions and adjust per-session defaults.";
|
return t("tab.subtitle.sessions");
|
||||||
case "cron":
|
case "cron":
|
||||||
return "Schedule wakeups and recurring agent runs.";
|
return t("tab.subtitle.cron");
|
||||||
case "skills":
|
case "skills":
|
||||||
return "Manage skill availability and API key injection.";
|
return t("tab.subtitle.skills");
|
||||||
case "nodes":
|
case "nodes":
|
||||||
return "Paired devices, capabilities, and command exposure.";
|
return t("tab.subtitle.nodes");
|
||||||
case "chat":
|
case "chat":
|
||||||
return "Direct gateway chat session for quick interventions.";
|
return t("tab.subtitle.chat");
|
||||||
case "config":
|
case "config":
|
||||||
return "Edit ~/.clawdbot/moltbot.json safely.";
|
return t("tab.subtitle.config");
|
||||||
case "debug":
|
case "debug":
|
||||||
return "Gateway snapshots, events, and manual RPC calls.";
|
return t("tab.subtitle.debug");
|
||||||
case "logs":
|
case "logs":
|
||||||
return "Live tail of the gateway file logs.";
|
return t("tab.subtitle.logs");
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export type UiSettings = {
|
|||||||
splitRatio: number; // Sidebar split ratio (0.4 to 0.7, default 0.6)
|
splitRatio: number; // Sidebar split ratio (0.4 to 0.7, default 0.6)
|
||||||
navCollapsed: boolean; // Collapsible sidebar state
|
navCollapsed: boolean; // Collapsible sidebar state
|
||||||
navGroupsCollapsed: Record<string, boolean>; // Which nav groups are collapsed
|
navGroupsCollapsed: Record<string, boolean>; // Which nav groups are collapsed
|
||||||
|
locale: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function loadSettings(): UiSettings {
|
export function loadSettings(): UiSettings {
|
||||||
@ -32,6 +33,7 @@ export function loadSettings(): UiSettings {
|
|||||||
splitRatio: 0.6,
|
splitRatio: 0.6,
|
||||||
navCollapsed: false,
|
navCollapsed: false,
|
||||||
navGroupsCollapsed: {},
|
navGroupsCollapsed: {},
|
||||||
|
locale: "en",
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -84,6 +86,7 @@ export function loadSettings(): UiSettings {
|
|||||||
parsed.navGroupsCollapsed !== null
|
parsed.navGroupsCollapsed !== null
|
||||||
? parsed.navGroupsCollapsed
|
? parsed.navGroupsCollapsed
|
||||||
: defaults.navGroupsCollapsed,
|
: defaults.navGroupsCollapsed,
|
||||||
|
locale: typeof parsed.locale === "string" ? parsed.locale : defaults.locale,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return defaults;
|
return defaults;
|
||||||
|
|||||||
@ -157,7 +157,6 @@ export function renderOverview(props: OverviewProps) {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Default Session Key</span>
|
|
||||||
<input
|
<input
|
||||||
.value=${props.settings.sessionKey}
|
.value=${props.settings.sessionKey}
|
||||||
@input=${(e: Event) => {
|
@input=${(e: Event) => {
|
||||||
@ -166,6 +165,20 @@ export function renderOverview(props: OverviewProps) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Language</span>
|
||||||
|
<select
|
||||||
|
style="padding: 8px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg-surface); color: var(--text-main);"
|
||||||
|
.value=${props.settings.locale}
|
||||||
|
@change=${(e: Event) => {
|
||||||
|
const v = (e.target as HTMLSelectElement).value;
|
||||||
|
props.onSettingsChange({ ...props.settings, locale: v });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="pt-BR">Português (Brasil)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="margin-top: 14px;">
|
<div class="row" style="margin-top: 14px;">
|
||||||
<button class="btn" @click=${() => props.onConnect()}>Connect</button>
|
<button class="btn" @click=${() => props.onConnect()}>Connect</button>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user