feat(ui): translate Nostr, Google Chat, and config views to Chinese

- Add i18n to channels.nostr.ts and channels.nostr-profile-form.ts
- Add i18n to channels.googlechat.ts
- Add i18n to channels.config.ts (channel configuration section)
- Add translation keys for profile form fields and config messages
- Complete Chinese translations for all channel views

https://claude.ai/code/session_01UK3kVX7BRyE1zEHVh3vrFY
This commit is contained in:
Claude 2026-01-29 10:26:37 +00:00
parent d0a086c921
commit cdaf6b86da
No known key found for this signature in database
6 changed files with 180 additions and 98 deletions

View File

@ -268,27 +268,62 @@ export const enUS = {
// Google Chat // Google Chat
googlechat: { googlechat: {
title: "Google Chat", title: "Google Chat",
desc: "Google Chat via service account.", desc: "Service account status and channel configuration.",
credential: "Credential",
audience: "Audience",
lastStart: "Last start",
lastProbe: "Last probe",
probe: "Probe",
probeOk: "ok",
probeFailed: "failed",
}, },
// Nostr // Nostr
nostr: { nostr: {
title: "Nostr", title: "Nostr",
desc: "Nostr protocol via NIP-04 DMs.", desc: "Decentralized DMs via Nostr relays (NIP-04).",
publicKey: "Public Key",
lastStart: "Last start",
profile: "Profile",
editProfile: "Edit Profile", editProfile: "Edit Profile",
profilePicture: "Profile picture",
displayName: "Display Name",
noProfile: "No profile set. Click \"Edit Profile\" to add your name, bio, and avatar.",
profileForm: { profileForm: {
title: "Edit Nostr Profile", title: "Edit Profile",
name: "Display Name", account: "Account",
about: "About", name: "Username",
picture: "Picture URL", nameHelp: "Short username (e.g., satoshi)",
nip05: "NIP-05 Identifier", namePlaceholder: "satoshi",
lud16: "Lightning Address", displayName: "Display Name",
displayNameHelp: "Your full display name",
displayNamePlaceholder: "Satoshi Nakamoto",
about: "Bio",
aboutHelp: "A brief bio or description",
aboutPlaceholder: "Tell people about yourself...",
picture: "Avatar URL",
pictureHelp: "HTTPS URL to your profile picture",
picturePlaceholder: "https://example.com/avatar.jpg",
picturePreview: "Profile picture preview",
advanced: "Advanced",
banner: "Banner URL", banner: "Banner URL",
bannerHelp: "HTTPS URL to a banner image",
bannerPlaceholder: "https://example.com/banner.jpg",
website: "Website", website: "Website",
showAdvanced: "Show advanced fields", websiteHelp: "Your personal website",
hideAdvanced: "Hide advanced fields", websitePlaceholder: "https://example.com",
importFromRelays: "Import from relays", nip05: "NIP-05 Identifier",
nip05Help: "Verifiable identifier (e.g., you@domain.com)",
nip05Placeholder: "you@example.com",
lud16: "Lightning Address",
lud16Help: "Lightning address for tips (LUD-16)",
lud16Placeholder: "you@getalby.com",
showAdvanced: "Show Advanced",
hideAdvanced: "Hide Advanced",
importFromRelays: "Import from Relays",
importing: "Importing…", importing: "Importing…",
savePublish: "Save & Publish",
unsavedChanges: "You have unsaved changes",
}, },
}, },
@ -298,6 +333,10 @@ export const enUS = {
saveChanges: "Save Changes", saveChanges: "Save Changes",
reloadConfig: "Reload Config", reloadConfig: "Reload Config",
unsavedChanges: "Unsaved changes", unsavedChanges: "Unsaved changes",
loadingSchema: "Loading config schema…",
schemaUnavailable: "Schema unavailable. Use Raw.",
channelSchemaUnavailable: "Channel config schema unavailable.",
reload: "Reload",
}, },
}, },

View File

@ -275,27 +275,62 @@ export const zhTW = {
// Google Chat // Google Chat
googlechat: { googlechat: {
title: "Google Chat", title: "Google Chat",
desc: "透過服務帳戶連接 Google Chat。", desc: "服務帳戶狀態與頻道設定。",
credential: "憑證",
audience: "對象",
lastStart: "上次啟動",
lastProbe: "上次探測",
probe: "探測",
probeOk: "正常",
probeFailed: "失敗",
}, },
// Nostr // Nostr
nostr: { nostr: {
title: "Nostr", title: "Nostr",
desc: "透過 NIP-04 私訊連接 Nostr 協定。", desc: "透過 Nostr 中繼站進行去中心化私訊NIP-04。",
publicKey: "公鑰",
lastStart: "上次啟動",
profile: "個人檔案",
editProfile: "編輯個人檔案", editProfile: "編輯個人檔案",
profilePicture: "個人頭像",
displayName: "顯示名稱",
noProfile: "尚未設定個人檔案。點擊「編輯個人檔案」來新增您的名稱、簡介和頭像。",
profileForm: { profileForm: {
title: "編輯 Nostr 個人檔案", title: "編輯個人檔案",
name: "顯示名稱", account: "帳號",
about: "關於", name: "使用者名稱",
nameHelp: "簡短的使用者名稱satoshi",
namePlaceholder: "satoshi",
displayName: "顯示名稱",
displayNameHelp: "您的完整顯示名稱",
displayNamePlaceholder: "Satoshi Nakamoto",
about: "個人簡介",
aboutHelp: "簡短的自我介紹",
aboutPlaceholder: "介紹一下您自己...",
picture: "頭像網址", picture: "頭像網址",
nip05: "NIP-05 識別碼", pictureHelp: "您的頭像的 HTTPS 網址",
lud16: "閃電網路地址", picturePlaceholder: "https://example.com/avatar.jpg",
picturePreview: "頭像預覽",
advanced: "進階設定",
banner: "橫幅圖片網址", banner: "橫幅圖片網址",
bannerHelp: "橫幅圖片的 HTTPS 網址",
bannerPlaceholder: "https://example.com/banner.jpg",
website: "網站", website: "網站",
showAdvanced: "顯示進階欄位", websiteHelp: "您的個人網站",
hideAdvanced: "隱藏進階欄位", websitePlaceholder: "https://example.com",
nip05: "NIP-05 識別碼",
nip05Help: "可驗證的識別碼you@domain.com",
nip05Placeholder: "you@example.com",
lud16: "閃電網路地址",
lud16Help: "用於接收打賞的閃電網路地址LUD-16",
lud16Placeholder: "you@getalby.com",
showAdvanced: "顯示進階設定",
hideAdvanced: "隱藏進階設定",
importFromRelays: "從中繼站匯入", importFromRelays: "從中繼站匯入",
importing: "匯入中…", importing: "匯入中…",
savePublish: "儲存並發布",
unsavedChanges: "有未儲存的變更",
}, },
}, },
@ -305,6 +340,10 @@ export const zhTW = {
saveChanges: "儲存變更", saveChanges: "儲存變更",
reloadConfig: "重新載入組態", reloadConfig: "重新載入組態",
unsavedChanges: "有未儲存的變更", unsavedChanges: "有未儲存的變更",
loadingSchema: "載入組態結構中…",
schemaUnavailable: "結構不可用。請使用原始模式。",
channelSchemaUnavailable: "頻道組態結構不可用。",
reload: "重新載入",
}, },
}, },

View File

@ -1,5 +1,6 @@
import { html } from "lit"; import { html } from "lit";
import { t } from "../../i18n";
import type { ConfigUiHints } from "../types"; import type { ConfigUiHints } from "../types";
import type { ChannelsProps } from "./channels.types"; import type { ChannelsProps } from "./channels.types";
import { import {
@ -71,11 +72,11 @@ export function renderChannelConfigForm(props: ChannelConfigFormProps) {
const analysis = analyzeConfigSchema(props.schema); const analysis = analyzeConfigSchema(props.schema);
const normalized = analysis.schema; const normalized = analysis.schema;
if (!normalized) { if (!normalized) {
return html`<div class="callout danger">Schema unavailable. Use Raw.</div>`; return html`<div class="callout danger">${t("channels.config.schemaUnavailable")}</div>`;
} }
const node = resolveSchemaNode(normalized, ["channels", props.channelId]); const node = resolveSchemaNode(normalized, ["channels", props.channelId]);
if (!node) { if (!node) {
return html`<div class="callout danger">Channel config schema unavailable.</div>`; return html`<div class="callout danger">${t("channels.config.channelSchemaUnavailable")}</div>`;
} }
const configValue = props.configValue ?? {}; const configValue = props.configValue ?? {};
const value = resolveChannelValue(configValue, props.channelId); const value = resolveChannelValue(configValue, props.channelId);
@ -104,7 +105,7 @@ export function renderChannelConfigSection(params: {
return html` return html`
<div style="margin-top: 16px;"> <div style="margin-top: 16px;">
${props.configSchemaLoading ${props.configSchemaLoading
? html`<div class="muted">Loading config schema…</div>` ? html`<div class="muted">${t("channels.config.loadingSchema")}</div>`
: renderChannelConfigForm({ : renderChannelConfigForm({
channelId, channelId,
configValue: props.configForm, configValue: props.configForm,
@ -119,14 +120,14 @@ export function renderChannelConfigSection(params: {
?disabled=${disabled || !props.configFormDirty} ?disabled=${disabled || !props.configFormDirty}
@click=${() => props.onConfigSave()} @click=${() => props.onConfigSave()}
> >
${props.configSaving ? "Saving…" : "Save"} ${props.configSaving ? t("common.saving") : t("common.save")}
</button> </button>
<button <button
class="btn" class="btn"
?disabled=${disabled} ?disabled=${disabled}
@click=${() => props.onConfigReload()} @click=${() => props.onConfigReload()}
> >
Reload ${t("channels.config.reload")}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
import { html, nothing } from "lit"; import { html, nothing } from "lit";
import { t } from "../../i18n";
import { formatAgo } from "../format"; import { formatAgo } from "../format";
import type { GoogleChatStatus } from "../types"; import type { GoogleChatStatus } from "../types";
import { renderChannelConfigSection } from "./channels.config"; import { renderChannelConfigSection } from "./channels.config";
@ -15,37 +16,37 @@ export function renderGoogleChatCard(params: {
return html` return html`
<div class="card"> <div class="card">
<div class="card-title">Google Chat</div> <div class="card-title">Google Chat</div>
<div class="card-sub">Chat API webhook status and channel configuration.</div> <div class="card-sub">${t("channels.googlechat.desc")}</div>
${accountCountLabel} ${accountCountLabel}
<div class="status-list" style="margin-top: 16px;"> <div class="status-list" style="margin-top: 16px;">
<div> <div>
<span class="label">Configured</span> <span class="label">${t("channels.labels.configured")}</span>
<span>${googleChat ? (googleChat.configured ? "Yes" : "No") : "n/a"}</span> <span>${googleChat ? (googleChat.configured ? t("common.yes") : t("common.no")) : t("common.na")}</span>
</div> </div>
<div> <div>
<span class="label">Running</span> <span class="label">${t("channels.labels.running")}</span>
<span>${googleChat ? (googleChat.running ? "Yes" : "No") : "n/a"}</span> <span>${googleChat ? (googleChat.running ? t("common.yes") : t("common.no")) : t("common.na")}</span>
</div> </div>
<div> <div>
<span class="label">Credential</span> <span class="label">${t("channels.googlechat.credential")}</span>
<span>${googleChat?.credentialSource ?? "n/a"}</span> <span>${googleChat?.credentialSource ?? t("common.na")}</span>
</div> </div>
<div> <div>
<span class="label">Audience</span> <span class="label">${t("channels.googlechat.audience")}</span>
<span> <span>
${googleChat?.audienceType ${googleChat?.audienceType
? `${googleChat.audienceType}${googleChat.audience ? ` · ${googleChat.audience}` : ""}` ? `${googleChat.audienceType}${googleChat.audience ? ` · ${googleChat.audience}` : ""}`
: "n/a"} : t("common.na")}
</span> </span>
</div> </div>
<div> <div>
<span class="label">Last start</span> <span class="label">${t("channels.googlechat.lastStart")}</span>
<span>${googleChat?.lastStartAt ? formatAgo(googleChat.lastStartAt) : "n/a"}</span> <span>${googleChat?.lastStartAt ? formatAgo(googleChat.lastStartAt) : t("common.na")}</span>
</div> </div>
<div> <div>
<span class="label">Last probe</span> <span class="label">${t("channels.googlechat.lastProbe")}</span>
<span>${googleChat?.lastProbeAt ? formatAgo(googleChat.lastProbeAt) : "n/a"}</span> <span>${googleChat?.lastProbeAt ? formatAgo(googleChat.lastProbeAt) : t("common.na")}</span>
</div> </div>
</div> </div>
@ -57,7 +58,7 @@ export function renderGoogleChatCard(params: {
${googleChat?.probe ${googleChat?.probe
? html`<div class="callout" style="margin-top: 12px;"> ? html`<div class="callout" style="margin-top: 12px;">
Probe ${googleChat.probe.ok ? "ok" : "failed"} · ${t("channels.googlechat.probe")} ${googleChat.probe.ok ? t("channels.googlechat.probeOk") : t("channels.googlechat.probeFailed")} ·
${googleChat.probe.status ?? ""} ${googleChat.probe.error ?? ""} ${googleChat.probe.status ?? ""} ${googleChat.probe.error ?? ""}
</div>` </div>`
: nothing} : nothing}
@ -66,7 +67,7 @@ export function renderGoogleChatCard(params: {
<div class="row" style="margin-top: 12px;"> <div class="row" style="margin-top: 12px;">
<button class="btn" @click=${() => props.onRefresh(true)}> <button class="btn" @click=${() => props.onRefresh(true)}>
Probe ${t("channels.googlechat.probe")}
</button> </button>
</div> </div>
</div> </div>

View File

@ -6,6 +6,7 @@
import { html, nothing, type TemplateResult } from "lit"; import { html, nothing, type TemplateResult } from "lit";
import { t } from "../../i18n";
import type { NostrProfile as NostrProfileType } from "../types"; import type { NostrProfile as NostrProfileType } from "../types";
// ============================================================================ // ============================================================================
@ -147,7 +148,7 @@ export function renderNostrProfileForm(params: {
<div style="margin-bottom: 12px;"> <div style="margin-bottom: 12px;">
<img <img
src=${picture} src=${picture}
alt="Profile picture preview" alt="${t("channels.nostr.profileForm.picturePreview")}"
style="max-width: 80px; max-height: 80px; border-radius: 50%; object-fit: cover; border: 2px solid var(--border-color);" style="max-width: 80px; max-height: 80px; border-radius: 50%; object-fit: cover; border: 2px solid var(--border-color);"
@error=${(e: Event) => { @error=${(e: Event) => {
const img = e.target as HTMLImageElement; const img = e.target as HTMLImageElement;
@ -165,8 +166,8 @@ export function renderNostrProfileForm(params: {
return html` return html`
<div class="nostr-profile-form" style="padding: 16px; background: var(--bg-secondary); border-radius: 8px; margin-top: 12px;"> <div class="nostr-profile-form" style="padding: 16px; background: var(--bg-secondary); border-radius: 8px; margin-top: 12px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<div style="font-weight: 600; font-size: 16px;">Edit Profile</div> <div style="font-weight: 600; font-size: 16px;">${t("channels.nostr.profileForm.title")}</div>
<div style="font-size: 12px; color: var(--text-muted);">Account: ${accountId}</div> <div style="font-size: 12px; color: var(--text-muted);">${t("channels.nostr.profileForm.account")}: ${accountId}</div>
</div> </div>
${state.error ${state.error
@ -179,56 +180,56 @@ export function renderNostrProfileForm(params: {
${renderPicturePreview()} ${renderPicturePreview()}
${renderField("name", "Username", { ${renderField("name", t("channels.nostr.profileForm.name"), {
placeholder: "satoshi", placeholder: t("channels.nostr.profileForm.namePlaceholder"),
maxLength: 256, maxLength: 256,
help: "Short username (e.g., satoshi)", help: t("channels.nostr.profileForm.nameHelp"),
})} })}
${renderField("displayName", "Display Name", { ${renderField("displayName", t("channels.nostr.profileForm.displayName"), {
placeholder: "Satoshi Nakamoto", placeholder: t("channels.nostr.profileForm.displayNamePlaceholder"),
maxLength: 256, maxLength: 256,
help: "Your full display name", help: t("channels.nostr.profileForm.displayNameHelp"),
})} })}
${renderField("about", "Bio", { ${renderField("about", t("channels.nostr.profileForm.about"), {
type: "textarea", type: "textarea",
placeholder: "Tell people about yourself...", placeholder: t("channels.nostr.profileForm.aboutPlaceholder"),
maxLength: 2000, maxLength: 2000,
help: "A brief bio or description", help: t("channels.nostr.profileForm.aboutHelp"),
})} })}
${renderField("picture", "Avatar URL", { ${renderField("picture", t("channels.nostr.profileForm.picture"), {
type: "url", type: "url",
placeholder: "https://example.com/avatar.jpg", placeholder: t("channels.nostr.profileForm.picturePlaceholder"),
help: "HTTPS URL to your profile picture", help: t("channels.nostr.profileForm.pictureHelp"),
})} })}
${state.showAdvanced ${state.showAdvanced
? html` ? html`
<div style="border-top: 1px solid var(--border-color); padding-top: 12px; margin-top: 12px;"> <div style="border-top: 1px solid var(--border-color); padding-top: 12px; margin-top: 12px;">
<div style="font-weight: 500; margin-bottom: 12px; color: var(--text-muted);">Advanced</div> <div style="font-weight: 500; margin-bottom: 12px; color: var(--text-muted);">${t("channels.nostr.profileForm.advanced")}</div>
${renderField("banner", "Banner URL", { ${renderField("banner", t("channels.nostr.profileForm.banner"), {
type: "url", type: "url",
placeholder: "https://example.com/banner.jpg", placeholder: t("channels.nostr.profileForm.bannerPlaceholder"),
help: "HTTPS URL to a banner image", help: t("channels.nostr.profileForm.bannerHelp"),
})} })}
${renderField("website", "Website", { ${renderField("website", t("channels.nostr.profileForm.website"), {
type: "url", type: "url",
placeholder: "https://example.com", placeholder: t("channels.nostr.profileForm.websitePlaceholder"),
help: "Your personal website", help: t("channels.nostr.profileForm.websiteHelp"),
})} })}
${renderField("nip05", "NIP-05 Identifier", { ${renderField("nip05", t("channels.nostr.profileForm.nip05"), {
placeholder: "you@example.com", placeholder: t("channels.nostr.profileForm.nip05Placeholder"),
help: "Verifiable identifier (e.g., you@domain.com)", help: t("channels.nostr.profileForm.nip05Help"),
})} })}
${renderField("lud16", "Lightning Address", { ${renderField("lud16", t("channels.nostr.profileForm.lud16"), {
placeholder: "you@getalby.com", placeholder: t("channels.nostr.profileForm.lud16Placeholder"),
help: "Lightning address for tips (LUD-16)", help: t("channels.nostr.profileForm.lud16Help"),
})} })}
</div> </div>
` `
@ -240,7 +241,7 @@ export function renderNostrProfileForm(params: {
@click=${callbacks.onSave} @click=${callbacks.onSave}
?disabled=${state.saving || !isDirty} ?disabled=${state.saving || !isDirty}
> >
${state.saving ? "Saving..." : "Save & Publish"} ${state.saving ? t("common.saving") : t("channels.nostr.profileForm.savePublish")}
</button> </button>
<button <button
@ -248,14 +249,14 @@ export function renderNostrProfileForm(params: {
@click=${callbacks.onImport} @click=${callbacks.onImport}
?disabled=${state.importing || state.saving} ?disabled=${state.importing || state.saving}
> >
${state.importing ? "Importing..." : "Import from Relays"} ${state.importing ? t("channels.nostr.profileForm.importing") : t("channels.nostr.profileForm.importFromRelays")}
</button> </button>
<button <button
class="btn" class="btn"
@click=${callbacks.onToggleAdvanced} @click=${callbacks.onToggleAdvanced}
> >
${state.showAdvanced ? "Hide Advanced" : "Show Advanced"} ${state.showAdvanced ? t("channels.nostr.profileForm.hideAdvanced") : t("channels.nostr.profileForm.showAdvanced")}
</button> </button>
<button <button
@ -263,13 +264,13 @@ export function renderNostrProfileForm(params: {
@click=${callbacks.onCancel} @click=${callbacks.onCancel}
?disabled=${state.saving} ?disabled=${state.saving}
> >
Cancel ${t("common.cancel")}
</button> </button>
</div> </div>
${isDirty ${isDirty
? html`<div style="font-size: 12px; color: var(--warning-color); margin-top: 8px;"> ? html`<div style="font-size: 12px; color: var(--warning-color); margin-top: 8px;">
You have unsaved changes ${t("channels.nostr.profileForm.unsavedChanges")}
</div>` </div>`
: nothing} : nothing}
</div> </div>

View File

@ -1,5 +1,6 @@
import { html, nothing } from "lit"; import { html, nothing } from "lit";
import { t } from "../../i18n";
import { formatAgo } from "../format"; import { formatAgo } from "../format";
import type { ChannelAccountSnapshot, NostrStatus } from "../types"; import type { ChannelAccountSnapshot, NostrStatus } from "../types";
import type { ChannelsProps } from "./channels.types"; import type { ChannelsProps } from "./channels.types";
@ -14,7 +15,7 @@ import {
* Truncate a pubkey for display (shows first and last 8 chars) * Truncate a pubkey for display (shows first and last 8 chars)
*/ */
function truncatePubkey(pubkey: string | null | undefined): string { function truncatePubkey(pubkey: string | null | undefined): string {
if (!pubkey) return "n/a"; if (!pubkey) return t("common.na");
if (pubkey.length <= 20) return pubkey; if (pubkey.length <= 20) return pubkey;
return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`; return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`;
} }
@ -64,20 +65,20 @@ export function renderNostrCard(params: {
</div> </div>
<div class="status-list account-card-status"> <div class="status-list account-card-status">
<div> <div>
<span class="label">Running</span> <span class="label">${t("channels.labels.running")}</span>
<span>${account.running ? "Yes" : "No"}</span> <span>${account.running ? t("common.yes") : t("common.no")}</span>
</div> </div>
<div> <div>
<span class="label">Configured</span> <span class="label">${t("channels.labels.configured")}</span>
<span>${account.configured ? "Yes" : "No"}</span> <span>${account.configured ? t("common.yes") : t("common.no")}</span>
</div> </div>
<div> <div>
<span class="label">Public Key</span> <span class="label">${t("channels.nostr.publicKey")}</span>
<span class="monospace" title="${publicKey ?? ""}">${truncatePubkey(publicKey)}</span> <span class="monospace" title="${publicKey ?? ""}">${truncatePubkey(publicKey)}</span>
</div> </div>
<div> <div>
<span class="label">Last inbound</span> <span class="label">${t("channels.lastInbound")}</span>
<span>${account.lastInboundAt ? formatAgo(account.lastInboundAt) : "n/a"}</span> <span>${account.lastInboundAt ? formatAgo(account.lastInboundAt) : t("common.na")}</span>
</div> </div>
${account.lastError ${account.lastError
? html` ? html`
@ -117,7 +118,7 @@ export function renderNostrCard(params: {
return html` return html`
<div style="margin-top: 16px; padding: 12px; background: var(--bg-secondary); border-radius: 8px;"> <div style="margin-top: 16px; padding: 12px; background: var(--bg-secondary); border-radius: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<div style="font-weight: 500;">Profile</div> <div style="font-weight: 500;">${t("channels.nostr.profile")}</div>
${summaryConfigured ${summaryConfigured
? html` ? html`
<button <button
@ -125,7 +126,7 @@ export function renderNostrCard(params: {
@click=${onEditProfile} @click=${onEditProfile}
style="font-size: 12px; padding: 4px 8px;" style="font-size: 12px; padding: 4px 8px;"
> >
Edit Profile ${t("channels.nostr.editProfile")}
</button> </button>
` `
: nothing} : nothing}
@ -138,7 +139,7 @@ export function renderNostrCard(params: {
<div style="margin-bottom: 8px;"> <div style="margin-bottom: 8px;">
<img <img
src=${picture} src=${picture}
alt="Profile picture" alt="${t("channels.nostr.profilePicture")}"
style="width: 48px; height: 48px; border-radius: 50%; object-fit: cover; border: 2px solid var(--border-color);" style="width: 48px; height: 48px; border-radius: 50%; object-fit: cover; border: 2px solid var(--border-color);"
@error=${(e: Event) => { @error=${(e: Event) => {
(e.target as HTMLImageElement).style.display = "none"; (e.target as HTMLImageElement).style.display = "none";
@ -147,19 +148,19 @@ export function renderNostrCard(params: {
</div> </div>
` `
: nothing} : nothing}
${name ? html`<div><span class="label">Name</span><span>${name}</span></div>` : nothing} ${name ? html`<div><span class="label">${t("channels.nostr.profileForm.name")}</span><span>${name}</span></div>` : nothing}
${displayName ${displayName
? html`<div><span class="label">Display Name</span><span>${displayName}</span></div>` ? html`<div><span class="label">${t("channels.nostr.displayName")}</span><span>${displayName}</span></div>`
: nothing} : nothing}
${about ${about
? html`<div><span class="label">About</span><span style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">${about}</span></div>` ? html`<div><span class="label">${t("channels.nostr.profileForm.about")}</span><span style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">${about}</span></div>`
: nothing} : nothing}
${nip05 ? html`<div><span class="label">NIP-05</span><span>${nip05}</span></div>` : nothing} ${nip05 ? html`<div><span class="label">${t("channels.nostr.profileForm.nip05")}</span><span>${nip05}</span></div>` : nothing}
</div> </div>
` `
: html` : html`
<div style="color: var(--text-muted); font-size: 13px;"> <div style="color: var(--text-muted); font-size: 13px;">
No profile set. Click "Edit Profile" to add your name, bio, and avatar. ${t("channels.nostr.noProfile")}
</div> </div>
`} `}
</div> </div>
@ -169,7 +170,7 @@ export function renderNostrCard(params: {
return html` return html`
<div class="card"> <div class="card">
<div class="card-title">Nostr</div> <div class="card-title">Nostr</div>
<div class="card-sub">Decentralized DMs via Nostr relays (NIP-04).</div> <div class="card-sub">${t("channels.nostr.desc")}</div>
${accountCountLabel} ${accountCountLabel}
${hasMultipleAccounts ${hasMultipleAccounts
@ -181,22 +182,22 @@ export function renderNostrCard(params: {
: html` : html`
<div class="status-list" style="margin-top: 16px;"> <div class="status-list" style="margin-top: 16px;">
<div> <div>
<span class="label">Configured</span> <span class="label">${t("channels.labels.configured")}</span>
<span>${summaryConfigured ? "Yes" : "No"}</span> <span>${summaryConfigured ? t("common.yes") : t("common.no")}</span>
</div> </div>
<div> <div>
<span class="label">Running</span> <span class="label">${t("channels.labels.running")}</span>
<span>${summaryRunning ? "Yes" : "No"}</span> <span>${summaryRunning ? t("common.yes") : t("common.no")}</span>
</div> </div>
<div> <div>
<span class="label">Public Key</span> <span class="label">${t("channels.nostr.publicKey")}</span>
<span class="monospace" title="${summaryPublicKey ?? ""}" <span class="monospace" title="${summaryPublicKey ?? ""}"
>${truncatePubkey(summaryPublicKey)}</span >${truncatePubkey(summaryPublicKey)}</span
> >
</div> </div>
<div> <div>
<span class="label">Last start</span> <span class="label">${t("channels.nostr.lastStart")}</span>
<span>${summaryLastStartAt ? formatAgo(summaryLastStartAt) : "n/a"}</span> <span>${summaryLastStartAt ? formatAgo(summaryLastStartAt) : t("common.na")}</span>
</div> </div>
</div> </div>
`} `}
@ -210,7 +211,7 @@ export function renderNostrCard(params: {
${renderChannelConfigSection({ channelId: "nostr", props })} ${renderChannelConfigSection({ channelId: "nostr", props })}
<div class="row" style="margin-top: 12px;"> <div class="row" style="margin-top: 12px;">
<button class="btn" @click=${() => props.onRefresh(false)}>Refresh</button> <button class="btn" @click=${() => props.onRefresh(false)}>${t("common.refresh")}</button>
</div> </div>
</div> </div>
`; `;