Files
template-app/modules/knowledge/ui/labels.ts
T
2026-11-14 09:56:45 +00:00

119 lines
4.2 KiB
TypeScript

// The labels each component takes, built from a message lookup. Every key this module's pages use
// is named here and nowhere else, so `__tests__/i18n.test.ts` can run these builders against the
// catalogues and fail on a key either language lacks.
import { KNOWLEDGE_REFUSALS, type KnowledgeRefusal } from "../contract/index.js"
export type Translate = (key: string, values?: Record<string, string | number>) => string
/** A typed refusal as a page or a form shows it. */
export interface RefusalNotice {
readonly code: KnowledgeRefusal
readonly text: string
}
export type RefusalLabels = Readonly<Record<KnowledgeRefusal, string>>
export const refusalLabels = (t: Translate): RefusalLabels =>
Object.fromEntries(KNOWLEDGE_REFUSALS.map((code) => [code, t(`refusals.${code}`)])) as RefusalLabels
/** The typed refusal a procedure answered, or undefined for any other error (which the form words itself). */
export function refusalOf(labels: RefusalLabels, error: unknown): RefusalNotice | undefined {
const code = (error as { problem?: { code?: unknown } } | null)?.problem?.code
return typeof code === "string" && code in labels
? { code: code as KnowledgeRefusal, text: labels[code as KnowledgeRefusal] }
: undefined
}
/** The words for a failure a document row records: the file's own, or the service's refusal; else the code. */
export function failureText(t: Translate, code: string): string {
return failureLabels(t)[code] ?? code
}
/** The failures a document row can name with words of their own: the file's, and the service's refusals. */
const FILE_FAILURES = ["file_changed", "not_text", "not_indexable", "too_large"] as const
/** The words of each failure `failureText` knows, by code, for a page that words a live row in the browser. */
export const failureLabels = (t: Translate): Readonly<Record<string, string>> =>
Object.fromEntries([
...KNOWLEDGE_REFUSALS.map((code) => [code, t(`refusals.${code}`)]),
...FILE_FAILURES.map((code) => [code, t(`failures.${code}`)]),
])
/** The status shape the labels render, declared here: ui reads no core/. */
interface StatusCounts {
documents: { queued: number; indexing: number; indexed: number; failed: number; cancelled: number }
}
export interface StatusLabels {
readonly heading: string
readonly flagOff: string
readonly state: string
readonly reasons: string
readonly indexing: string
readonly documents: string
}
export function statusLabels(t: Translate, status: StatusCounts): StatusLabels {
return {
heading: t("status.heading"),
flagOff: t("status.flagOff"),
state: t("status.state"),
reasons: t("status.reasons"),
indexing: t("status.indexing"),
documents: t("status.documents", status.documents),
}
}
export const serviceStateLabel = (t: Translate, state: string): string => {
switch (state) {
case "ready":
return t("status.stateReady")
case "indexing":
return t("status.stateIndexing")
case "registered":
return t("status.stateRegistered")
case "degraded":
return t("status.stateDegraded")
case "disabled":
return t("status.stateDisabled")
default:
return t("status.stateUnavailable")
}
}
export interface SearchFormLabels {
readonly heading: string
readonly label: string
readonly placeholder: string
readonly submit: string
readonly busy: string
readonly empty: string
readonly results: string
readonly snippet: string
readonly source: string
readonly chunk: string
readonly page: string
readonly disclosure: string
readonly refusals: RefusalLabels
}
export function searchFormLabels(t: Translate): SearchFormLabels {
return {
heading: t("search.heading"),
label: t("search.label"),
placeholder: t("search.placeholder"),
submit: t("search.submit"),
busy: t("search.busy"),
empty: t("search.empty"),
// Not filled in here: only the browser knows the count, and `formatMessage` brings the plural rules.
results: t("search.results"),
snippet: t("search.snippet"),
source: t("search.source"),
chunk: t("search.chunk", { chunk: "{chunk}" }),
page: t("search.page", { page: "{page}" }),
disclosure: t("search.disclosure"),
refusals: refusalLabels(t),
}
}