/** Localization, reason formatting, message marker cleanup, and relative timestamp helpers. */ import type { Translate } from '@deepseek-ai/dsh-client-ui-slots' import type { DshSuperagentKey } from './locales.ts' import type { Localized } from '../sources/kind.ts' import type { SetupProblem } from '../setup.ts' import type { Health } from '../sources/host.ts' import { reasonText, type Reason } from '../core/reasons.ts' export type T = Translate export const errorText = (failure: unknown): string => failure instanceof Error ? failure.message : String(failure) export const localized = (t: T, text: Localized | string | undefined): string => typeof text === 'string' ? text : text ? text[t('lang') === 'en' ? 'en' : 'zh'] : '' export const actorName = (t: T, actor: { name?: Localized } | undefined): string => actor?.name ? localized(t, actor.name) : t('automatedNotice') export function valueLabel(t: T, group: string, key: string | undefined): string { if (!key) return '' const name = `${group}.${key}` as DshSuperagentKey const text = t(name) return text === name ? key : text } /** Format one or more structured or raw reasons in the active UI language, truncating multi-line host errors to their first line. */ export function reasonLabel(t: T, reason: string | Reason | readonly Reason[] | undefined): string { if (typeof reason === 'string') return reason ? label(t, { code: reason }) : '' return [reason ?? []].flat().map(item => label(t, item)).filter(Boolean).join(';') } function label(t: T, reason: Reason): string { const key = `reason.${reason.code}` as DshSuperagentKey const text = reason.code === 'said' ? reason.values?.text ?? '' : t(key, reason.values) if (reason.code !== 'said' && text !== key) return text const written = reason.code === 'said' ? text : reasonText({ ...reason, triggerId: undefined }) if (/^\w*Error\b/.test(written)) { const first = written.split('\n')[0]!; return first.length > 140 ? `${first.slice(0, 140)}…` : first } return written } export const setupText = (t: T, problem: Pick): string => t(`setup.${problem.code}`, problem.values) export const healthText = (t: T, health: Pick): string => health.code ? t(`healthCode.${health.code}` as const, health.values) : health.detail /** Replaces Feishu and GitHub content placeholders (images, cards, media, forwards, unresolved mentions) with localized labels. */ export function readableMarkers(t: T, text: string): string { return text.replace(/!\[[^\]]*\]\([^)]*\)|\[Image: [^\]]*\]|\[image\]/g, t('imageMarker')) .replace(/\[Chat card: [^\]]*\]/g, t('cardMarker')) .replace(/\[Media: [^\]]*\]/g, t('mediaMarker')).replace(/\[Merged forward\]/g, t('forwardMarker')) .replace(/@_user_\d+/g, t('unknownMention')) } const timeLocale = (lang: string) => lang === 'en' ? 'en' : 'zh-CN' export function when(value: string | undefined, lang: string): string { if (!value) return '' const date = new Date(value) if (Number.isNaN(date.getTime())) return value const seconds = Math.round((date.getTime() - Date.now()) / 1000) if (Math.abs(seconds) < 86_400) { const relative = new Intl.RelativeTimeFormat(timeLocale(lang), { numeric: 'auto' }) if (Math.abs(seconds) < 60) return relative.format(0, 'second') if (Math.abs(seconds) < 3600) return relative.format(Math.round(seconds / 60), 'minute') return relative.format(Math.round(seconds / 3600), 'hour') } const sameYear = date.getFullYear() === new Date().getFullYear() const year = sameYear ? {} : { year: 'numeric' } as const return date.toLocaleString(timeLocale(lang), { ...year, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) } export function fullTime(value: string | undefined, lang: string): string { const date = new Date(value ?? '') return Number.isNaN(date.getTime()) ? '' : date.toLocaleString(timeLocale(lang)) }