Files
pi-imgen/extensions/imgen/ui/strings.ts
T

690 lines
28 KiB
TypeScript

/**
* 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<string, string | number>;
/**
* 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<K extends string>(labels: Record<K, string>, 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<Role, string> = {
title: "Titolo",
subtitle: "Sottotitolo",
date: "Data e ora",
venue: "Dove",
details: "Dettagli",
price: "Prezzo",
footer: "In fondo",
};
const ROLE_PLACEHOLDERS: Record<Role, string> = {
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<Role, string> = {
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<Format, string> = {
"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<TemplateName, string> = {
"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<string, string> = {
"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;