feat(ui): translate config, cron, skills, debug views to Chinese

- Update config.ts with i18n translations for sidebar, actions, forms
- Update cron.ts with translations for scheduler, job form, history
- Update skills.ts with translations for skill list, filters, actions
- Update debug.ts with translations for snapshots, RPC, events
- Add missing translation keys to en-US and zh-TW locale files
This commit is contained in:
Claude 2026-01-28 23:08:15 +00:00
parent a2497ecaab
commit eccf005580
No known key found for this signature in database
6 changed files with 279 additions and 136 deletions

View File

@ -304,8 +304,8 @@ export const enUS = {
cron: {
title: "Cron Jobs",
desc: "Scheduled agent wakeups and recurring tasks.",
noJobs: "No cron jobs configured.",
addJob: "Add Job",
noJobs: "No jobs yet.",
addJob: "Add job",
runNow: "Run",
remove: "Remove",
enable: "Enable",
@ -314,6 +314,57 @@ export const enUS = {
lastRun: "Last run",
nextRun: "Next run",
// Scheduler card
scheduler: "Scheduler",
schedulerDesc: "Gateway-owned cron scheduler status.",
jobs: "Jobs",
// New job form
newJob: "New Job",
newJobDesc: "Create a scheduled wakeup or agent run.",
name: "Name",
description: "Description",
agentId: "Agent ID",
agentIdPlaceholder: "default",
scheduleKind: "Schedule",
everyLabel: "Every",
atLabel: "At",
cronLabel: "Cron",
runAt: "Run at",
every: "Every",
unit: "Unit",
minutes: "Minutes",
hours: "Hours",
days: "Days",
expression: "Expression",
timezone: "Timezone (optional)",
session: "Session",
main: "Main",
isolated: "Isolated",
wakeMode: "Wake mode",
nextHeartbeat: "Next heartbeat",
now: "Now",
payload: "Payload",
systemEvent: "System event",
agentTurn: "Agent turn",
systemText: "System text",
agentMessage: "Agent message",
deliver: "Deliver",
to: "To",
toPlaceholder: "+1555… or chat id",
timeoutSeconds: "Timeout (seconds)",
postToMainPrefix: "Post to main prefix",
// Jobs list
jobsList: "Jobs",
jobsListDesc: "All scheduled jobs stored in the gateway.",
// Run history
runHistory: "Run history",
runHistoryDesc: "Latest runs for",
selectJob: "Select a job to inspect run history.",
noRuns: "No runs yet.",
form: {
schedule: "Schedule (cron)",
message: "Message",
@ -336,8 +387,9 @@ export const enUS = {
desc: "Manage bundled and installed skills.",
noSkills: "No skills found.",
filter: "Filter skills",
apiKey: "API Key",
saveKey: "Save Key",
shown: "shown",
apiKey: "API key",
saveKey: "Save key",
install: "Install",
installing: "Installing…",
enabled: "Enabled",
@ -345,6 +397,11 @@ export const enUS = {
toggle: "Toggle",
keySaved: "API key saved",
keyError: "Failed to save API key",
eligible: "eligible",
blocked: "blocked",
missing: "Missing",
reason: "Reason",
blockedByAllowlist: "blocked by allowlist",
},
// Nodes page
@ -427,14 +484,29 @@ export const enUS = {
status: "Status",
health: "Health",
models: "Models",
modelsDesc: "Catalog from models.list.",
heartbeat: "Heartbeat",
lastHeartbeat: "Last heartbeat",
events: "Events",
eventLog: "Event Log",
eventLogDesc: "Latest gateway events.",
noEvents: "No events yet.",
rpcCall: "RPC Call",
manualRpc: "Manual RPC",
manualRpcDesc: "Send a raw gateway method with JSON params.",
method: "Method",
params: "Params",
paramsJson: "Params (JSON)",
call: "Call",
result: "Result",
noResult: "No result yet.",
snapshots: "Snapshots",
snapshotsDesc: "Status, health, and heartbeat data.",
securityAudit: "Security audit",
critical: "critical",
warnings: "warnings",
noCritical: "No critical issues",
runAuditCmd: "Run for details.",
},
// Logs page

View File

@ -311,7 +311,7 @@ export const zhTW = {
cron: {
title: "排程任務",
desc: "排程代理喚醒與週期性任務。",
noJobs: "尚未設定排程任務。",
noJobs: "尚任務。",
addJob: "新增任務",
runNow: "立即執行",
remove: "移除",
@ -321,6 +321,57 @@ export const zhTW = {
lastRun: "上次執行",
nextRun: "下次執行",
// 排程器卡片
scheduler: "排程器",
schedulerDesc: "閘道器所管理的排程器狀態。",
jobs: "任務數",
// 新任務表單
newJob: "新增任務",
newJobDesc: "建立排程喚醒或代理執行。",
name: "名稱",
description: "描述",
agentId: "代理 ID",
agentIdPlaceholder: "default",
scheduleKind: "排程類型",
everyLabel: "間隔",
atLabel: "指定時間",
cronLabel: "Cron",
runAt: "執行時間",
every: "每隔",
unit: "單位",
minutes: "分鐘",
hours: "小時",
days: "天",
expression: "運算式",
timezone: "時區(選填)",
session: "工作階段",
main: "主要",
isolated: "獨立",
wakeMode: "喚醒模式",
nextHeartbeat: "下次心跳",
now: "立即",
payload: "內容類型",
systemEvent: "系統事件",
agentTurn: "代理回合",
systemText: "系統文字",
agentMessage: "代理訊息",
deliver: "傳送",
to: "收件者",
toPlaceholder: "+1555… 或聊天 ID",
timeoutSeconds: "逾時(秒)",
postToMainPrefix: "發送至主工作階段前綴",
// 任務列表
jobsList: "任務",
jobsListDesc: "所有儲存在閘道器的排程任務。",
// 執行歷史
runHistory: "執行歷史",
runHistoryDesc: "最近的執行記錄:",
selectJob: "選擇任務以檢視執行歷史。",
noRuns: "尚無執行記錄。",
form: {
schedule: "排程cron 格式)",
message: "訊息",
@ -343,6 +394,7 @@ export const zhTW = {
desc: "管理內建與已安裝的技能。",
noSkills: "找不到技能。",
filter: "篩選技能",
shown: "個顯示中",
apiKey: "API 金鑰",
saveKey: "儲存金鑰",
install: "安裝",
@ -352,6 +404,11 @@ export const zhTW = {
toggle: "切換",
keySaved: "API 金鑰已儲存",
keyError: "儲存 API 金鑰失敗",
eligible: "可用",
blocked: "已封鎖",
missing: "缺少",
reason: "原因",
blockedByAllowlist: "被許可清單封鎖",
},
// 節點頁面
@ -434,14 +491,29 @@ export const zhTW = {
status: "狀態",
health: "健康狀態",
models: "模型",
modelsDesc: "來自 models.list 的目錄。",
heartbeat: "心跳",
lastHeartbeat: "上次心跳",
events: "事件",
eventLog: "事件日誌",
eventLogDesc: "最新的閘道器事件。",
noEvents: "尚無事件。",
rpcCall: "RPC 呼叫",
manualRpc: "手動 RPC",
manualRpcDesc: "使用 JSON 參數發送原始閘道器方法。",
method: "方法",
params: "參數",
paramsJson: "參數JSON",
call: "呼叫",
result: "結果",
noResult: "尚無結果。",
snapshots: "快照",
snapshotsDesc: "狀態、健康狀態與心跳資料。",
securityAudit: "安全稽核",
critical: "嚴重",
warnings: "警告",
noCritical: "無嚴重問題",
runAuditCmd: "執行以檢視詳情。",
},
// 日誌頁面

View File

@ -1,4 +1,5 @@
import { html, nothing } from "lit";
import { t } from "../../i18n";
import type { ConfigUiHints } from "../types";
import { analyzeConfigSchema, renderConfigForm, SECTION_META } from "./config-form";
import {
@ -73,21 +74,15 @@ const sidebarIcons = {
default: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>`,
};
// Section definitions
const SECTIONS: Array<{ key: string; label: string }> = [
{ key: "env", label: "Environment" },
{ key: "update", label: "Updates" },
{ key: "agents", label: "Agents" },
{ key: "auth", label: "Authentication" },
{ key: "channels", label: "Channels" },
{ key: "messages", label: "Messages" },
{ key: "commands", label: "Commands" },
{ key: "hooks", label: "Hooks" },
{ key: "skills", label: "Skills" },
{ key: "tools", label: "Tools" },
{ key: "gateway", label: "Gateway" },
{ key: "wizard", label: "Setup Wizard" },
];
// Section definitions - labels are resolved via t() at render time
const SECTION_KEYS = [
"env", "update", "agents", "auth", "channels", "messages",
"commands", "hooks", "skills", "tools", "gateway", "wizard",
] as const;
function getSectionLabel(key: string): string {
return t(`config.sections.${key}`) || humanize(key);
}
type SubsectionEntry = {
key: string;
@ -191,13 +186,15 @@ export function renderConfig(props: ConfigProps) {
// Get available sections from schema
const schemaProps = analysis.schema?.properties ?? {};
const availableSections = SECTIONS.filter(s => s.key in schemaProps);
const knownKeys = new Set(SECTION_KEYS);
const availableSections = SECTION_KEYS
.filter(k => k in schemaProps)
.map(k => ({ key: k, label: getSectionLabel(k) }));
// Add any sections in schema but not in our list
const knownKeys = new Set(SECTIONS.map(s => s.key));
const extraSections = Object.keys(schemaProps)
.filter(k => !knownKeys.has(k))
.map(k => ({ key: k, label: k.charAt(0).toUpperCase() + k.slice(1) }));
.map(k => ({ key: k, label: getSectionLabel(k) }));
const allSections = [...availableSections, ...extraSections];
@ -255,8 +252,8 @@ export function renderConfig(props: ConfigProps) {
<!-- Sidebar -->
<aside class="config-sidebar">
<div class="config-sidebar__header">
<div class="config-sidebar__title">Settings</div>
<span class="pill pill--sm ${validity === "valid" ? "pill--ok" : validity === "invalid" ? "pill--danger" : ""}">${validity}</span>
<div class="config-sidebar__title">${t("config.title")}</div>
<span class="pill pill--sm ${validity === "valid" ? "pill--ok" : validity === "invalid" ? "pill--danger" : ""}">${t(`config.${validity}`)}</span>
</div>
<!-- Search -->
@ -268,7 +265,7 @@ export function renderConfig(props: ConfigProps) {
<input
type="text"
class="config-search__input"
placeholder="Search settings..."
placeholder=${t("config.searchSettings")}
.value=${props.searchQuery}
@input=${(e: Event) => props.onSearchChange((e.target as HTMLInputElement).value)}
/>
@ -287,7 +284,7 @@ export function renderConfig(props: ConfigProps) {
@click=${() => props.onSectionChange(null)}
>
<span class="config-nav__icon">${sidebarIcons.all}</span>
<span class="config-nav__label">All Settings</span>
<span class="config-nav__label">${t("config.allSettings")}</span>
</button>
${allSections.map(section => html`
<button
@ -308,13 +305,13 @@ export function renderConfig(props: ConfigProps) {
?disabled=${props.schemaLoading || !props.schema}
@click=${() => props.onFormModeChange("form")}
>
Form
${t("config.form")}
</button>
<button
class="config-mode-toggle__btn ${props.formMode === "raw" ? "active" : ""}"
@click=${() => props.onFormModeChange("raw")}
>
Raw
${t("config.raw")}
</button>
</div>
</div>
@ -326,35 +323,35 @@ export function renderConfig(props: ConfigProps) {
<div class="config-actions">
<div class="config-actions__left">
${hasChanges ? html`
<span class="config-changes-badge">${props.formMode === "raw" ? "Unsaved changes" : `${diff.length} unsaved change${diff.length !== 1 ? "s" : ""}`}</span>
<span class="config-changes-badge">${props.formMode === "raw" ? t("config.unsavedChanges") : t("config.unsavedCount", { count: diff.length })}</span>
` : html`
<span class="config-status muted">No changes</span>
<span class="config-status muted">${t("config.noChanges")}</span>
`}
</div>
<div class="config-actions__right">
<button class="btn btn--sm" ?disabled=${props.loading} @click=${props.onReload}>
${props.loading ? "Loading…" : "Reload"}
${props.loading ? t("common.loading") : t("config.reload")}
</button>
<button
class="btn btn--sm primary"
?disabled=${!canSave}
@click=${props.onSave}
>
${props.saving ? "Saving…" : "Save"}
${props.saving ? t("common.saving") : t("common.save")}
</button>
<button
class="btn btn--sm"
?disabled=${!canApply}
@click=${props.onApply}
>
${props.applying ? "Applying…" : "Apply"}
${props.applying ? t("common.applying") : t("common.apply")}
</button>
<button
class="btn btn--sm"
?disabled=${!canUpdate}
@click=${props.onUpdate}
>
${props.updating ? "Updating…" : "Update"}
${props.updating ? t("config.updating") : t("config.update")}
</button>
</div>
</div>
@ -363,7 +360,7 @@ export function renderConfig(props: ConfigProps) {
${hasChanges && props.formMode === "form" ? html`
<details class="config-diff">
<summary class="config-diff__summary">
<span>View ${diff.length} pending change${diff.length !== 1 ? "s" : ""}</span>
<span>${t("config.viewPending", { count: diff.length })}</span>
<svg class="config-diff__chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
@ -404,7 +401,7 @@ export function renderConfig(props: ConfigProps) {
class="config-subnav__item ${effectiveSubsection === null ? "active" : ""}"
@click=${() => props.onSubsectionChange(ALL_SUBSECTION)}
>
All
${t("common.all")}
</button>
${subsections.map(
(entry) => html`
@ -430,7 +427,7 @@ export function renderConfig(props: ConfigProps) {
${props.schemaLoading
? html`<div class="config-loading">
<div class="config-loading__spinner"></div>
<span>Loading schema</span>
<span>${t("config.loadingSchema")}</span>
</div>`
: renderConfigForm({
schema: analysis.schema,
@ -445,14 +442,13 @@ export function renderConfig(props: ConfigProps) {
})}
${formUnsafe
? html`<div class="callout danger" style="margin-top: 12px;">
Form view can't safely edit some fields.
Use Raw to avoid losing config entries.
${t("config.formUnsafe")}
</div>`
: nothing}
`
: html`
<label class="field config-raw-field">
<span>Raw JSON5</span>
<span>${t("config.rawJson5")}</span>
<textarea
.value=${props.raw}
@input=${(e: Event) =>

View File

@ -1,5 +1,6 @@
import { html, nothing } from "lit";
import { t } from "../../i18n";
import { formatMs } from "../format";
import {
formatCronPayload,
@ -57,42 +58,42 @@ export function renderCron(props: CronProps) {
return html`
<section class="grid grid-cols-2">
<div class="card">
<div class="card-title">Scheduler</div>
<div class="card-sub">Gateway-owned cron scheduler status.</div>
<div class="card-title">${t("cron.scheduler")}</div>
<div class="card-sub">${t("cron.schedulerDesc")}</div>
<div class="stat-grid" style="margin-top: 16px;">
<div class="stat">
<div class="stat-label">Enabled</div>
<div class="stat-label">${t("common.enabled")}</div>
<div class="stat-value">
${props.status
? props.status.enabled
? "Yes"
: "No"
: "n/a"}
? t("common.yes")
: t("common.no")
: t("common.na")}
</div>
</div>
<div class="stat">
<div class="stat-label">Jobs</div>
<div class="stat-value">${props.status?.jobs ?? "n/a"}</div>
<div class="stat-label">${t("cron.jobs")}</div>
<div class="stat-value">${props.status?.jobs ?? t("common.na")}</div>
</div>
<div class="stat">
<div class="stat-label">Next wake</div>
<div class="stat-label">${t("cron.status.nextWake")}</div>
<div class="stat-value">${formatNextRun(props.status?.nextWakeAtMs ?? null)}</div>
</div>
</div>
<div class="row" style="margin-top: 12px;">
<button class="btn" ?disabled=${props.loading} @click=${props.onRefresh}>
${props.loading ? "Refreshing…" : "Refresh"}
${props.loading ? t("common.loading") : t("common.refresh")}
</button>
${props.error ? html`<span class="muted">${props.error}</span>` : nothing}
</div>
</div>
<div class="card">
<div class="card-title">New Job</div>
<div class="card-sub">Create a scheduled wakeup or agent run.</div>
<div class="card-title">${t("cron.newJob")}</div>
<div class="card-sub">${t("cron.newJobDesc")}</div>
<div class="form-grid" style="margin-top: 16px;">
<label class="field">
<span>Name</span>
<span>${t("cron.name")}</span>
<input
.value=${props.form.name}
@input=${(e: Event) =>
@ -100,7 +101,7 @@ export function renderCron(props: CronProps) {
/>
</label>
<label class="field">
<span>Description</span>
<span>${t("cron.description")}</span>
<input
.value=${props.form.description}
@input=${(e: Event) =>
@ -108,16 +109,16 @@ export function renderCron(props: CronProps) {
/>
</label>
<label class="field">
<span>Agent ID</span>
<span>${t("cron.agentId")}</span>
<input
.value=${props.form.agentId}
@input=${(e: Event) =>
props.onFormChange({ agentId: (e.target as HTMLInputElement).value })}
placeholder="default"
placeholder=${t("cron.agentIdPlaceholder")}
/>
</label>
<label class="field checkbox">
<span>Enabled</span>
<span>${t("common.enabled")}</span>
<input
type="checkbox"
.checked=${props.form.enabled}
@ -126,7 +127,7 @@ export function renderCron(props: CronProps) {
/>
</label>
<label class="field">
<span>Schedule</span>
<span>${t("cron.scheduleKind")}</span>
<select
.value=${props.form.scheduleKind}
@change=${(e: Event) =>
@ -134,16 +135,16 @@ export function renderCron(props: CronProps) {
scheduleKind: (e.target as HTMLSelectElement).value as CronFormState["scheduleKind"],
})}
>
<option value="every">Every</option>
<option value="at">At</option>
<option value="cron">Cron</option>
<option value="every">${t("cron.everyLabel")}</option>
<option value="at">${t("cron.atLabel")}</option>
<option value="cron">${t("cron.cronLabel")}</option>
</select>
</label>
</div>
${renderScheduleFields(props)}
<div class="form-grid" style="margin-top: 12px;">
<label class="field">
<span>Session</span>
<span>${t("cron.session")}</span>
<select
.value=${props.form.sessionTarget}
@change=${(e: Event) =>
@ -151,12 +152,12 @@ export function renderCron(props: CronProps) {
sessionTarget: (e.target as HTMLSelectElement).value as CronFormState["sessionTarget"],
})}
>
<option value="main">Main</option>
<option value="isolated">Isolated</option>
<option value="main">${t("cron.main")}</option>
<option value="isolated">${t("cron.isolated")}</option>
</select>
</label>
<label class="field">
<span>Wake mode</span>
<span>${t("cron.wakeMode")}</span>
<select
.value=${props.form.wakeMode}
@change=${(e: Event) =>
@ -164,12 +165,12 @@ export function renderCron(props: CronProps) {
wakeMode: (e.target as HTMLSelectElement).value as CronFormState["wakeMode"],
})}
>
<option value="next-heartbeat">Next heartbeat</option>
<option value="now">Now</option>
<option value="next-heartbeat">${t("cron.nextHeartbeat")}</option>
<option value="now">${t("cron.now")}</option>
</select>
</label>
<label class="field">
<span>Payload</span>
<span>${t("cron.payload")}</span>
<select
.value=${props.form.payloadKind}
@change=${(e: Event) =>
@ -177,13 +178,13 @@ export function renderCron(props: CronProps) {
payloadKind: (e.target as HTMLSelectElement).value as CronFormState["payloadKind"],
})}
>
<option value="systemEvent">System event</option>
<option value="agentTurn">Agent turn</option>
<option value="systemEvent">${t("cron.systemEvent")}</option>
<option value="agentTurn">${t("cron.agentTurn")}</option>
</select>
</label>
</div>
<label class="field" style="margin-top: 12px;">
<span>${props.form.payloadKind === "systemEvent" ? "System text" : "Agent message"}</span>
<span>${props.form.payloadKind === "systemEvent" ? t("cron.systemText") : t("cron.agentMessage")}</span>
<textarea
.value=${props.form.payloadText}
@input=${(e: Event) =>
@ -197,7 +198,7 @@ export function renderCron(props: CronProps) {
? html`
<div class="form-grid" style="margin-top: 12px;">
<label class="field checkbox">
<span>Deliver</span>
<span>${t("cron.deliver")}</span>
<input
type="checkbox"
.checked=${props.form.deliver}
@ -208,7 +209,7 @@ export function renderCron(props: CronProps) {
/>
</label>
<label class="field">
<span>Channel</span>
<span>${t("cron.form.channel")}</span>
<select
.value=${props.form.channel || "last"}
@change=${(e: Event) =>
@ -225,16 +226,16 @@ export function renderCron(props: CronProps) {
</select>
</label>
<label class="field">
<span>To</span>
<span>${t("cron.to")}</span>
<input
.value=${props.form.to}
@input=${(e: Event) =>
props.onFormChange({ to: (e.target as HTMLInputElement).value })}
placeholder="+1555… or chat id"
placeholder=${t("cron.toPlaceholder")}
/>
</label>
<label class="field">
<span>Timeout (seconds)</span>
<span>${t("cron.timeoutSeconds")}</span>
<input
.value=${props.form.timeoutSeconds}
@input=${(e: Event) =>
@ -246,7 +247,7 @@ export function renderCron(props: CronProps) {
${props.form.sessionTarget === "isolated"
? html`
<label class="field">
<span>Post to main prefix</span>
<span>${t("cron.postToMainPrefix")}</span>
<input
.value=${props.form.postToMainPrefix}
@input=${(e: Event) =>
@ -262,17 +263,17 @@ export function renderCron(props: CronProps) {
: nothing}
<div class="row" style="margin-top: 14px;">
<button class="btn primary" ?disabled=${props.busy} @click=${props.onAdd}>
${props.busy ? "Saving…" : "Add job"}
${props.busy ? t("common.saving") : t("cron.addJob")}
</button>
</div>
</div>
</section>
<section class="card" style="margin-top: 18px;">
<div class="card-title">Jobs</div>
<div class="card-sub">All scheduled jobs stored in the gateway.</div>
<div class="card-title">${t("cron.jobsList")}</div>
<div class="card-sub">${t("cron.jobsListDesc")}</div>
${props.jobs.length === 0
? html`<div class="muted" style="margin-top: 12px;">No jobs yet.</div>`
? html`<div class="muted" style="margin-top: 12px;">${t("cron.noJobs")}</div>`
: html`
<div class="list" style="margin-top: 12px;">
${props.jobs.map((job) => renderJob(job, props))}
@ -281,16 +282,16 @@ export function renderCron(props: CronProps) {
</section>
<section class="card" style="margin-top: 18px;">
<div class="card-title">Run history</div>
<div class="card-sub">Latest runs for ${props.runsJobId ?? "(select a job)"}.</div>
<div class="card-title">${t("cron.runHistory")}</div>
<div class="card-sub">${t("cron.runHistoryDesc")} ${props.runsJobId ?? ""}.</div>
${props.runsJobId == null
? html`
<div class="muted" style="margin-top: 12px;">
Select a job to inspect run history.
${t("cron.selectJob")}
</div>
`
: props.runs.length === 0
? html`<div class="muted" style="margin-top: 12px;">No runs yet.</div>`
? html`<div class="muted" style="margin-top: 12px;">${t("cron.noRuns")}</div>`
: html`
<div class="list" style="margin-top: 12px;">
${props.runs.map((entry) => renderRun(entry))}
@ -305,7 +306,7 @@ function renderScheduleFields(props: CronProps) {
if (form.scheduleKind === "at") {
return html`
<label class="field" style="margin-top: 12px;">
<span>Run at</span>
<span>${t("cron.runAt")}</span>
<input
type="datetime-local"
.value=${form.scheduleAt}
@ -321,7 +322,7 @@ function renderScheduleFields(props: CronProps) {
return html`
<div class="form-grid" style="margin-top: 12px;">
<label class="field">
<span>Every</span>
<span>${t("cron.every")}</span>
<input
.value=${form.everyAmount}
@input=${(e: Event) =>
@ -331,7 +332,7 @@ function renderScheduleFields(props: CronProps) {
/>
</label>
<label class="field">
<span>Unit</span>
<span>${t("cron.unit")}</span>
<select
.value=${form.everyUnit}
@change=${(e: Event) =>
@ -339,9 +340,9 @@ function renderScheduleFields(props: CronProps) {
everyUnit: (e.target as HTMLSelectElement).value as CronFormState["everyUnit"],
})}
>
<option value="minutes">Minutes</option>
<option value="hours">Hours</option>
<option value="days">Days</option>
<option value="minutes">${t("cron.minutes")}</option>
<option value="hours">${t("cron.hours")}</option>
<option value="days">${t("cron.days")}</option>
</select>
</label>
</div>
@ -350,7 +351,7 @@ function renderScheduleFields(props: CronProps) {
return html`
<div class="form-grid" style="margin-top: 12px;">
<label class="field">
<span>Expression</span>
<span>${t("cron.expression")}</span>
<input
.value=${form.cronExpr}
@input=${(e: Event) =>
@ -358,7 +359,7 @@ function renderScheduleFields(props: CronProps) {
/>
</label>
<label class="field">
<span>Timezone (optional)</span>
<span>${t("cron.timezone")}</span>
<input
.value=${form.cronTz}
@input=${(e: Event) =>
@ -396,7 +397,7 @@ function renderJob(job: CronJob, props: CronProps) {
props.onToggle(job, !job.enabled);
}}
>
${job.enabled ? "Disable" : "Enable"}
${job.enabled ? t("cron.disable") : t("cron.enable")}
</button>
<button
class="btn"
@ -406,7 +407,7 @@ function renderJob(job: CronJob, props: CronProps) {
props.onRun(job);
}}
>
Run
${t("cron.runNow")}
</button>
<button
class="btn"
@ -416,7 +417,7 @@ function renderJob(job: CronJob, props: CronProps) {
props.onLoadRuns(job.id);
}}
>
Runs
${t("cron.runs")}
</button>
<button
class="btn danger"
@ -426,7 +427,7 @@ function renderJob(job: CronJob, props: CronProps) {
props.onRemove(job);
}}
>
Remove
${t("cron.remove")}
</button>
</div>
</div>

View File

@ -1,5 +1,6 @@
import { html, nothing } from "lit";
import { t } from "../../i18n";
import { formatEventPayload } from "../presenter";
import type { EventLogEntry } from "../app-events";
@ -32,51 +33,51 @@ export function renderDebug(props: DebugProps) {
const securityTone = critical > 0 ? "danger" : warn > 0 ? "warn" : "success";
const securityLabel =
critical > 0
? `${critical} critical`
? `${critical} ${t("debug.critical")}`
: warn > 0
? `${warn} warnings`
: "No critical issues";
? `${warn} ${t("debug.warnings")}`
: t("debug.noCritical");
return html`
<section class="grid grid-cols-2">
<div class="card">
<div class="row" style="justify-content: space-between;">
<div>
<div class="card-title">Snapshots</div>
<div class="card-sub">Status, health, and heartbeat data.</div>
<div class="card-title">${t("debug.snapshots")}</div>
<div class="card-sub">${t("debug.snapshotsDesc")}</div>
</div>
<button class="btn" ?disabled=${props.loading} @click=${props.onRefresh}>
${props.loading ? "Refreshing…" : "Refresh"}
${props.loading ? t("common.loading") : t("common.refresh")}
</button>
</div>
<div class="stack" style="margin-top: 12px;">
<div>
<div class="muted">Status</div>
<div class="muted">${t("debug.status")}</div>
${securitySummary
? html`<div class="callout ${securityTone}" style="margin-top: 8px;">
Security audit: ${securityLabel}${info > 0 ? ` · ${info} info` : ""}. Run
<span class="mono">moltbot security audit --deep</span> for details.
${t("debug.securityAudit")}: ${securityLabel}${info > 0 ? ` · ${info} ${t("common.info").toLowerCase()}` : ""}. ${t("debug.runAuditCmd")}
<span class="mono">moltbot security audit --deep</span>
</div>`
: nothing}
<pre class="code-block">${JSON.stringify(props.status ?? {}, null, 2)}</pre>
</div>
<div>
<div class="muted">Health</div>
<div class="muted">${t("debug.health")}</div>
<pre class="code-block">${JSON.stringify(props.health ?? {}, null, 2)}</pre>
</div>
<div>
<div class="muted">Last heartbeat</div>
<div class="muted">${t("debug.lastHeartbeat")}</div>
<pre class="code-block">${JSON.stringify(props.heartbeat ?? {}, null, 2)}</pre>
</div>
</div>
</div>
<div class="card">
<div class="card-title">Manual RPC</div>
<div class="card-sub">Send a raw gateway method with JSON params.</div>
<div class="card-title">${t("debug.manualRpc")}</div>
<div class="card-sub">${t("debug.manualRpcDesc")}</div>
<div class="form-grid" style="margin-top: 16px;">
<label class="field">
<span>Method</span>
<span>${t("debug.method")}</span>
<input
.value=${props.callMethod}
@input=${(e: Event) =>
@ -85,7 +86,7 @@ export function renderDebug(props: DebugProps) {
/>
</label>
<label class="field">
<span>Params (JSON)</span>
<span>${t("debug.paramsJson")}</span>
<textarea
.value=${props.callParams}
@input=${(e: Event) =>
@ -95,7 +96,7 @@ export function renderDebug(props: DebugProps) {
</label>
</div>
<div class="row" style="margin-top: 12px;">
<button class="btn primary" @click=${props.onCall}>Call</button>
<button class="btn primary" @click=${props.onCall}>${t("debug.call")}</button>
</div>
${props.callError
? html`<div class="callout danger" style="margin-top: 12px;">
@ -109,8 +110,8 @@ export function renderDebug(props: DebugProps) {
</section>
<section class="card" style="margin-top: 18px;">
<div class="card-title">Models</div>
<div class="card-sub">Catalog from models.list.</div>
<div class="card-title">${t("debug.models")}</div>
<div class="card-sub">${t("debug.modelsDesc")}</div>
<pre class="code-block" style="margin-top: 12px;">${JSON.stringify(
props.models ?? [],
null,
@ -119,10 +120,10 @@ export function renderDebug(props: DebugProps) {
</section>
<section class="card" style="margin-top: 18px;">
<div class="card-title">Event Log</div>
<div class="card-sub">Latest gateway events.</div>
<div class="card-title">${t("debug.eventLog")}</div>
<div class="card-sub">${t("debug.eventLogDesc")}</div>
${props.eventLog.length === 0
? html`<div class="muted" style="margin-top: 12px;">No events yet.</div>`
? html`<div class="muted" style="margin-top: 12px;">${t("debug.noEvents")}</div>`
: html`
<div class="list" style="margin-top: 12px;">
${props.eventLog.map(

View File

@ -1,5 +1,6 @@
import { html, nothing } from "lit";
import { t } from "../../i18n";
import { clampText } from "../format";
import type { SkillStatusEntry, SkillStatusReport } from "../types";
import type { SkillMessageMap } from "../controllers/skills";
@ -36,25 +37,25 @@ export function renderSkills(props: SkillsProps) {
<section class="card">
<div class="row" style="justify-content: space-between;">
<div>
<div class="card-title">Skills</div>
<div class="card-sub">Bundled, managed, and workspace skills.</div>
<div class="card-title">${t("skills.title")}</div>
<div class="card-sub">${t("skills.desc")}</div>
</div>
<button class="btn" ?disabled=${props.loading} @click=${props.onRefresh}>
${props.loading ? "Loading…" : "Refresh"}
${props.loading ? t("common.loading") : t("common.refresh")}
</button>
</div>
<div class="filters" style="margin-top: 14px;">
<label class="field" style="flex: 1;">
<span>Filter</span>
<span>${t("common.filter")}</span>
<input
.value=${props.filter}
@input=${(e: Event) =>
props.onFilterChange((e.target as HTMLInputElement).value)}
placeholder="Search skills"
placeholder=${t("skills.filter")}
/>
</label>
<div class="muted">${filtered.length} shown</div>
<div class="muted">${filtered.length} ${t("skills.shown")}</div>
</div>
${props.error
@ -62,7 +63,7 @@ export function renderSkills(props: SkillsProps) {
: nothing}
${filtered.length === 0
? html`<div class="muted" style="margin-top: 16px;">No skills found.</div>`
? html`<div class="muted" style="margin-top: 16px;">${t("skills.noSkills")}</div>`
: html`
<div class="list" style="margin-top: 16px;">
${filtered.map((skill) => renderSkill(skill, props))}
@ -85,8 +86,8 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
...skill.missing.os.map((o) => `os:${o}`),
];
const reasons: string[] = [];
if (skill.disabled) reasons.push("disabled");
if (skill.blockedByAllowlist) reasons.push("blocked by allowlist");
if (skill.disabled) reasons.push(t("skills.disabled").toLowerCase());
if (skill.blockedByAllowlist) reasons.push(t("skills.blockedByAllowlist"));
return html`
<div class="list-item">
<div class="list-main">
@ -97,21 +98,21 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
<div class="chip-row" style="margin-top: 6px;">
<span class="chip">${skill.source}</span>
<span class="chip ${skill.eligible ? "chip-ok" : "chip-warn"}">
${skill.eligible ? "eligible" : "blocked"}
${skill.eligible ? t("skills.eligible") : t("skills.blocked")}
</span>
${skill.disabled ? html`<span class="chip chip-warn">disabled</span>` : nothing}
${skill.disabled ? html`<span class="chip chip-warn">${t("skills.disabled").toLowerCase()}</span>` : nothing}
</div>
${missing.length > 0
? html`
<div class="muted" style="margin-top: 6px;">
Missing: ${missing.join(", ")}
${t("skills.missing")}: ${missing.join(", ")}
</div>
`
: nothing}
${reasons.length > 0
? html`
<div class="muted" style="margin-top: 6px;">
Reason: ${reasons.join(", ")}
${t("skills.reason")}: ${reasons.join(", ")}
</div>
`
: nothing}
@ -123,7 +124,7 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
?disabled=${busy}
@click=${() => props.onToggle(skill.skillKey, skill.disabled)}
>
${skill.disabled ? "Enable" : "Disable"}
${skill.disabled ? t("cron.enable") : t("cron.disable")}
</button>
${canInstall
? html`<button
@ -132,7 +133,7 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
@click=${() =>
props.onInstall(skill.skillKey, skill.name, skill.install[0].id)}
>
${busy ? "Installing…" : skill.install[0].label}
${busy ? t("skills.installing") : skill.install[0].label}
</button>`
: nothing}
</div>
@ -151,7 +152,7 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
${skill.primaryEnv
? html`
<div class="field" style="margin-top: 10px;">
<span>API key</span>
<span>${t("skills.apiKey")}</span>
<input
type="password"
.value=${apiKey}
@ -165,7 +166,7 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
?disabled=${busy}
@click=${() => props.onSaveKey(skill.skillKey)}
>
Save key
${t("skills.saveKey")}
</button>
`
: nothing}