Files
dsh-superagent/src/client/text.ts
T
2026-10-31 20:05:39 +08:00

64 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 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<DshSuperagentKey>
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<SetupProblem, 'code' | 'values'>): string => t(`setup.${problem.code}`, problem.values)
export const healthText = (t: T, health: Pick<Health, 'detail' | 'code' | 'values'>): 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))
}