${label}
-
Channel status and configuration.
+
${t("channels.generic.subtitle")}
${accountCountLabel}
${accounts.length > 0
@@ -227,16 +228,22 @@ function renderGenericChannelCard(
: html`
- Configured
- ${configured == null ? "n/a" : configured ? "Yes" : "No"}
+ ${t("channels.status.configured")}
+
+ ${configured == null ? t("common.na") : configured ? t("common.yes") : t("common.no")}
+
- Running
- ${running == null ? "n/a" : running ? "Yes" : "No"}
+ ${t("channels.status.running")}
+
+ ${running == null ? t("common.na") : running ? t("common.yes") : t("common.no")}
+
- Connected
- ${connected == null ? "n/a" : connected ? "Yes" : "No"}
+ ${t("channels.status.connected")}
+
+ ${connected == null ? t("common.na") : connected ? t("common.yes") : t("common.no")}
+
`}
@@ -274,24 +281,38 @@ function hasRecentActivity(account: ChannelAccountSnapshot): boolean {
return Date.now() - account.lastInboundAt < RECENT_ACTIVITY_THRESHOLD_MS;
}
-function deriveRunningStatus(account: ChannelAccountSnapshot): "Yes" | "No" | "Active" {
- if (account.running) return "Yes";
+function deriveRunningStatus(account: ChannelAccountSnapshot): "yes" | "no" | "active" {
+ if (account.running) return "yes";
// If we have recent inbound activity, the channel is effectively running
- if (hasRecentActivity(account)) return "Active";
- return "No";
+ if (hasRecentActivity(account)) return "active";
+ return "no";
}
-function deriveConnectedStatus(account: ChannelAccountSnapshot): "Yes" | "No" | "Active" | "n/a" {
- if (account.connected === true) return "Yes";
- if (account.connected === false) return "No";
+function deriveConnectedStatus(account: ChannelAccountSnapshot): "yes" | "no" | "active" | "na" {
+ if (account.connected === true) return "yes";
+ if (account.connected === false) return "no";
// If connected is null/undefined but we have recent activity, show as active
- if (hasRecentActivity(account)) return "Active";
- return "n/a";
+ if (hasRecentActivity(account)) return "active";
+ return "na";
}
function renderGenericAccount(account: ChannelAccountSnapshot) {
const runningStatus = deriveRunningStatus(account);
const connectedStatus = deriveConnectedStatus(account);
+ const runningText =
+ runningStatus === "active"
+ ? t("channels.status.active")
+ : runningStatus === "yes"
+ ? t("common.yes")
+ : t("common.no");
+ const connectedText =
+ connectedStatus === "active"
+ ? t("channels.status.active")
+ : connectedStatus === "na"
+ ? t("common.na")
+ : connectedStatus === "yes"
+ ? t("common.yes")
+ : t("common.no");
return html`
@@ -301,20 +322,20 @@ function renderGenericAccount(account: ChannelAccountSnapshot) {
- Running
- ${runningStatus}
+ ${t("channels.status.running")}
+ ${runningText}
- Configured
- ${account.configured ? "Yes" : "No"}
+ ${t("channels.status.configured")}
+ ${account.configured ? t("common.yes") : t("common.no")}
- Connected
- ${connectedStatus}
+ ${t("channels.status.connected")}
+ ${connectedText}
- Last inbound
- ${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}
+ ${t("channels.status.lastInbound")}
+ ${account.lastInboundAt ? formatAgo(account.lastInboundAt) : t("common.na")}
${account.lastError
? html`
diff --git a/ui/src/ui/views/channels.whatsapp.ts b/ui/src/ui/views/channels.whatsapp.ts
index eae3be695..418197f20 100644
--- a/ui/src/ui/views/channels.whatsapp.ts
+++ b/ui/src/ui/views/channels.whatsapp.ts
@@ -5,6 +5,7 @@ import type { WhatsAppStatus } from "../types";
import type { ChannelsProps } from "./channels.types";
import { renderChannelConfigSection } from "./channels.config";
import { formatDuration } from "./channels.shared";
+import { t } from "../i18n";
export function renderWhatsAppCard(params: {
props: ChannelsProps;
@@ -15,47 +16,47 @@ export function renderWhatsAppCard(params: {
return html`
-
WhatsApp
-
Link WhatsApp Web and monitor connection health.
+
${t("channels.whatsapp.title")}
+
${t("channels.whatsapp.subtitle")}
${accountCountLabel}
- Configured
- ${whatsapp?.configured ? "Yes" : "No"}
+ ${t("channels.status.configured")}
+ ${whatsapp?.configured ? t("common.yes") : t("common.no")}
- Linked
- ${whatsapp?.linked ? "Yes" : "No"}
+ ${t("channels.whatsapp.linked")}
+ ${whatsapp?.linked ? t("common.yes") : t("common.no")}
- Running
- ${whatsapp?.running ? "Yes" : "No"}
+ ${t("channels.status.running")}
+ ${whatsapp?.running ? t("common.yes") : t("common.no")}
- Connected
- ${whatsapp?.connected ? "Yes" : "No"}
+ ${t("channels.status.connected")}
+ ${whatsapp?.connected ? t("common.yes") : t("common.no")}
- Last connect
+ ${t("channels.whatsapp.lastConnect")}
${whatsapp?.lastConnectedAt
? formatAgo(whatsapp.lastConnectedAt)
- : "n/a"}
+ : t("common.na")}
- Last message
+ ${t("channels.whatsapp.lastMessage")}
- ${whatsapp?.lastMessageAt ? formatAgo(whatsapp.lastMessageAt) : "n/a"}
+ ${whatsapp?.lastMessageAt ? formatAgo(whatsapp.lastMessageAt) : t("common.na")}
- Auth age
+ ${t("channels.whatsapp.authAge")}
${whatsapp?.authAgeMs != null
? formatDuration(whatsapp.authAgeMs)
- : "n/a"}
+ : t("common.na")}
@@ -74,7 +75,7 @@ export function renderWhatsAppCard(params: {
${props.whatsappQrDataUrl
? html`
-

+
`
: nothing}
@@ -84,31 +85,31 @@ export function renderWhatsAppCard(params: {
?disabled=${props.whatsappBusy}
@click=${() => props.onWhatsAppStart(false)}
>
- ${props.whatsappBusy ? "Working…" : "Show QR"}
+ ${props.whatsappBusy ? t("common.loading") : t("channels.whatsapp.showQr")}
diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts
index a9b4da572..da696bad6 100644
--- a/ui/src/ui/views/chat.ts
+++ b/ui/src/ui/views/chat.ts
@@ -15,6 +15,7 @@ import {
} from "../chat/grouped-render";
import { renderMarkdownSidebar } from "./markdown-sidebar";
import "../components/resizable-divider";
+import { t } from "../i18n";
export type CompactionIndicatorStatus = {
active: boolean;
@@ -78,7 +79,7 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un
if (status.active) {
return html`
- ${icons.loader} Compacting context...
+ ${icons.loader} ${t("chat.compacting")}
`;
}
@@ -89,7 +90,7 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un
if (elapsed < COMPACTION_TOAST_DURATION_MS) {
return html`
- ${icons.check} Context compacted
+ ${icons.check} ${t("chat.compacted")}
`;
}
@@ -151,13 +152,13 @@ function renderAttachmentPreview(props: ChatProps) {
@@ -294,7 +295,9 @@ export function renderChat(props: ChatProps) {
${props.queue.length
? html`
-
Queued (${props.queue.length})
+
+ ${t("chat.queued", { count: props.queue.length })}
+
${props.queue.map(
(item) => html`
@@ -302,13 +305,13 @@ export function renderChat(props: ChatProps) {
${item.text ||
(item.attachments?.length
- ? `Image (${item.attachments.length})`
+ ? t("chat.imageCount", { count: item.attachments.length })
: "")}
@@ -415,7 +418,10 @@ function buildChatItems(props: ChatProps): Array
{
key: "chat:history:notice",
message: {
role: "system",
- content: `Showing last ${CHAT_HISTORY_RENDER_LIMIT} messages (${historyStart} hidden).`,
+ content: t("chat.showingLast", {
+ limit: CHAT_HISTORY_RENDER_LIMIT,
+ hidden: historyStart,
+ }),
timestamp: Date.now(),
},
});
diff --git a/ui/src/ui/views/config-form.node.ts b/ui/src/ui/views/config-form.node.ts
index 9d121d7f1..6afadebd6 100644
--- a/ui/src/ui/views/config-form.node.ts
+++ b/ui/src/ui/views/config-form.node.ts
@@ -1,5 +1,6 @@
import { html, nothing, type TemplateResult } from "lit";
import type { ConfigUiHints } from "../types";
+import { t } from "../i18n";
import {
defaultValue,
hintForPath,
@@ -7,6 +8,8 @@ import {
isSensitivePath,
pathKey,
schemaType,
+ translateHelp,
+ translateLabel,
type JsonSchema,
} from "./config-form.shared";
@@ -35,6 +38,28 @@ const icons = {
edit: html``,
};
+function optionLabel(hint: unknown, value: unknown): string {
+ const raw = value == null ? "" : String(value);
+ const translated = t(`config.option.${raw}`);
+ const fallback = translated !== `config.option.${raw}` ? translated : raw;
+
+ if (!hint || typeof hint !== "object") return fallback;
+ const record = hint as Record;
+ const direct = record.optionLabels;
+ const itemTemplate =
+ record.itemTemplate && typeof record.itemTemplate === "object"
+ ? (record.itemTemplate as Record)
+ : null;
+ const fromTemplate = itemTemplate?.optionLabels;
+ const map = (direct && typeof direct === "object" ? direct : null) ??
+ (fromTemplate && typeof fromTemplate === "object" ? fromTemplate : null);
+ if (!map) return fallback;
+ const mapRecord = map as Record;
+ const mapped = mapRecord[raw];
+ if (mapped == null) return fallback;
+ return String(mapped);
+}
+
export function renderNode(params: {
schema: JsonSchema;
value: unknown;
@@ -49,14 +74,15 @@ export function renderNode(params: {
const showLabel = params.showLabel ?? true;
const type = schemaType(schema);
const hint = hintForPath(path, hints);
- const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
- const help = hint?.help ?? schema.description;
+ const lastKey = String(path.at(-1));
+ const label = hint?.label ?? translateLabel(lastKey, schema.title);
+ const help = hint?.help ?? translateHelp(lastKey, schema.description);
const key = pathKey(path);
if (unsupported.has(key)) {
return html`
${label}
-
Unsupported schema node. Use Raw mode.
+
${t("config.field.unsupportedNode")}
`;
}
@@ -95,7 +121,7 @@ export function renderNode(params: {
?disabled=${disabled}
@click=${() => onPatch(path, lit)}
>
- ${String(lit)}
+ ${optionLabel(hint, lit)}
`)}
@@ -154,7 +180,7 @@ export function renderNode(params: {
?disabled=${disabled}
@click=${() => onPatch(path, opt)}
>
- ${String(opt)}
+ ${optionLabel(hint, opt)}
`)}
@@ -210,7 +236,7 @@ export function renderNode(params: {
return html`
${label}
-
Unsupported type: ${type}. Use Raw mode.
+
${t("config.field.unsupportedType", { type })}
`;
}
@@ -228,12 +254,17 @@ function renderTextInput(params: {
const { schema, value, path, hints, disabled, onPatch, inputType } = params;
const showLabel = params.showLabel ?? true;
const hint = hintForPath(path, hints);
- const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
- const help = hint?.help ?? schema.description;
+ const lastKey = String(path.at(-1));
+ const label = hint?.label ?? translateLabel(lastKey, schema.title);
+ const help = hint?.help ?? translateHelp(lastKey, schema.description);
const isSensitive = hint?.sensitive ?? isSensitivePath(path);
const placeholder =
hint?.placeholder ??
- (isSensitive ? "••••" : schema.default !== undefined ? `Default: ${schema.default}` : "");
+ (isSensitive
+ ? "••••"
+ : schema.default !== undefined
+ ? t("config.field.defaultPlaceholder", { value: String(schema.default) })
+ : "");
const displayValue = value ?? "";
return html`
@@ -265,7 +296,7 @@ function renderTextInput(params: {
@@ -287,8 +318,9 @@ function renderNumberInput(params: {
const { schema, value, path, hints, disabled, onPatch } = params;
const showLabel = params.showLabel ?? true;
const hint = hintForPath(path, hints);
- const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
- const help = hint?.help ?? schema.description;
+ const lastKey = String(path.at(-1));
+ const label = hint?.label ?? translateLabel(lastKey, schema.title);
+ const help = hint?.help ?? translateHelp(lastKey, schema.description);
const displayValue = value ?? schema.default ?? "";
const numValue = typeof displayValue === "number" ? displayValue : 0;
@@ -338,8 +370,9 @@ function renderSelect(params: {
const { schema, value, path, hints, disabled, options, onPatch } = params;
const showLabel = params.showLabel ?? true;
const hint = hintForPath(path, hints);
- const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
- const help = hint?.help ?? schema.description;
+ const lastKey = String(path.at(-1));
+ const label = hint?.label ?? translateLabel(lastKey, schema.title);
+ const help = hint?.help ?? translateHelp(lastKey, schema.description);
const resolvedValue = value ?? schema.default;
const currentIndex = options.findIndex(
(opt) => opt === resolvedValue || String(opt) === String(resolvedValue),
@@ -359,9 +392,9 @@ function renderSelect(params: {
onPatch(path, val === unset ? undefined : options[Number(val)]);
}}
>
-
+
${options.map((opt, idx) => html`
-
+
`)}
@@ -381,8 +414,9 @@ function renderObject(params: {
const { schema, value, path, hints, unsupported, disabled, onPatch } = params;
const showLabel = params.showLabel ?? true;
const hint = hintForPath(path, hints);
- const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
- const help = hint?.help ?? schema.description;
+ const lastKey = String(path.at(-1));
+ const label = hint?.label ?? translateLabel(lastKey, schema.title);
+ const help = hint?.help ?? translateHelp(lastKey, schema.description);
const fallback = value ?? schema.default;
const obj = fallback && typeof fallback === "object" && !Array.isArray(fallback)
@@ -480,26 +514,31 @@ function renderArray(params: {
const { schema, value, path, hints, unsupported, disabled, onPatch } = params;
const showLabel = params.showLabel ?? true;
const hint = hintForPath(path, hints);
- const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
- const help = hint?.help ?? schema.description;
+ const lastKey = String(path.at(-1));
+ const label = hint?.label ?? translateLabel(lastKey, schema.title);
+ const help = hint?.help ?? translateHelp(lastKey, schema.description);
const itemsSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;
if (!itemsSchema) {
return html`
${label}
-
Unsupported array schema. Use Raw mode.
+
${t("config.field.unsupportedArray")}
`;
}
const arr = Array.isArray(value) ? value : Array.isArray(schema.default) ? schema.default : [];
+ const countText =
+ arr.length === 1
+ ? t("config.array.count.one")
+ : t("config.array.count.many", { count: arr.length });
return html`
${help ? html`
${help}
` : nothing}
${arr.length === 0 ? html`
- No items yet. Click "Add" to create one.
+ ${t("config.array.empty")}
` : html`
@@ -528,7 +567,7 @@ function renderArray(params: {
@@ -325,36 +353,42 @@ export function renderConfig(props: ConfigProps) {
- ${hasChanges ? html`
- ${props.formMode === "raw" ? "Unsaved changes" : `${diff.length} unsaved change${diff.length !== 1 ? "s" : ""}`}
- ` : html`
- No changes
- `}
+ ${hasChanges
+ ? html`
+
+ ${props.formMode === "raw"
+ ? t("config.changes.raw")
+ : diff.length === 1
+ ? t("config.changes.form.one")
+ : t("config.changes.form.many", { count: diff.length })}
+
+ `
+ : html`${t("config.changes.none")}`}
@@ -363,7 +397,11 @@ export function renderConfig(props: ConfigProps) {
${hasChanges && props.formMode === "form" ? html`
- View ${diff.length} pending change${diff.length !== 1 ? "s" : ""}
+
+ ${diff.length === 1
+ ? t("config.diff.view.one")
+ : t("config.diff.view.many", { count: diff.length })}
+
@@ -404,7 +442,7 @@ export function renderConfig(props: ConfigProps) {
class="config-subnav__item ${effectiveSubsection === null ? "active" : ""}"
@click=${() => props.onSubsectionChange(ALL_SUBSECTION)}
>
- All
+ ${t("config.subsection.all")}
${subsections.map(
(entry) => html`
@@ -430,11 +468,11 @@ export function renderConfig(props: ConfigProps) {
${props.schemaLoading
? html`
-
Loading schema…
+
${t("config.loadingSchema")}
`
: renderConfigForm({
schema: analysis.schema,
- uiHints: props.uiHints,
+ uiHints,
value: props.formValue,
disabled: props.loading || !props.formValue,
unsupportedPaths: analysis.unsupportedPaths,
@@ -445,14 +483,13 @@ export function renderConfig(props: ConfigProps) {
})}
${formUnsafe
? html`
- Form view can't safely edit some fields.
- Use Raw to avoid losing config entries.
+ ${t("config.formUnsafe")}
`
: nothing}
`
: html`