import { pausedReason as pausedReasonWords, rejectReason as rejectReasonWords } from '../api'; import type { Book, BookDetail, Run } from '../api'; import { useText, type Text } from '../i18n/text'; import { Button } from '../ui/Button'; import { Callout } from '../ui/Callout'; import { counted, date, languageName, number, statusOf, translatedPercent } from './format'; import { chapters } from './units'; import styles from './About.module.css'; /** * Metadata of the book (remark 6), extended exactly up to the boundary of the contract. The * boundary is a defence of the approaches, not a gap: there is no money on the screen (§4.8), and * no models or pipeline stages either. */ export function About({ detail, onStartRun }: { detail: BookDetail; onStartRun?: () => void }) { const text = useText(); const { book, run } = detail; const startable = statusOf(book.status).startable === true; return (
{/* The one action of the book card, and it stands where the card is: a book is translated from the place that says what the book is. It is offered only where a run can begin — which state that is, is decided by the vocabulary on the api seam, in one file. */} {startable && onStartRun !== undefined && ( )}
{fields(detail, text).map(([name, value]) => (
{name}
{value}
))}
); } /** * The states a book is IN rather than the fields it has: still arriving, being cut into sections, * refused, halted. Each of them is a state the list of pairs below cannot say anything about, and * each of them is what the person is waiting on. * * ⚠ Parsing carries NO number, and that is the contract's shape and not an omission: there is no * counter of it anywhere on this surface, so a percentage would have to be invented (0.2.3). */ function State({ book, run }: { book: Book; run: Run | null }) { const text = useText(); if (book.status === 'uploading') return ; if (book.status === 'parsing') return ; if (book.status === 'rejected') { // The machine reason is the platform's, the phrase and the advice are the product's — and the // advice is the point: "the file is not one we can read" and "not us, not now" end in // different next actions (0.2.3, PD-173). const reason = rejectReasonWords.describe(book.reject_reason ?? null); return ( {text(reason.advice)} ); } // A halt is stated where the action is, not only in the status strip: the strip says WHAT, the // card is where the person looks for what to do about it. The phrase is the owner's (В-6), and // resume is deliberately not offered — after a ceiling stop it does not move the run at all. if (book.status === 'paused') { return ( ); } return null; } function fields(detail: BookDetail, text: Text): [string, string][] { const { book, run } = detail; const rows: [string, string][] = [ [text('about.title'), book.title], [ text('about.languages'), `${languageName(book.source_lang)} → ${languageName(book.target_lang)}`, ], [text('about.genre'), book.genre ?? '—'], [text('about.status'), text(statusOf(book.status).label)], [text('about.translated'), `${String(translatedPercent(book.progress))}%`], [text('about.chapters'), number(book.chapter_count)], [text('about.blocks'), number(book.progress.draft.total)], // Optional on the wire, so an absent size is a dash rather than a zero — the same shape the // dates use for "not known". [ text('about.characters'), book.character_count === undefined ? '—' : number(book.character_count), ], [text('about.notes'), number(book.note_count)], [text('about.added'), date(book.added_at)], ]; if (run) { rows.push( [text('about.runStarted'), date(run.started_at)], // `finished_at` comes both as `null` and as altogether absent: an unfinished run may have no // field at all — both forms mean one and the same thing and are drawn as a dash. [text('about.runFinished'), run.finished_at ? date(run.finished_at) : '—'], // The ceiling and the stop at the signature are the person's own choice before the start, // not the workings of the engine: showing them is honest and useful, he set them himself. [text('about.ceiling'), counted(run.ceiling_chapters, chapters)], [text('about.verifyBank'), run.verify_bank ? text('about.yes') : text('about.no')], ); } return rows; }