/** * strings.ts — every word the user ever reads, in one place. * * RULE: no user-facing Italian text anywhere else in the codebase. Commands, tools, * backends and renderers import `S` (and the helpers below) and never inline a phrase. * Model-facing text (tool `description`, `promptSnippet`, art prompts) stays ENGLISH and * does NOT live here — this file is only what a human reads on screen. * * Tone: warm, plain, non-technical. He hosts events, he does not write software. * - "immagine", never "render" / "output" / "asset" * - "carattere", never "font" / "tipografia" * - "impaginazione", never "layout" / "template" * - "stile salvato", never "preset" * - say what happened and what to do next; never show a stack trace as the whole message */ import type { Block, Format, TemplateName } from "../design/spec.ts"; /** Block roles, as the brief form calls them. Keyed off the spec so it stays exhaustive. */ type Role = Block["role"]; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** Values accepted by {@link fill}. */ export type FillVars = Record; /** * Tiny interpolation: `fill("Ho salvato in {dove}", { dove: p })`. * Unknown placeholders are left untouched (a visible `{x}` is a bug worth seeing). */ export function fill(template: string, vars: FillVars = {}): string { return template.replace(/\{(\w+)\}/g, (whole, key: string) => Object.prototype.hasOwnProperty.call(vars, key) ? String(vars[key]) : whole, ); } /** Italian list: ["a"] -> "a", ["a","b"] -> "a e b", ["a","b","c"] -> "a, b e c". */ export function list(items: readonly string[]): string { if (items.length === 0) return ""; if (items.length === 1) return items[0]!; return `${items.slice(0, -1).join(", ")} e ${items[items.length - 1]!}`; } /** Picks the right word for a count. Does not include the number. */ export function plural(n: number, one: string, many: string): string { return n === 1 ? one : many; } /** A duration a human would say out loud, from milliseconds. */ export function duration(ms: number): string { const s = Math.max(0, Math.round(ms / 1000)); if (s < 5) return "un istante"; if (s < 60) return `${s} secondi`; const m = Math.round(s / 60); return m === 1 ? "circa un minuto" : `circa ${m} minuti`; } /** A bulleted block, ready to print under a message. */ export function bullets(items: readonly string[]): string { return items.map((i) => ` • ${i}`).join("\n"); } /** What the user reads when something goes wrong: what happened, then what to do. */ export interface ErrorMessage { /** One warm sentence: what happened. Never a stack trace. */ readonly message: string; /** One sentence: what he can do about it. Omitted when there is nothing to do. */ readonly fix?: string; /** Raw technical detail, for the log / the model — never the whole user message. */ readonly detail?: string; } /** Joins an {@link ErrorMessage} into the two lines shown on screen. */ export function errorText(e: ErrorMessage): string { return e.fix ? `${e.message}\n${e.fix}` : e.message; } /** * Builds a `ctx.ui.select()` menu from a labels record plus an order, and maps the * chosen label back to its key. `select()` returns the label string and can return * `undefined` on cancel — `pick()` passes that straight through. */ export function menu(labels: Record, order: readonly K[]) { return { /** Fresh mutable array — `ctx.ui.select()` wants `string[]`. */ options: order.map((k) => labels[k]), pick(chosen: string | undefined): K | undefined { if (chosen === undefined) return undefined; return order.find((k) => labels[k] === chosen); }, }; } // --------------------------------------------------------------------------- // Label tables typed against the fixed contracts (exhaustive by construction) // --------------------------------------------------------------------------- const ROLE_LABELS: Record = { title: "Titolo", subtitle: "Sottotitolo", date: "Data e ora", venue: "Dove", details: "Dettagli", price: "Prezzo", footer: "In fondo", }; const ROLE_PLACEHOLDERS: Record = { title: "Concerto jazz al chiostro", subtitle: "Trio Marini in concerto", date: "sabato 12 settembre, ore 21", venue: "Chiostro di San Domenico, Ravenna", details: "Prenotazione consigliata", price: "Ingresso libero", footer: "Con il patrocinio del Comune", }; const ROLE_HINTS: Record = { title: "Il nome dell'evento, quello che si legge da lontano.", subtitle: "Una riga in più, se serve. Puoi lasciarla vuota.", date: "Scrivila come la diresti tu: la ricopio esattamente così.", venue: "Nome del posto e città.", details: "Cose utili: prenotazioni, età, cosa portare.", price: "Per esempio «Ingresso libero» oppure «10 €».", footer: "Patrocini, sponsor, sito, numero di telefono.", }; const FORMAT_LABELS: Record = { "a3-portrait": "Locandina A3 verticale (da stampare)", "a4-portrait": "Foglio A4 verticale (da stampare)", "ig-post": "Post per Instagram", "ig-story": "Storia per Instagram", "fb-cover": "Copertina di Facebook", "yt-thumb": "Anteprima per YouTube", }; const TEMPLATE_LABELS: Record = { "hero-bottom": "Immagine grande, scritte in basso", banded: "Fascia colorata in mezzo", framed: "Immagine dentro una cornice colorata", "centred-stack": "Scritte al centro sopra l'immagine", split: "Metà immagine, metà colore", }; /** Output file names -> what that file actually is, for the closing summary. */ const FILE_LABELS: Record = { "poster-a3.pdf": "Locandina A3 pronta per la tipografia", "poster-a3.png": "Locandina A3 come immagine, per WhatsApp", "ig-post.png": "Post per Instagram", "ig-story.png": "Storia per Instagram", "fb-cover.png": "Copertina di Facebook", "yt-thumb.png": "Anteprima per YouTube", "art.png": "L'immagine di sfondo, senza scritte", "caption.txt": "Il testo da copiare sui social", "spec.json": "Le scelte di questo lavoro, per rifarlo uguale", }; // --------------------------------------------------------------------------- // The strings // --------------------------------------------------------------------------- export const S = { app: { name: "Locandine", tagline: "Locandine, loghi e immagini per i tuoi eventi.", /** Shown once at session start when everything is a posto. */ ready: "Tutto pronto. Scrivi /poster per iniziare.", }, common: { yes: "Sì", no: "No", ok: "Va bene", cancel: "Annulla", back: "Torna indietro", skip: "Salta", done: "Fatto", none: "Nessuno", empty: "(vuoto)", /** Every dialog can be abandoned; say so once, gently. */ escHint: "Premi Esc per lasciare perdere.", optional: "facoltativo", cancelled: "Lasciamo perdere. Non ho toccato niente.", somethingChanged: "Ho aggiornato quello che avevi chiesto.", }, commands: { poster: { description: "Crea la locandina di un evento", usage: "Puoi anche scrivere tutto di seguito: /poster concerto jazz al chiostro 12 settembre", }, logo: { description: "Crea un logo", usage: "Per esempio: /logo per la mia band folk, tono caldo e artigianale", }, social: { description: "Crea le immagini per Instagram e Facebook", usage: "Per esempio: /social serata karaoke venerdì al bar Centrale", }, retouch: { description: "Ritocca un'immagine che hai già", usage: "Per esempio: /retouch togli lo sfondo alla foto del logo", }, presets: { description: "Gestisci i tuoi stili salvati", usage: "Colori, carattere e logo che riusi ogni volta.", }, }, brief: { title: "Raccontami l'evento", intro: "Riempi quello che sai. Quello che lasci vuoto semplicemente non comparirà.", /** Free text typed after the command was understood and used to pre-fill. */ prefilled: "Ho letto quello che hai scritto e ho già riempito qualcosa. Controlla e correggi pure.", prefilledNothing: "Non sono riuscito a ricavarci molto: riempi tu i campi qui sotto.", /** The catch-all field every form must have. */ freeTextLabel: "Altro / contesto", freeTextPlaceholder: "Il tono, il pubblico, cosa NON deve esserci, qualsiasi cosa ti venga in mente", freeTextHint: "Scrivi come parli: al resto penso io.", labels: ROLE_LABELS, placeholders: ROLE_PLACEHOLDERS, hints: ROLE_HINTS, submit: "Vai", /** Shown when he confirms a form with no title at all. */ needTitle: "Mi serve almeno il titolo dell'evento.", confirmTitle: "Ho capito così", confirmQuestion: "Procedo?", }, formats: { title: "Che cosa ti serve?", labels: FORMAT_LABELS, order: [ "a3-portrait", "a4-portrait", "ig-post", "ig-story", "fb-cover", "yt-thumb", ] as const satisfies readonly Format[], /** The default bundle: everything, always. He asked for finished files. */ allNote: "Preparo la locandina da stampare e tutte le versioni per i social.", }, templates: { title: "Come disponiamo le scritte?", labels: TEMPLATE_LABELS, order: [ "hero-bottom", "banded", "framed", "centred-stack", "split", ] as const satisfies readonly TemplateName[], keepCurrent: "Lascia com'è", }, presets: { title: "Da dove partiamo?", fresh: "Parti da zero", freshHint: "Scelgo io colori e carattere adatti all'evento.", use: "Usa lo stile «{nome}»", useHint: "Riprende colori, carattere e logo che hai salvato.", none: "Non hai ancora salvato nessuno stile.", manageTitle: "I tuoi stili salvati", createLabel: "Salva questo stile", nameQuestion: "Come lo chiamiamo?", namePlaceholder: "La mia band", saved: "Ho salvato lo stile «{nome}».", overwriteQuestion: "Esiste già uno stile che si chiama «{nome}». Lo sostituisco?", deleteQuestion: "Cancello lo stile «{nome}»?", deleted: "Ho cancellato lo stile «{nome}».", applied: "Sto usando lo stile «{nome}».", defaultSet: "D'ora in poi parto da «{nome}».", // --- /presets: il gestore degli stili salvati --- intro: "Uno stile salvato è l'insieme di colori, carattere e logo che riusi ogni volta.", actions: { list: "Guarda quelli che hai", create: "Crea uno stile nuovo", edit: "Modifica uno stile", remove: "Cancella uno stile", setDefault: "Scegli da quale partire", close: "Chiudi", }, order: ["list", "create", "edit", "remove", "setDefault", "close"] as const, pick: "Quale stile?", listHeader: "I tuoi stili salvati:", listEmpty: "Non hai ancora salvato nessuno stile.", listHint: "Scrivi /presets e scegli «Crea uno stile nuovo».", isDefault: "— parto da questo", detailTitle: "Lo stile «{nome}»", fieldLabels: { palette: "Colori", fonts: "Carattere", logo: "Logo", tone: "Descrizione", }, moodQuestion: "Che aria deve avere?", moodKeep: "Lascia com'è", fontsQuestion: "Che carattere usiamo?", fontsKeep: "Quello che viene con l'aria che hai scelto", coloursQuestion: "Vuoi mettere i colori tuoi?", colourAsk: "{campo} — scrivilo così: #B4121F", colourKeep: "Lascia vuoto per tenere quello che c'è.", colourFields: { bg: "Sfondo", ink: "Testo", accent: "Colore in evidenza", scrim: "Velo dietro le scritte", }, colourInvalid: "«{valore}» non è un colore che so leggere: ci vuole qualcosa come #1A2B3C.", colourAdjusted: "Ho ritoccato qualche colore perché le scritte restassero leggibili:", logoQuestion: "Dov'è il file del logo?", logoPlaceholder: "~/Immagini/logo-band.png", logoHint: "Trascina qui il file. Lascia vuoto se non hai un logo.", logoNone: "Nessun logo", logoNotAFile: "«{percorso}» non è un file: controlla il percorso.", logoBadType: "Il logo dev'essere un PNG, un JPG o un SVG.", toneQuestion: "Come lo descriveresti in due parole?", tonePlaceholder: "caldo, artigianale, un po' anni Settanta", toneHint: "Mi serve per dare il tono alle immagini. Puoi lasciarlo vuoto.", summaryTitle: "Ecco com'è «{nome}»", confirmSave: "Lo salvo?", defaultQuestion: "Vuoi che d'ora in poi parta da questo?", defaultNone: "Nessuno: ogni volta parto da zero.", defaultCleared: "Va bene: d'ora in poi parto da zero ogni volta.", editWhat: "Cosa cambio di «{nome}»?", editFields: { label: "Il nome", mood: "L'aria: colori e carattere insieme", colours: "Solo i colori", fonts: "Solo il carattere", logo: "Il logo", tone: "La descrizione", done: "Ho finito", }, editOrder: ["label", "mood", "colours", "fonts", "logo", "tone", "done"] as const, updated: "Ho aggiornato lo stile «{nome}».", nameMissing: "Mi serve un nome, altrimenti non so come richiamarlo.", nameBad: "Quel nome non va bene: usa lettere e numeri, per esempio «La mia band».", nothingChanged: "Non ho cambiato niente.", savedWhere: "L'ho scritto nelle tue impostazioni: {dove}", needsTerminal: "Per creare o modificare uno stile mi serve il terminale: apri pi e scrivi /presets.", }, progress: { thinking: "Sto immaginando la locandina…", generating: "Genero l'immagine…", typesetting: "Compongo il testo…", pdf: "Preparo il PDF per la stampa…", warmup: "Preparo il motore delle immagini…", loadingModel: "Carico il modello…", draft: "Faccio una prova veloce…", finalArt: "Rifaccio l'immagine in alta definizione. Ci vuole qualche minuto…", cropping: "Ritaglio le versioni per i social…", exporting: "Salvo i file…", checkingPdf: "Controllo che il PDF sia stampabile…", removingBackground: "Tolgo lo sfondo…", upscaling: "Ingrandisco l'immagine…", vectorizing: "Ricalco il logo con linee nitide…", caption: "Scrivo il testo per i social…", openingFolder: "Apro la cartella…", /** For long steps: fill(S.progress.step, { n, tot, cosa }). */ step: "Passo {n} di {tot} — {cosa}", elapsed: "Sono passati {tempo}.", stillWorking: "Ci sto ancora lavorando. Non è bloccato.", /** Status-bar line while a generation holds the machine. */ busyStatus: "Sto lavorando a «{titolo}»", }, draft: { ready: "Ecco una prova veloce.", note: "È una bozza sgranata: serve solo a decidere l'aspetto. Il file buono lo faccio dopo.", seedNote: "Se l'immagine ti piace, la rifaccio identica ma nitida.", compare: "Prima / dopo", }, refine: { title: "Che ne dici?", hint: "Puoi cambiare quello che vuoi quante volte vuoi.", actions: { art: "Rigenera l'immagine", colours: "Cambia colori", fonts: "Cambia carattere", layout: "Cambia impaginazione", text: "Modifica il testo", accept: "Va bene così", }, order: ["art", "colours", "fonts", "layout", "text", "accept"] as const, /** The whole point of the pipeline: say plainly that these are instant. */ instant: "Questo lo cambio subito, senza rifare l'immagine.", slow: "Questo vuol dire rifare l'immagine da capo: ci vogliono un paio di minuti.", artQuestion: "Cosa cambio nell'immagine?", artPlaceholder: "più luce, niente persone, di sera invece che di giorno", coloursQuestion: "Che colori vuoi?", coloursPlaceholder: "più caldi, sul rosso mattone, sfondo scuro", fontsQuestion: "Che carattere preferisci?", fontsHint: "Scegli dall'elenco, oppure dimmi che effetto vuoi: elegante, robusto, allegro.", layoutQuestion: "Come vuoi disporre le scritte?", textQuestion: "Cosa correggo nel testo?", textHint: "Riscrivo esattamente quello che scrivi tu, accenti compresi.", applied: "Fatto. Ecco com'è adesso.", nothingToChange: "Non ho capito cosa cambiare: prova a dirmelo con altre parole.", }, confirm: { overwrite: { title: "Cartella già esistente", message: "In «{cartella}» c'è già un lavoro con questo nome. Lo sostituisco?", }, regenerateArt: { title: "Rifaccio l'immagine", message: "Devo ridisegnare tutto da capo: ci vogliono un paio di minuti. Procedo?", }, finalRender: { title: "Versione definitiva", message: "Preparo i file buoni in alta definizione. È il passaggio più lento. Procedo?", }, discard: { title: "Butto via il lavoro", message: "Perdi la bozza che abbiamo fatto finora. Sei sicuro?", }, quitWhileWorking: { title: "Sto ancora lavorando", message: "Se esci adesso l'immagine in corso va persa. Esco lo stesso?", }, /** Captions are ALWAYS asked for, never generated silently. */ wantCaption: { title: "Testo per i social", message: "Vuoi che scriva anche il testo del post con qualche hashtag?", }, openFolder: { title: "Cartella", message: "Apro la cartella con i file?", }, usePhoto: { title: "Foto", message: "Uso la foto «{file}» al posto di un'immagine disegnata?", }, }, caption: { ready: "Ecco un testo che puoi copiare:", savedTo: "L'ho messo in caption.txt.", declined: "Va bene, niente testo.", /** He writes his own copy sometimes; never touch it. */ keepingYours: "Il testo che hai scritto tu lo lascio esattamente com'è.", hashtagsNote: "Gli hashtag sono un'ipotesi: cambiali pure.", }, done: { header: "Ho finito.", folder: "Trovi tutto in {cartella}", filesHeader: "Nella cartella ci sono:", files: FILE_LABELS, printNote: "Il PDF è già pronto per la tipografia: ha il margine di taglio e le scritte al posto giusto.", printNoCropMarks: "Non ci sono i segni di taglio, perché le tipografie online li rifiutano.", reopenNote: "Per rifare questa locandina cambiando due parole, riapri la cartella e chiedimelo.", elapsed: "Ci ho messo {tempo}.", }, warnings: { titleTooLong: "Il titolo è lungo: l'ho rimpicciolito per farlo stare. Se preferisci, accorcialo.", blockDropped: "Non c'era spazio per «{campo}», così l'ho tolto. Dimmi se lo rivuoi.", contrastFixed: "Le scritte non si leggevano bene sull'immagine: ho schiarito il testo.", scrimAdded: "Ho messo un velo dietro le scritte, altrimenti sparivano nell'immagine.", accentLowContrast: "Il colore in evidenza si vede poco su questo sfondo.", photoLowRes: "La foto è piccola per una stampa A3: stampata si vedranno i pixel.", photoCropped: "La foto verrà ritagliata al centro per entrare nel formato.", fontFallback: "Il carattere «{carattere}» non c'era, ne ho usato uno simile.", slowMachine: "Sta andando più piano del solito. Meglio non aprire altri programmi pesanti.", draftOnly: "Questa è ancora la bozza: i file buoni non li ho fatti.", }, errors: { modelsDiskMissing: (path: string): ErrorMessage => ({ message: "Non trovo il disco con i modelli: non è collegato.", fix: fill("Collega il disco e riprova. Lo cerco in {dove}.", { dove: path }), detail: path, }), modelMissing: (file: string, path: string): ErrorMessage => ({ message: fill("Manca il modello «{file}»: senza quello non so disegnare.", { file }), fix: fill("Dovrebbe stare in {dove}. Scarica il file e rimettilo lì.", { dove: path }), detail: `${path}/${file}`, }), drawThingsMissing: { message: "Non trovo il programma che disegna le immagini.", fix: "Installa Draw Things e il suo comando da terminale, poi scrivi /doctor per ricontrollare.", } as ErrorMessage, typstMissing: { message: "Non trovo il programma che compone le scritte, quindi non posso preparare i file.", fix: "Installa Typst (brew install typst), poi scrivi /doctor per ricontrollare.", } as ErrorMessage, serverStartFailed: (detail?: string): ErrorMessage => ({ message: "Il motore delle immagini non è partito.", fix: "Riprova fra un momento. Se insiste, chiudi e riapri Draw Things.", detail, }), serverNotReady: { message: "Il motore delle immagini non risponde.", fix: "Aspetto ancora un po' se riprovi; altrimenti riavvia Draw Things.", } as ErrorMessage, outOfMemory: { message: "La memoria del computer è finita mentre disegnavo.", fix: "Chiudi gli altri programmi e riprova. Posso anche fare l'immagine più piccola.", } as ErrorMessage, busy: { message: "Sto già disegnando un'altra immagine.", fix: "Aspetta che finisca: due immagini insieme bloccherebbero il computer.", } as ErrorMessage, /** * Draw Things issue #121: partire da un'immagine esistente (img2img / edit) è * inaffidabile su 16GB e il comando ritaglia da solo al centro. * TODO: drawthings.ts should raise this via BackendError(..., errorText(S.errors.img2imgBug)). */ img2imgBug: { message: "Partire da un'immagine che hai già, su questo computer, non funziona in modo affidabile: è un difetto noto del programma che disegna.", fix: "Facciamo un'immagine nuova partendo dalla descrizione: viene meglio e non rischiamo.", detail: "draw-things-cli img2img (upstream issue #121)", } as ErrorMessage, generationFailed: (detail?: string): ErrorMessage => ({ message: "Non sono riuscito a disegnare l'immagine.", fix: "Riprova: quasi sempre alla seconda va. Se continua, scrivi /doctor.", detail, }), cancelled: { message: "Ho annullato. Non ho salvato niente.", } as ErrorMessage, timeout: (minutes: number): ErrorMessage => ({ message: fill("Ci stava mettendo più di {n} {minuti}, così ho fermato tutto.", { n: minutes, minuti: plural(minutes, "minuto", "minuti"), }), fix: "Riprova, magari con un'immagine più piccola.", }), photoMissing: (path: string): ErrorMessage => ({ message: "Non trovo la foto che mi hai indicato.", fix: "Controlla il percorso, oppure trascinala qui dentro.", detail: path, }), logoMissing: (path: string): ErrorMessage => ({ message: "Non trovo il file del logo.", fix: "Controlla dove l'hai messo, poi aggiorna il tuo stile salvato.", detail: path, }), fontMissing: (family: string): ErrorMessage => ({ message: fill("Il carattere «{carattere}» non è installato.", { carattere: family }), fix: "Ne scelgo un altro simile, oppure dimmi tu quale preferisci.", detail: family, }), directorUnavailable: { message: "Non riesco a parlare con l'assistente che decide lo stile.", fix: "Controlla la connessione, poi riprova.", } as ErrorMessage, directorFailed: (detail?: string): ErrorMessage => ({ message: "Non sono riuscito a decidere come impostare la locandina.", fix: "Riprova, magari raccontandomi un po' di più sull'evento.", detail, }), briefEmpty: { message: "Non ho capito di che evento si tratta.", fix: "Bastano due parole: che cos'è, quando e dove.", } as ErrorMessage, renderFailed: (detail?: string): ErrorMessage => ({ message: "Qualcosa è andato storto mentre componevo le scritte.", fix: "Riprova. Se succede ancora, prova a togliere i caratteri strani dal testo.", detail, }), pdfFailed: (detail?: string): ErrorMessage => ({ message: "Non sono riuscito a preparare il PDF per la stampa.", fix: "Le immagini per i social però ci sono. Riprova per il PDF.", detail, }), preflightFailed: (problems: readonly string[]): ErrorMessage => ({ message: "Il PDF ha qualche problema per la tipografia:", fix: bullets(problems), detail: problems.join("; "), }), exportFailed: (what: string): ErrorMessage => ({ message: fill("Non sono riuscito a salvare «{cosa}».", { cosa: what }), fix: "Gli altri file ci sono. Riprova solo per questo.", detail: what, }), writeFailed: (path: string, detail?: string): ErrorMessage => ({ message: "Non riesco a scrivere nella cartella dei lavori.", fix: fill("Controlla di poter scrivere in {dove}.", { dove: path }), detail, }), diskFull: (path: string): ErrorMessage => ({ message: "Lo spazio sul disco è finito.", fix: fill("Libera un po' di posto in {dove} e riprova.", { dove: path }), detail: path, }), openFolderFailed: (path: string): ErrorMessage => ({ message: "Non sono riuscito ad aprire la cartella.", fix: fill("La trovi qui: {dove}", { dove: path }), detail: path, }), backgroundRemovalFailed: { message: "Non sono riuscito a togliere lo sfondo.", fix: "Funziona meglio con un soggetto ben staccato dallo sfondo.", } as ErrorMessage, upscaleFailed: { message: "Non sono riuscito a ingrandire l'immagine.", fix: "Tengo quella che c'è: per i social va benissimo lo stesso.", } as ErrorMessage, vectorizeFailed: { message: "Non sono riuscito a ricalcare il logo con linee nitide.", fix: "Ti lascio la versione a pixel, che per lo schermo va bene.", } as ErrorMessage, presetUnknown: (name: string): ErrorMessage => ({ message: fill("Non ho nessuno stile salvato che si chiami «{nome}».", { nome: name }), fix: "Scrivi /presets per vedere quelli che hai.", detail: name, }), configWriteFailed: (path: string, detail?: string): ErrorMessage => ({ message: "Non riesco a salvare le tue impostazioni.", fix: fill("Controlla di poter scrivere in {dove}.", { dove: path }), detail, }), configBroken: (path: string, detail?: string): ErrorMessage => ({ message: "Le tue impostazioni non si leggono, così vado avanti con quelle standard.", fix: fill("Se vuoi sistemarle, il file è {dove}.", { dove: path }), detail, }), unknown: (detail?: string): ErrorMessage => ({ message: "È successo qualcosa che non avevo previsto.", fix: "Riprova. Se si ripete, scrivi /doctor e mandami cosa dice.", detail, }), }, /** * Health check. TODO: doctor.ts is being written in parallel — assumed to report one * line per check using these labels plus `ok` / `ko` markers. */ doctor: { title: "Controllo generale", running: "Controllo che ci sia tutto…", allGood: "Tutto a posto: puoi lavorare.", someProblems: "Qualcosa non va:", ok: "a posto", ko: "da sistemare", checks: { config: "Le tue impostazioni", modelsDisk: "Il disco con i modelli", models: "I modelli per disegnare", drawThings: "Il programma che disegna", server: "Il motore delle immagini", typst: "Il programma che compone le scritte", fonts: "I caratteri", outputDir: "La cartella dei lavori", diskSpace: "Lo spazio sul disco", memory: "La memoria disponibile", }, hint: "Sistemato quello che manca, scrivi di nuovo /doctor.", }, notify: { finished: "Locandina pronta.", finishedNamed: "«{titolo}» è pronta.", failed: "Non sono riuscito a finire.", cancelled: "Annullato.", draftReady: "Bozza pronta: dimmi che ne pensi.", }, /** Labels shown next to a running tool in the terminal. Descriptions stay English. */ toolLabels: { brief: "Raccolgo le informazioni", direct: "Decido lo stile", generateArt: "Disegno l'immagine", typeset: "Compongo il testo", export: "Salvo i file", retouch: "Ritocco l'immagine", caption: "Scrivo il testo per i social", doctor: "Controllo generale", }, } as const; /** The shape of the whole table, for anything that wants to type against it. */ export type Strings = typeof S; /** Ready-made `ctx.ui.select()` menus for the fixed choice lists. */ export const menus = { refine: () => menu(S.refine.actions, S.refine.order), formats: () => menu(FORMAT_LABELS, S.formats.order), templates: () => menu(TEMPLATE_LABELS, S.templates.order), }; /** What to call an output file on screen; falls back to the bare name. */ export function fileLabel(name: string): string { return FILE_LABELS[name] ?? name; } export default S;