diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f51d435..d6f80e27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,10 +59,15 @@ because it turns other people's test suites red. lets you choose another language. The choice takes effect the next time the window opens, and Restart now does that at once. It is off while files are being made, and anything typed on the other tabs is cleared. In Polish - the settings of every format, the presets and the sizes of a run are Polish - as well, so a size reads 10,0 MB. The values you pick from a list - (portrait, a4), the names of the formats and the reason a run is refused - stay in English, and so do the command line, recipes and manifests. The + everything the window says is Polish: the settings of every format, the + values you pick from a list (pionowo for portrait), the names of the + formats, the titles of the presets, the reason a run is refused, the time + left, the licence on About and the words of the menus and dialogs, such as + Copy and Cancel. A size reads 10,0 MB. A value you typed is shown as you + typed it, and so are values that are names, such as a4 or utf-8. The + command line, recipes and manifests stay in English, so a recipe written + from the window still says portrait. A fault in the program itself is said + in Polish with its English detail under it, for whoever reports it. The same tab says what the window keeps between runs (the output directory, the window size and the language), shows the folder they are kept in, and Forget clears them. `tfg-gui --pseudo-language` opens the window with diff --git a/internal/cli/errors.go b/internal/cli/errors.go index b7aee8f5..e95568f6 100644 --- a/internal/cli/errors.go +++ b/internal/cli/errors.go @@ -5,7 +5,6 @@ import ( "context" "errors" "fmt" - "io/fs" "os" "path/filepath" "strings" @@ -106,14 +105,16 @@ func inOurWords(err error) string { // systemReason is our own English sentence for a system error, with the number // beside it. The number is the part that survives translation. func systemReason(errno syscall.Errno) string { - reason := "the system refused it" - switch { - case errors.Is(errno, fs.ErrNotExist): + var reason string + switch core.SystemKindOf(errno) { + case core.SystemNothingThere: reason = "there is nothing at that path" - case errors.Is(errno, fs.ErrPermission): + case core.SystemNoPermission: reason = "the system refused permission" - case errors.Is(errno, fs.ErrExist): + case core.SystemAlreadyThere: reason = "something is already there" + default: + reason = "the system refused it" } return fmt.Sprintf("%s (system error %d)", reason, uintptr(errno)) } diff --git a/internal/cli/recipecmd.go b/internal/cli/recipecmd.go index 8562413e..ae83e86c 100644 --- a/internal/cli/recipecmd.go +++ b/internal/cli/recipecmd.go @@ -253,7 +253,7 @@ func loadRecipeReporting(path string, asJSON bool, errOut io.Writer) (*preset.Re var invalid *recipe.ValidationError if errors.As(err, &invalid) { for _, p := range invalid.Problems { - report.Problems = append(report.Problems, validateProblem{What: p.What, Why: p.Why, Fix: p.Fix, At: p.At}) + report.Problems = append(report.Problems, validateProblem{What: p.What.String(), Why: p.Why.String(), Fix: p.Fix.String(), At: p.At}) } } else { report.Problems = append(report.Problems, validateProblem{What: err.Error()}) diff --git a/internal/core/humanise.go b/internal/core/humanise.go index 1f954a20..b9c80ab9 100644 --- a/internal/core/humanise.go +++ b/internal/core/humanise.go @@ -165,12 +165,33 @@ func Noun(n int, one, many string) string { // Roughly keeps an estimate at the precision it deserves. Seconds on a two // minute estimate are noise that changes every redraw. func Roughly(d time.Duration) string { + r := Rough(d) + switch r.Unit { + case 's': + return fmt.Sprintf("%ds", r.Seconds) + case 'm': + return fmt.Sprintf("%dm", r.Minutes) + default: + return fmt.Sprintf("%dh%dm", r.Hours, r.Minutes) + } +} + +// RoughTime is Roughly in parts, for a window that writes the units in its own +// language: Unit is 's' for seconds alone, 'm' for minutes alone and 'h' for +// hours with minutes. One arithmetic, so the two surfaces round alike. +type RoughTime struct { + Unit rune + Hours, Minutes, Seconds int +} + +// Rough is an estimate at the precision Roughly gives it, in parts. +func Rough(d time.Duration) RoughTime { switch { case d < time.Minute: - return fmt.Sprintf("%ds", int(d.Seconds())+1) + return RoughTime{Unit: 's', Seconds: int(d.Seconds()) + 1} case d < time.Hour: - return fmt.Sprintf("%dm", int(d.Minutes())+1) + return RoughTime{Unit: 'm', Minutes: int(d.Minutes()) + 1} default: - return fmt.Sprintf("%dh%dm", int(d.Hours()), int(d.Minutes())%60) + return RoughTime{Unit: 'h', Hours: int(d.Hours()), Minutes: int(d.Minutes()) % 60} } } diff --git a/internal/core/limits.go b/internal/core/limits.go index f2c66738..7531b446 100644 --- a/internal/core/limits.go +++ b/internal/core/limits.go @@ -1,8 +1,6 @@ package core import ( - "errors" - "fmt" "math" "strings" ) @@ -40,7 +38,7 @@ const MaxFilesPerRun = 1_000_000 // // It carries the four parts every refusal in this tool carries, so both callers // report it in the same words rather than each phrasing it again. -var ErrTooManyFiles = errors.New(TooManyFilesWhy + ". " + TooManyFilesFix) +var ErrTooManyFiles = Refuse(Says("core.TooManyFiles", "%s. %s", A("Why", TooManyFilesWhy), A("Fix", TooManyFilesFix))) // The same refusal in the two parts a report keeps apart. ErrTooManyFiles is // built from them rather than beside them, so the sentence and the parts cannot @@ -52,10 +50,10 @@ var ( // the sentence saying the old one, which is a refusal that lies about its // own rule. A variable rather than a constant because Sprintf is not a // constant expression, and that is the whole cost. - TooManyFilesWhy = fmt.Sprintf( + TooManyFilesWhy = Says("core.TooManyFilesWhy", "this build plans at most %d files in one run, because the whole plan is worked out in memory before anything is written - "+ - "that is what lets a run that cannot succeed be refused before the first byte", MaxFilesPerRun) - TooManyFilesFix = "Ask for fewer files, or split the work into several runs" + "that is what lets a run that cannot succeed be refused before the first byte", A("Most", MaxFilesPerRun)) + TooManyFilesFix = Says("core.TooManyFilesFix", "Ask for fewer files, or split the work into several runs") // The same refusal for the quantity that actually runs out. // @@ -65,11 +63,11 @@ var ( // thousand pages - so the file ceiling lets a run ask for about 52 GB of // plan while every number in it is legal. The sentence says memory rather // than files because that is what the person has to change. - PlanTooLargeWhy = fmt.Sprintf( + PlanTooLargeWhy = Says("core.PlanTooLargeWhy", "the whole plan is held in memory before anything is written, and this build works to a ceiling of %s for it - "+ "how much a file costs to plan depends on the format, so a ceiling on the number of files alone cannot see this", - HumanBytes(MaxPlanBytes)) - PlanTooLargeFix = "Ask for fewer files, or make each one cheaper to plan - fewer pages, fewer entries - or split the work into several runs" + A("Ceiling", Bytes(MaxPlanBytes))) + PlanTooLargeFix = Says("core.PlanTooLargeFix", "Ask for fewer files, or make each one cheaper to plan - fewer pages, fewer entries - or split the work into several runs") ) // MaxPlanBytes is the most memory this build will let a plan take. @@ -201,12 +199,12 @@ func IsRunLockName(name string) bool { // code 0, which is the guard being satisfied rather than skipped. func AddSizes(total, size int64) (int64, error) { if size < 0 { - return 0, errors.New("a file cannot be smaller than zero bytes") + return 0, Refuse(Says("core.SizeBelowZero", "a file cannot be smaller than zero bytes")) } if total > math.MaxInt64-size { - return 0, errors.New( - "the sizes in this run add up to more than a number of bytes can hold, so the total cannot be measured and the free space check cannot be trusted. " + - "Ask for fewer files or smaller ones") + return 0, Refuse(Says("core.SizesOverflow", + "the sizes in this run add up to more than a number of bytes can hold, so the total cannot be measured and the free space check cannot be trusted. "+ + "Ask for fewer files or smaller ones")) } return total + size, nil } diff --git a/internal/core/records.go b/internal/core/records.go index 0cba32b6..940dce85 100644 --- a/internal/core/records.go +++ b/internal/core/records.go @@ -66,7 +66,7 @@ type Record interface { func FillRecords(ctx context.Context, w io.Writer, rng *rand.Rand, remaining int64, rec Record) error { shortest := rec.Shortest() if remaining < shortest { - return fmt.Errorf("core: %d B are owed and the shortest whole record needs %d B", remaining, shortest) + return Defect(fmt.Errorf("core: %d B are owed and the shortest whole record needs %d B", remaining, shortest)) } // One buffer, reused. A record built into its own allocation costs a @@ -90,7 +90,7 @@ func FillRecords(ctx context.Context, w io.Writer, rng *rand.Rand, remaining int // today and the interface does not forbid it, so it is named here // rather than left to a hang somebody has to interrupt. if len(buf) == mark { - return fmt.Errorf("core: a record of this format added no bytes, so the file could never be filled") + return Defect(fmt.Errorf("core: a record of this format added no bytes, so the file could never be filled")) } // Stop while what is left still fits a whole closing record. That is // what keeps the last one from being a stub. @@ -115,7 +115,7 @@ func FillRecords(ctx context.Context, w io.Writer, rng *rand.Rand, remaining int mark := len(buf) buf = rec.AppendExact(buf, rng, remaining) if got := int64(len(buf) - mark); got != remaining { - return fmt.Errorf("core: the closing record came to %d B and %d B were owed", got, remaining) + return Defect(fmt.Errorf("core: the closing record came to %d B and %d B were owed", got, remaining)) } return WriteAll(w, buf) } diff --git a/internal/core/refusal.go b/internal/core/refusal.go new file mode 100644 index 00000000..00c5c139 --- /dev/null +++ b/internal/core/refusal.go @@ -0,0 +1,139 @@ +package core + +import "errors" + +// Refusal is a refusal that is one sentence and nothing else - the shape every +// fmt.Errorf in the engine had before refusals were said as data. +// +// It answers the three questions the surfaces ask of any refusal: what it says +// (Error, for the command line), what it says in parts (Said, for a window that +// speaks another language) and which setting it is about (AboutSetting, for a +// window that marks a box). What it wraps stays reachable through Unwrap, so a +// cause that decides the exit code is still found by errors.As. +type Refusal struct { + said Said + setting string +} + +// Refuse is a refusal about the run as a whole. +func Refuse(s Said) error { return &Refusal{said: s} } + +// RefuseAbout is a refusal about one setting, named by its recipe key. The +// layout may name the setting through SettingSlot, and each surface puts its +// own name there - see setting.go. +func RefuseAbout(setting string, s Said) error { return &Refusal{said: s, setting: setting} } + +// Error is the sentence the command line prints. +func (r *Refusal) Error() string { return r.InTheWordsOf(r.setting) } + +// InTheWordsOf is the sentence with the setting named the way one surface names +// it. The slots are filled in the layout before the values go in, so a value +// somebody typed as "{setting}" is printed as typed rather than read as a slot. +func (r *Refusal) InTheWordsOf(name string) string { + if name == "" { + name = r.setting + } + return r.said.InTheWordsOf(name) +} + +// Said is the sentence in parts. +func (r *Refusal) Said() Said { return r.said } + +// AboutSetting is the recipe key this refusal is about, or empty. +// +// A refusal about no setting of its own that wraps one about a setting answers +// for the one it wraps - the way fmt.Errorf did, which has no such method and +// so let errors.As find the one inside. Answering empty would put the refusal +// at the foot of the form, away from the box it is about. +func (r *Refusal) AboutSetting() string { + if r.setting != "" { + return r.setting + } + for _, w := range r.said.wrapped() { + var about interface{ AboutSetting() string } + if errors.As(w, &about) { + return about.AboutSetting() + } + } + return "" +} + +// Unwrap is the error the layout wraps with %w, or nil - one at most, the way +// fmt.Errorf with one %w gives one. Not every error it wraps: a window opens +// an error that unwraps to several into separate refusals, one per box +// (spread in internal/gui/window), and this sentence is one refusal whatever +// it carries. It unwrapped to a list until 2026-10-06, and a refusal wrapping +// nothing spread into no refusal at all, so a box typed wrong stayed unmarked. +func (r *Refusal) Unwrap() error { + if wrapped := r.said.wrapped(); len(wrapped) > 0 { + return wrapped[0] + } + return nil +} + +// InTheWordsOf is String with the setting slots of the layout filled first, so +// a value somebody typed as "{setting}" is printed as typed rather than read as +// a slot. +func (s Said) InTheWordsOf(name string) string { + if name == "" { + return s.String() + } + filled := s + filled.layout = InTheWordsOf(s.layout, name) + if s.one != "" { + filled.one = InTheWordsOf(s.one, name) + } + return filled.String() +} + +// wrapped is every value the layout takes with %w. +func (s Said) wrapped() []error { + var out []error + for i, d := range directivesOf(s.Layout()) { + if i >= len(s.args) || d[len(d)-1] != 'w' { + continue + } + if err, ok := s.args[i].Value.(error); ok { + out = append(out, err) + } + } + return out +} + +// SaidOf is the sentence an error says: its own, when it is one of ours and +// the whole of what it says, and otherwise the error itself as the one value +// of a sentence - which a window words as an error of the system, of a library +// or of the program. Either way the English is the error's text to the byte. +func SaidOf(err error) Said { + var said interface{ Said() Said } + if errors.As(err, &said) && said.Said().String() == err.Error() { + return said.Said() + } + return Says("core.Error", "%v", A("Error", err)) +} + +// DefectError is an error that says the program is wrong rather than the +// request: a plan this generator did not make, an encoder breaking its own +// contract. Nobody can correct it from a form, so a window says that in its +// own words and shows this text as it came, for whoever reports it - the +// owner's decision of 2026-10-05. +type DefectError struct { + Err error +} + +// Defect marks an error as one only a fault in the program can produce. +func Defect(err error) error { + if err == nil { + return nil + } + return &DefectError{Err: err} +} + +func (d *DefectError) Error() string { return d.Err.Error() } +func (d *DefectError) Unwrap() error { return d.Err } + +// IsDefect is whether an error, anywhere in its chain, is a defect. +func IsDefect(err error) bool { + var d *DefectError + return errors.As(err, &d) +} diff --git a/internal/core/said.go b/internal/core/said.go new file mode 100644 index 00000000..76077aff --- /dev/null +++ b/internal/core/said.go @@ -0,0 +1,495 @@ +package core + +import ( + "fmt" + "reflect" + "strings" + "unicode" + "unicode/utf8" +) + +// Said is a sentence for a person, kept as its parts until a surface says it. +// +// The engine refuses in English and the command line prints that English as it +// always has (D9). A window speaks the language somebody chose, and until +// 2026-10-05 it had nothing to translate: every refusal reached it as finished +// English text, so a Polish window showed Polish buttons over English refusals. +// docs/OKNO-PO-POLSKU-2026-10-05.md is the analysis. +// +// So a refusal is kept as three things. An id, which a window looks its own +// words up by. The layout, which is the fmt layout the sentence was always +// printed with, copied character for character - the command line renders it +// with fmt and the same values, which is why its bytes cannot move. And the +// values, each with a name, because a translation is written with named fields +// rather than printf verbs: a translator who writes %s where the code hands a +// number puts "%!d(string=...)" on a screen and nothing says a word, while a +// named field cannot be the wrong type. That is the owner's decision of +// 2026-08-26, see sayf in internal/gui/text. +// +// The setting slots of setting.go stay in the layout as they are. Nothing here +// fills them, so each refusal fills them exactly where it did before. +type Said struct { + id string + layout string + // one is the layout said of exactly one, for a sentence whose noun has a + // number. Empty for a sentence without one. + one string + args []Arg + // noStop is a sentence said without the full stop it ends with, for a + // reader that puts its own after it - see WithoutFullStop. + noStop bool + // capital is a sentence that starts with a capital letter where it stands - + // see Capitalized. + capital bool +} + +// Capitalized is this sentence starting with a capital letter, in every +// language - it stands where a new sentence begins. +func (s Said) Capitalized() Said { + s.capital = true + return s +} + +// Shaped is a sentence's text with the full stop and the capital this +// sentence was asked for, for whichever language the text is in. +func (s Said) Shaped(text string) string { + if s.noStop { + text = strings.TrimSuffix(text, ".") + } + if s.capital && text != "" { + r, size := utf8.DecodeRuneInString(text) + text = string(unicode.ToUpper(r)) + text[size:] + } + return text +} + +// WithoutFullStop is this sentence without the full stop at its end, in every +// language - the place it goes puts a stop of its own after it. +func (s Said) WithoutFullStop() Said { + s.noStop = true + return s +} + +// Stopless is whether the full stop at the end is left off. +func (s Said) Stopless() bool { return s.noStop } + +// Arg is one value of a sentence, with the name a translation calls it by. +type Arg struct { + Name string + Value any +} + +// A is one named value of a sentence. +func A(name string, value any) Arg { return Arg{Name: name, Value: value} } + +// CountArg is the name of the value that chooses between the forms of a +// sentence said with SaysN. +const CountArg = "Count" + +// Says is a sentence with one form. +// +// The id and the layout are literals at the call site, and a guard reads them +// from there to write the English catalogue a translator copies - a sentence +// assembled from pieces is a sentence nobody can translate. +func Says(id, layout string, args ...Arg) Said { + return Said{id: id, layout: layout, args: settled(layout, args)} +} + +// SaysN is a sentence whose noun follows a number: one layout for exactly one +// and one for every other count, the way English has always printed it here. +// The number is the value named Count, and a translation carries as many forms +// as its language has. +func SaysN(id, one, other string, args ...Arg) Said { + return Said{id: id, layout: other, one: one, args: settled(other, args)} +} + +// settled renders, at once, every value that could change before the sentence +// is read. +// +// fmt.Sprintf renders when it is called, and a Said renders when it is read. A +// list or a pointer changed in between would print something other than the +// sentence the command line printed before Said existed - so anything that is +// not a plain value is rendered now, with the verb the layout gives it, which +// is the text it would have had. Numbers, strings, sentences and errors are +// kept, because a window says those in its own words. +func settled(layout string, args []Arg) []Arg { + verbs := directivesOf(layout) + out := make([]Arg, len(args)) + for i, a := range args { + out[i] = a + if keeps(a.Value) || i >= len(verbs) { + continue + } + out[i].Value = fmt.Sprintf(verbs[i], a.Value) + } + return out +} + +// keeps is whether a value can be held as it is until the sentence is read. +func keeps(v any) bool { + switch v.(type) { + case nil, Said, error, Bytes, Term, Choice, Choices, Joined, Sentences, Conjoined, Lines, fmt.Formatter, interface{ Said() Said }: + return true + } + switch reflect.ValueOf(v).Kind() { + case reflect.Bool, reflect.String, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return true + default: + return false + } +} + +// ID is the key a window looks this sentence up by. +func (s Said) ID() string { return s.id } + +// Layout is the English layout the command line renders, the form for exactly +// one when the count is one. +func (s Said) Layout() string { + if s.one != "" && s.count() == 1 { + return s.one + } + return s.layout +} + +// Layouts is both forms of a sentence with a number, and the one form twice of +// a sentence without one - for the guard that writes the English catalogue. +func (s Said) Layouts() (one, other string) { + if s.one == "" { + return s.layout, s.layout + } + return s.one, s.layout +} + +// Plural is whether a count chooses the form of this sentence. +func (s Said) Plural() bool { return s.one != "" } + +// Args is the values, in the order the layout takes them. +func (s Said) Args() []Arg { return s.args } + +// IsZero is whether nothing was said. +func (s Said) IsZero() bool { return s.id == "" && s.layout == "" } + +// count is the value named Count, or -1 when there is none. +func (s Said) count() int64 { + for _, a := range s.args { + if a.Name == CountArg { + return asCount(a.Value) + } + } + return -1 +} + +func asCount(v any) int64 { + r := reflect.ValueOf(v) + switch r.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return r.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return int64(r.Uint()) + default: + return -1 + } +} + +// String is the sentence in English, with the setting slots left in it - +// exactly the text fmt made of the layout and the values before Said existed. +// +// The %w of a wrapping layout is printed the way fmt.Errorf prints it, as the +// error's own text. +func (s Said) String() string { + values := make([]any, len(s.args)) + for i, a := range s.args { + values[i] = a.Value + } + if s.countUnsaid() { + values = values[:len(values)-1] + } + return s.Shaped(fmt.Sprintf(withoutWrapVerb(s.Layout()), values...)) +} + +// countUnsaid is whether the number that chooses the form is not printed - +// "holds" against "hold". It is then the last value, and the layout takes one +// value fewer than the sentence carries. +func (s Said) countUnsaid() bool { + n := len(s.args) + return s.one != "" && n > 0 && s.args[n-1].Name == CountArg && len(directivesOf(s.Layout())) == n-1 +} + +// Format lets a sentence be a value of another, rendered with the verb the +// outer layout gives it. +func (s Said) Format(f fmt.State, verb rune) { + fmt.Fprintf(f, fmt.FormatString(f, verb), s.String()) +} + +// Bytes is a number of bytes said the way a person reads it - 10.0 MB. +// +// A type rather than core.HumanBytes at the call site, so a window can write +// the decimal mark of its own language where the command line writes a dot. +type Bytes int64 + +// Format prints the readable size for %s and %v, and the number for anything +// else. +func (b Bytes) Format(f fmt.State, verb rune) { + if verb == 's' || verb == 'v' { + fmt.Fprintf(f, fmt.FormatString(f, verb), HumanBytes(int64(b))) + return + } + fmt.Fprintf(f, fmt.FormatString(f, verb), int64(b)) +} + +// SettingField is the field a translation names the setting by, where the +// English layout has SettingSlot. Reserved: no value of a sentence is called +// this. +const SettingField = "Setting" + +// NamedLayout is a layout with every value written as the field a translation +// uses - {{.Format}} where the English has %s - and the setting slot as +// {{.Setting}}. The article slot goes: it is English grammar, and every other +// language that needs an article writes its own. +// +// This is what a translator copies, and what the pseudo language disguises. +// It is never what the command line prints - that is the layout itself. +func NamedLayout(layout string, names []string) string { + layout = strings.ReplaceAll(layout, ArticleSlot+" ", "") + layout = strings.ReplaceAll(layout, ArticleSlot, "") + layout = strings.ReplaceAll(layout, SettingSlot, "{{."+SettingField+"}}") + var b strings.Builder + value := 0 + for i := 0; i < len(layout); i++ { + if layout[i] != '%' { + b.WriteByte(layout[i]) + continue + } + end := directiveEnd(layout, i+1) + if end >= len(layout) { + b.WriteString(layout[i:]) + break + } + if layout[end] == '%' { + b.WriteByte('%') + } else if value < len(names) { + b.WriteString("{{." + names[value] + "}}") + value++ + } + i = end + } + return b.String() +} + +// Names is the name of every value, in order. +func (s Said) Names() []string { + out := make([]string, len(s.args)) + for i, a := range s.args { + out[i] = a.Name + } + return out +} + +// Directives is how the layout prints each value, in order - "%q", "%d". +func (s Said) Directives() []string { return directivesOf(s.Layout()) } + +// DirectivesOf is how a layout prints each value, in order, for the guard that +// reads every layout out of the code. +func DirectivesOf(layout string) []string { return parseDirectives(layout) } + +// Term is a word a registry declares - a unit, the shape of a text - as a value +// of a sentence. The command line prints the word as the registry spells it, +// and a window looks it up under Key, the key its catalogue keeps the word by. +type Term struct { + Key string + Text string +} + +// Format prints the word, with the verb the layout gives it. +func (t Term) Format(f fmt.State, verb rune) { + fmt.Fprintf(f, fmt.FormatString(f, verb), t.Text) +} + +// LabelTerm is the name of a declared setting - "width" - which a window shows +// as the label above its box. +func LabelTerm(name string) Term { return Term{Key: LabelKey(name), Text: name} } + +// LabelKey and JointKey are two more of the keys a window keeps words under - +// see UnitKey. FormatOwner is whose declaration a format's words are. +func LabelKey(name string) string { return "Label." + name } +func JointKey(owner, of, by string) string { return "Joint." + owner + "." + of + "." + by } +func FormatOwner(id string) string { return "format/" + id } + +// UnitTerm is the word for what a number counts - "pixels". +func UnitTerm(unit string) Term { return Term{Key: UnitKey(unit), Text: unit} } + +// UnitKey and ChoiceKey are the keys a window's catalogue keeps a unit and a +// value of a list under. Here rather than in the window, so the sentence that +// carries the word and the catalogue that translates it cannot come to spell +// the key two ways. +func UnitKey(unit string) string { return "Unit." + unit } +func ChoiceKey(of, value string) string { return "Choice." + of + "." + value } + +// KindKey is the key of the word for what a tool works on - file, folder. +func KindKey(kind string) string { return "Kind." + kind } + +// Choice is a value of a closed list, as a value of a sentence. +// +// The command line prints the value as the layout says - "accept", quoted +// where the layout quotes. A window shows a list's values under names in its +// own language, and a sentence naming one has to call it what the list calls +// it, without the quotes of a value somebody typed (the owner's decision of +// 2026-10-05). Of is the setting whose list it is, by its recipe key. +type Choice struct { + Of string + Value string +} + +// Format prints the value, with the verb the layout gives it. +func (c Choice) Format(f fmt.State, verb rune) { + fmt.Fprintf(f, fmt.FormatString(f, verb), c.Value) +} + +// Joined is names in a sentence joined the way English joins them - "a, b and +// c", "a or b" - which a window joins with its own words. +type Joined struct { + Items []string + // And joins the last two with "and", and false with "or". + And bool +} + +// Format prints the names joined, with the verb the layout gives the whole. +func (j Joined) Format(f fmt.State, verb rune) { + word := "or" + if j.And { + word = "and" + } + joined := "" + switch len(j.Items) { + case 0: + case 1: + joined = j.Items[0] + default: + joined = strings.Join(j.Items[:len(j.Items)-1], ", ") + " " + word + " " + j.Items[len(j.Items)-1] + } + fmt.Fprintf(f, fmt.FormatString(f, verb), joined) +} + +// Conjoined is several sentences joined with "and" - "2 PDF files of 10 B and +// 1 TXT file of 4 B" - which a window joins the way its language does. +type Conjoined []Said + +// Format prints the sentences in English, each pair joined with "and". +func (c Conjoined) Format(f fmt.State, verb rune) { + parts := make([]string, len(c)) + for i, s := range c { + parts[i] = s.String() + } + fmt.Fprintf(f, fmt.FormatString(f, verb), strings.Join(parts, " and ")) +} + +// Sentences is several sentences said as a list, one after another with a +// comma between them. +type Sentences []Said + +// Format prints the sentences in English, joined with commas. +func (l Sentences) Format(f fmt.State, verb rune) { + parts := make([]string, len(l)) + for i, s := range l { + parts[i] = s.String() + } + fmt.Fprintf(f, fmt.FormatString(f, verb), strings.Join(parts, ", ")) +} + +// Lines is several sentences said one under another, each on a line of its own +// and indented by two spaces after the first. +type Lines []Said + +// Format prints the sentences in English, one a line. +func (l Lines) Format(f fmt.State, verb rune) { + parts := make([]string, len(l)) + for i, s := range l { + parts[i] = s.String() + } + fmt.Fprintf(f, fmt.FormatString(f, verb), strings.Join(parts, "\n ")) +} + +// Choices is several values of one closed list, said as a list - "comma, tab, +// semicolon" - for a sentence that names what a list offers. +type Choices struct { + Of string + Values []string +} + +// Format prints the values joined the way the command line always joined +// them. +func (c Choices) Format(f fmt.State, verb rune) { + fmt.Fprintf(f, fmt.FormatString(f, verb), strings.Join(c.Values, ", ")) +} + +// directivesOf is every value directive of a layout, in the order the values +// are taken - "%q", "%d", "% x". A literal %% takes no value and is left out. +// +// Parsed every time rather than kept per layout. Measured 2026-10-06: a parse +// is 141 ns against 16 ns for a kept answer, so a note said for each of a +// million files costs about a tenth of a second more on a run that takes tens +// of minutes - and keeping it needed a sync.Map, concurrency in a package +// that has none anywhere else. +func directivesOf(layout string) []string { return parseDirectives(layout) } + +// parseDirectives reads a layout the way fmt does: a per cent sign, then flags, +// a width, a precision, then one verb letter. Widths taken from a value (*) and +// explicit positions ([2]) are refused by the guard over every layout, so a +// directive here is always one value. +func parseDirectives(layout string) []string { + var out []string + for i := 0; i < len(layout); i++ { + if layout[i] != '%' { + continue + } + end := directiveEnd(layout, i+1) + if end >= len(layout) { + break + } + if layout[end] != '%' { + out = append(out, layout[i:end+1]) + } + i = end + } + return out +} + +// directiveEnd is where the verb of the directive starting after a per cent +// sign stands. +func directiveEnd(layout string, from int) int { + j := from + for j < len(layout) && strings.ContainsRune("+-# 0123456789.", rune(layout[j])) { + j++ + } + return j +} + +// withoutWrapVerb is a layout with %w spelled %v, which is what fmt.Errorf +// prints for it and what fmt.Sprintf would otherwise print as %!w(...). +func withoutWrapVerb(layout string) string { + if !strings.Contains(layout, "%w") { + return layout + } + var b strings.Builder + for i := 0; i < len(layout); i++ { + b.WriteByte(layout[i]) + if layout[i] != '%' { + continue + } + end := directiveEnd(layout, i+1) + if end >= len(layout) { + break + } + b.WriteString(layout[i+1 : end]) + if layout[end] == 'w' { + b.WriteByte('v') + } else { + b.WriteByte(layout[end]) + } + i = end + } + return b.String() +} diff --git a/internal/core/setting.go b/internal/core/setting.go index e9b64d16..cad9da0a 100644 --- a/internal/core/setting.go +++ b/internal/core/setting.go @@ -98,37 +98,11 @@ const SettingSize = "size" // engine. const SettingSizeRange = "size-range" -// SettingErrorf is a refusal that names the setting it is about through a slot, -// so each surface reads it in its own words. -// -// The format string is kept unrendered rather than the finished message, -// because the values put into it come from whoever ran the tool. A size typed -// as "{setting}" would otherwise be read back as a slot and answered with the -// name of the box, which is the shape of defect this whole mechanism exists to -// remove rather than to move somewhere quieter. -func SettingErrorf(setting, format string, a ...any) error { - return &settingError{setting: setting, format: format, args: a} -} - -type settingError struct { - setting string - format string - args []any -} - -func (e *settingError) Error() string { return e.InTheWordsOf(e.setting) } - -func (e *settingError) InTheWordsOf(name string) string { - if name == "" { - name = e.setting - } - return fmt.Sprintf(InTheWordsOf(e.format, name), e.args...) -} - -// AboutSetting lets a window put this refusal beside the box it came from, -// through the same interface the engine, the format registry and the preset -// package already answer. -func (e *settingError) AboutSetting() string { return e.setting } +// A refusal that names its setting through a slot is RefuseAbout since +// 2026-10-05 (refusal.go). It keeps the layout unrendered for the reason this +// file gave for SettingErrorf, which it replaced: the values put into it come +// from whoever ran the tool, and a size typed as "{setting}" would otherwise be +// read back as a slot and answered with the name of the box. // KeyTargets and KeyContains are the two list names a settings address is built // from. The rest of the vocabulary lives in internal/recipe, with the reader diff --git a/internal/core/size.go b/internal/core/size.go index 53de87fd..ca6680d8 100644 --- a/internal/core/size.go +++ b/internal/core/size.go @@ -1,8 +1,6 @@ package core import ( - "errors" - "fmt" "math" "strconv" "strings" @@ -35,7 +33,7 @@ import ( func ParseSize(s string) (int64, error) { raw := strings.TrimSpace(s) if raw == "" { - return 0, SettingErrorf(SettingSize, "empty {setting}: write a number of bytes or a size such as 10mb, 1.5gib or 700kB") + return 0, RefuseAbout(SettingSize, Says("core.SizeEmpty", "empty {setting}: write a number of bytes or a size such as 10mb, 1.5gib or 700kB")) } digits := strings.TrimRight(raw, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ ") @@ -43,7 +41,7 @@ func ParseSize(s string) (int64, error) { digits = strings.TrimSpace(digits) if digits == "" { - return 0, SettingErrorf(SettingSize, "{setting} %q has no number: write something like 10mb or 1048576", s) + return 0, RefuseAbout(SettingSize, Says("core.SizeNoNumber", "{setting} %q has no number: write something like 10mb or 1048576", A("Value", s))) } multiplier, ok := unitBytes(unit) @@ -52,17 +50,17 @@ func ParseSize(s string) (int64, error) { // of this sentence said kb was for thousands, which is the opposite of // what the tool does and appeared in the one place somebody has // already got their units wrong. - return 0, SettingErrorf(SettingSize, "{setting} %q uses an unknown unit %q: use b, kb, mb, gb or tb, all counting in 1024s, and kib, mib, gib or tib for the same thing spelled out", s, unit) + return 0, RefuseAbout(SettingSize, Says("core.SizeUnknownUnit", "{setting} %q uses an unknown unit %q: use b, kb, mb, gb or tb, all counting in 1024s, and kib, mib, gib or tib for the same thing spelled out", A("Value", s), A("Unit", unit))) } // A whole number with no unit, or with a unit, is the common case and // stays in integer arithmetic so nothing is lost on the way. if n, err := strconv.ParseInt(digits, 10, 64); err == nil { if n < 0 { - return 0, SettingErrorf(SettingSize, "{setting} %q is negative: a file cannot be smaller than zero bytes", s) + return 0, sizeNegative(s) } if multiplier != 1 && n > math.MaxInt64/multiplier { - return 0, SettingErrorf(SettingSize, "{setting} %q is too large to express in bytes", s) + return 0, sizeTooLarge(s) } return n * multiplier, nil } @@ -79,16 +77,16 @@ func ParseSize(s string) (int64, error) { // // The decimal point stays, because 1.5gib is a real thing people write. if !plainDecimal(digits) { - return 0, SettingErrorf(SettingSize, - "{setting} %q is not a plain number: write the digits out, as 10mb, 1.5gib or 1048576. Spellings such as 1e5, 0x10 or 1_000 are refused rather than guessed at, the same as in a recipe", s) + return 0, RefuseAbout(SettingSize, Says("core.SizeNotPlain", + "{setting} %q is not a plain number: write the digits out, as 10mb, 1.5gib or 1048576. Spellings such as 1e5, 0x10 or 1_000 are refused rather than guessed at, the same as in a recipe", A("Value", s))) } f, err := strconv.ParseFloat(digits, 64) if err != nil { - return 0, SettingErrorf(SettingSize, "{setting} %q is not a number: write something like 10mb, 1.5gib or 1048576", s) + return 0, RefuseAbout(SettingSize, Says("core.SizeNotANumber", "{setting} %q is not a number: write something like 10mb, 1.5gib or 1048576", A("Value", s))) } if f < 0 { - return 0, SettingErrorf(SettingSize, "{setting} %q is negative: a file cannot be smaller than zero bytes", s) + return 0, sizeNegative(s) } exact := f * float64(multiplier) @@ -101,17 +99,27 @@ func ParseSize(s string) (int64, error) { // accepted and came back as -9223372036854775808. ParseInt refuses it for // being out of range, so it reaches the float path, which does not. if exact >= float64(math.MaxInt64) { - return 0, SettingErrorf(SettingSize, "{setting} %q is too large to express in bytes", s) + return 0, sizeTooLarge(s) } n := int64(exact) if exact != math.Trunc(exact) { - return 0, SettingErrorf(SettingSize, + return 0, RefuseAbout(SettingSize, Says("core.SizeNotWhole", "{setting} %q is not a whole number of bytes: it works out to %.4f bytes. Ask for %d or %d instead", - s, exact, n, n+1) + A("Value", s), A("Exact", exact), A("Below", n), A("Above", n+1))) } return n, nil } +// sizeNegative and sizeTooLarge are two refusals ParseSize gives from two +// places each. +func sizeNegative(s string) error { + return RefuseAbout(SettingSize, Says("core.SizeNegative", "{setting} %q is negative: a file cannot be smaller than zero bytes", A("Value", s))) +} + +func sizeTooLarge(s string) error { + return RefuseAbout(SettingSize, Says("core.SizeTooLarge", "{setting} %q is too large to express in bytes", A("Value", s))) +} + // plainDecimal reports whether the text is digits, with at most one decimal // point and an optional leading sign. Nothing else counts as a number here. // @@ -173,7 +181,7 @@ func unitBytes(unit string) (int64, bool) { func ParseSizeRange(text string) (low, high int64, err error) { parts := strings.Split(text, "-") if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return 0, 0, fmt.Errorf("%q is not a range. A range is exactly two sizes with one hyphen between them, such as 1kb-8kb", text) + return 0, 0, Refuse(Says("core.RangeNotARange", "%q is not a range. A range is exactly two sizes with one hyphen between them, such as 1kb-8kb", A("Value", text))) } ends := [2]int64{} @@ -189,9 +197,9 @@ func ParseSizeRange(text string) (low, high int64, err error) { // is a reasonable thing to arrive at from a script that computes both ends, // so refusing it would be pedantry rather than protection. if ends[0] > ends[1] { - return 0, 0, fmt.Errorf( + return 0, 0, Refuse(Says("core.RangeBackwards", "the range runs backwards, from %d B down to %d B. The smaller end comes first, and a range that runs backwards holds no sizes at all. Write it as %d-%d", - ends[0], ends[1], ends[1], ends[0]) + A("From", ends[0]), A("To", ends[1]), A("Low", ends[1]), A("High", ends[0]))) } return ends[0], ends[1], nil } @@ -227,16 +235,16 @@ func ParseBoundary(s string) (int64, error) { // the limit to the smallest number there is, and the refusal that followed said // "TXT cannot be smaller than 0 B. Requested: -9223372036854775808 B" - an // answer about the bottom of the range to a question about the top of it. -var ErrBoundaryTooLarge = errors.New( - "a boundary set needs one size above the limit, and there is no number above this one. " + - "Use a limit at least one byte below the largest, or check that the number is the one you meant") +var ErrBoundaryTooLarge = Refuse(Says("core.BoundaryTooLarge", + "a boundary set needs one size above the limit, and there is no number above this one. "+ + "Use a limit at least one byte below the largest, or check that the number is the one you meant")) // The other end, in the two parts a report keeps apart. Built from them rather // than beside them, the same way ErrTooManyFiles is, so the sentence and the // parts cannot come to disagree. -const ( - BoundaryTooSmallWhy = "the set needs a size one byte below the limit, and there is nothing below zero" - BoundaryTooSmallFix = "Use a limit of at least 1 B" +var ( + BoundaryTooSmallWhy = Says("core.BoundaryTooSmallWhy", "the set needs a size one byte below the limit, and there is nothing below zero") + BoundaryTooSmallFix = Says("core.BoundaryTooSmallFix", "Use a limit of at least 1 B") ) // ErrBoundaryTooSmall is a limit with no room below it for the first file. @@ -249,7 +257,7 @@ const ( // this file already argued the same thing about ParseSizeRange and // ParseBoundary: two implementations of one rule are a place for them to // disagree. -var ErrBoundaryTooSmall = errors.New(BoundaryTooSmallWhy + ". " + BoundaryTooSmallFix) +var ErrBoundaryTooSmall = Refuse(Says("core.BoundaryTooSmall", "%s. %s", A("Why", BoundaryTooSmallWhy), A("Fix", BoundaryTooSmallFix))) // BoundarySizes turns a limit into the three sizes a boundary set means: one // byte under it, the limit itself, and one byte over. diff --git a/internal/core/system.go b/internal/core/system.go new file mode 100644 index 00000000..692a896a --- /dev/null +++ b/internal/core/system.go @@ -0,0 +1,39 @@ +package core + +import ( + "errors" + "io/fs" + "syscall" +) + +// SystemKind is what the system refused, by what the error means rather than +// by its words. +// +// The words are the system's, in the system's language - Windows answers in +// English when it can and in the language of the machine when it cannot - so +// both surfaces put a sentence of their own in their place, with the number +// beside it, which is the part that means the same everywhere. The command +// line has done that since 2026-09-25. The kinds are here so the window and +// the command line cannot come to tell them apart differently (O251). +type SystemKind int + +// The kinds a refusal of the system is told apart by. +const ( + SystemRefusedIt SystemKind = iota + SystemNothingThere + SystemNoPermission + SystemAlreadyThere +) + +// SystemKindOf is the kind of one error of the system. +func SystemKindOf(errno syscall.Errno) SystemKind { + switch { + case errors.Is(errno, fs.ErrNotExist): + return SystemNothingThere + case errors.Is(errno, fs.ErrPermission): + return SystemNoPermission + case errors.Is(errno, fs.ErrExist): + return SystemAlreadyThere + } + return SystemRefusedIt +} diff --git a/internal/damage/apply.go b/internal/damage/apply.go index 9203909b..42875eb2 100644 --- a/internal/damage/apply.go +++ b/internal/damage/apply.go @@ -83,7 +83,7 @@ func (d Descriptor) checkOne(p format.Property, raw string) error { return nil } why := p.Allows(raw) - if why == "" { + if why.IsZero() { return nil } return &format.PropertyValueError{ diff --git a/internal/damage/refusals.go b/internal/damage/refusals.go index f5690804..84f26838 100644 --- a/internal/damage/refusals.go +++ b/internal/damage/refusals.go @@ -1,7 +1,6 @@ package damage import ( - "fmt" "strings" "github.com/donislawdev/TestingFilesGenerator/internal/core" @@ -20,8 +19,10 @@ type UnknownError struct { } // What happened, without the list of names. -func (e *UnknownError) What() string { - return fmt.Sprintf("there is no damage called %q", e.ID) +func (e *UnknownError) What() string { return e.what().String() } + +func (e *UnknownError) what() core.Said { + return core.Says("damage.UnknownWhat", "there is no damage called %q", core.A("Damage", e.ID)) } // Why this is refused rather than skipped. @@ -29,23 +30,37 @@ func (e *UnknownError) What() string { // Silently ignoring it would produce an intact file whose manifest says it is // broken, which is untouchable rule 6 read backwards: the silence is not a // missing file but a missing hole in one. -func (e *UnknownError) Why() string { - return "a damage has to be one this build knows, and one it does not would leave the file intact while the manifest called it broken" +func (e *UnknownError) Why() string { return e.why().String() } + +func (e *UnknownError) why() core.Said { + return core.Says("damage.UnknownWhy", "a damage has to be one this build knows, and one it does not would leave the file intact while the manifest called it broken") } // Instead names what there is, from the registry rather than from a list. -func (e *UnknownError) Instead() string { +func (e *UnknownError) Instead() string { return e.instead().String() } + +func (e *UnknownError) instead() core.Said { if len(e.Known) == 0 { - return "remove the damage line" + return core.Says("damage.UnknownInsteadRemove", "remove the damage line") } - return "use one of: " + strings.Join(e.Known, ", ") + return core.Says("damage.UnknownInstead", "use one of: %s", core.A("Known", strings.Join(e.Known, ", "))) } -func (e *UnknownError) Error() string { +func (e *UnknownError) Error() string { return e.Said().String() } + +// Parts is what happened, why and what to do instead, for a reader that lays +// them out apart and in its own language. +func (e *UnknownError) Parts() (what, why, instead core.Said) { + return e.what(), e.why(), e.instead() +} + +// Said is the whole refusal, for a window that says it in its own language. +func (e *UnknownError) Said() core.Said { if len(e.Known) == 0 { - return e.What() + return e.what() } - return e.What() + ". This build has: " + strings.Join(e.Known, ", ") + return core.Says("damage.Unknown", "%s. This build has: %s", + core.A("What", e.what()), core.A("Known", strings.Join(e.Known, ", "))) } // TooSmallError is a file smaller than the damage it was given. @@ -65,22 +80,30 @@ type TooSmallError struct { // damage. func (e *TooSmallError) AboutSetting() string { return "size" } -func (e *TooSmallError) What() string { - return fmt.Sprintf("%s needs at least %s and the file is %s", - e.Damage, core.ExactBytes(e.Floor), core.ExactBytes(e.Requested)) +func (e *TooSmallError) What() string { return e.what().String() } + +func (e *TooSmallError) what() core.Said { + return core.Says("damage.TooSmallWhat", "%s needs at least %s and the file is %s", + core.A("Damage", e.Damage), core.A("Floor", core.ExactBytes(e.Floor)), + core.A("Requested", core.ExactBytes(e.Requested))) } func (e *TooSmallError) Why() string { - return "a file smaller than the damage would come out unchanged, and an unchanged file described as broken is the one thing this tool must not write" + return core.Says("damage.TooSmallWhy", "a file smaller than the damage would come out unchanged, and an unchanged file described as broken is the one thing this tool must not write").String() } -func (e *TooSmallError) Instead() string { - return fmt.Sprintf("Ask for %s or more, or take the damage off this target.", - core.ExactBytes(e.Floor)) +func (e *TooSmallError) Instead() string { return e.instead().String() } + +func (e *TooSmallError) instead() core.Said { + return core.Says("damage.TooSmallInstead", "Ask for %s or more, or take the damage off this target.", + core.A("Floor", core.ExactBytes(e.Floor))) } -func (e *TooSmallError) Error() string { - return e.What() + ". " + e.Instead() +func (e *TooSmallError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *TooSmallError) Said() core.Said { + return core.Says("damage.TooSmall", "%s. %s", core.A("What", e.what()), core.A("Instead", e.instead())) } // NoChangeError is a damage that ran and moved nothing. @@ -98,20 +121,27 @@ type NoChangeError struct { File string } -func (e *NoChangeError) What() string { - return fmt.Sprintf("%s changed nothing in %s", e.Damage, e.File) +func (e *NoChangeError) What() string { return e.what().String() } + +func (e *NoChangeError) what() core.Said { + return core.Says("damage.NoChangeWhat", "%s changed nothing in %s", core.A("Damage", e.Damage), core.A("File", e.File)) } -func (e *NoChangeError) Why() string { - return "the file would be accepted by every reader while the manifest called it broken, so the run stops rather than writing it" +func (e *NoChangeError) Why() string { return e.why().String() } + +func (e *NoChangeError) why() core.Said { + return core.Says("damage.NoChangeWhy", "the file would be accepted by every reader while the manifest called it broken, so the run stops rather than writing it") } func (e *NoChangeError) Instead() string { - return "give the damage a larger file, different settings, or take it off this target" + return core.Says("damage.NoChangeInstead", "give the damage a larger file, different settings, or take it off this target").String() } -func (e *NoChangeError) Error() string { - return e.What() + ". " + e.Why() +func (e *NoChangeError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *NoChangeError) Said() core.Said { + return core.Says("damage.NoChange", "%s. %s", core.A("What", e.what()), core.A("Why", e.why())) } // RuledOutExpectation is the one declared outcome a damaged file cannot have. @@ -177,18 +207,33 @@ type ExpectationConflictError struct { // disagrees with it. func (e *ExpectationConflictError) AboutSetting() string { return "expected" } -func (e *ExpectationConflictError) What() string { - return fmt.Sprintf("this target damages the file and expects %q", e.Outcome) +func (e *ExpectationConflictError) What() string { return e.what().String() } + +func (e *ExpectationConflictError) what() core.Said { + return core.Says("damage.ConflictWhat", "this target damages the file and expects %q", core.A("Outcome", core.Choice{Of: "expected", Value: e.Outcome})) } -func (e *ExpectationConflictError) Why() string { - return "a damaged file is one a judge was measured to refuse, so expecting it to be accepted is an expectation nothing could meet" +func (e *ExpectationConflictError) Why() string { return e.why().String() } + +func (e *ExpectationConflictError) why() core.Said { + return core.Says("damage.ConflictWhy", "a damaged file is one a judge was measured to refuse, so expecting it to be accepted is an expectation nothing could meet") } -func (e *ExpectationConflictError) Instead() string { - return "leave expected out and get reject, or write sanitize if the system under test is meant to repair the file, or unspecified if that is the question" +// Parts is what happened, why and what to do instead, for a reader that lays +// them out apart and in its own language. +func (e *ExpectationConflictError) Parts() (what, why, instead core.Said) { + return e.what(), e.why(), e.instead() } -func (e *ExpectationConflictError) Error() string { - return e.What() + ". " + e.Instead() +func (e *ExpectationConflictError) Instead() string { return e.instead().String() } + +func (e *ExpectationConflictError) instead() core.Said { + return core.Says("damage.ConflictInstead", "leave expected out and get reject, or write sanitize if the system under test is meant to repair the file, or unspecified if that is the question") +} + +func (e *ExpectationConflictError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *ExpectationConflictError) Said() core.Said { + return core.Says("damage.Conflict", "%s. %s", core.A("What", e.what()), core.A("Instead", e.instead())) } diff --git a/internal/engine/crash.go b/internal/engine/crash.go index 0ea8740f..9037fe89 100644 --- a/internal/engine/crash.go +++ b/internal/engine/crash.go @@ -6,6 +6,8 @@ import ( "io" "github.com/donislawdev/TestingFilesGenerator/internal/format" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // GeneratorCrashError is a generator that stopped the way a program stops when @@ -34,18 +36,40 @@ type GeneratorCrashError struct { Value any } -func (e *GeneratorCrashError) Error() string { +func (e *GeneratorCrashError) Error() string { return e.Said().String() } + +// Said is the whole sentence, for a window that says it in its own language. +// The value the generator stopped with is said as it came - it is for the +// report, the same as a defect's detail. +func (e *GeneratorCrashError) Said() core.Said { + return core.Says("engine.GeneratorCrash", + "the %s generator stopped with an internal error while %s: %v. "+ + "This is a defect in this tool rather than something a recipe can ask for. "+ + "Please report it with the settings that produced it", + core.A("Format", e.Format), core.A("While", e.while()), core.A("Value", fmt.Sprint(e.Value))) +} + +// while is the half of the work the generator stopped in. +func (e *GeneratorCrashError) while() core.Said { + switch { + case e.While == crashPlanning && e.Name == "": + return core.Says("engine.CrashPlanning", "planning a file") + case e.While == crashWriting && e.Name != "": + return core.Says("engine.CrashWriting", "writing %s", core.A("File", e.Name)) + } where := e.While if e.Name != "" { where += " " + e.Name } - return fmt.Sprintf( - "the %s generator stopped with an internal error while %s: %v. "+ - "This is a defect in this tool rather than something a recipe can ask for. "+ - "Please report it with the settings that produced it", - e.Format, where, e.Value) + return core.Says("engine.CrashWhile", "%s", core.A("While", where)) } +// The two halves of the work a generator can stop in. +const ( + crashPlanning = "planning a file" + crashWriting = "writing" +) + // planWithoutCrashing asks a generator what it would produce and turns a panic // into an ordinary error. // @@ -62,7 +86,7 @@ func planWithoutCrashing(desc format.Descriptor, r format.Request) (p format.Pla if v == nil { return } - err = &GeneratorCrashError{Format: desc.ID, While: "planning a file", Value: v} + err = &GeneratorCrashError{Format: desc.ID, While: crashPlanning, Value: v} }() return desc.Generator.Plan(r) } @@ -93,7 +117,7 @@ func writeWithoutCrashing(ctx context.Context, f PlannedFile, w io.Writer) (err if v == nil { return } - err = &GeneratorCrashError{Format: f.Desc.ID, While: "writing", Name: f.Name, Value: v} + err = &GeneratorCrashError{Format: f.Desc.ID, While: crashWriting, Name: f.Name, Value: v} }() return f.Desc.Generator.Write(ctx, w, f.Plan) } diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 828b8443..4482f151 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -195,6 +195,12 @@ type Result struct { // with nothing able to remove them. A refused run has nothing to record. Started bool + // NotesSaid is every note of the run as a sentence, by the English the + // manifest holds - one entry for each different note rather than one for + // each file, so a million files with the same note cost one. A window says + // the notes in its own language from here, and the manifest stays English. + NotesSaid map[string]core.Said + // reservation is the run's hold on its manifest name, taken before the // first file and handed to SaveRecord, which saves through it. Nil for a // dry run and for a run refused before it reserved anything. @@ -236,20 +242,20 @@ func (f PlannedFile) Damaged() bool { func settleTarget(t *Target, opt Options, seen map[string]bool) (format.Descriptor, error) { if t.ID == "" { return format.Descriptor{}, &RecipeError{Setting: SettingID, - Detail: "a target has no id", - Because: "every target needs a stable id, it anchors the seed and links to the manifest"} + Detail: core.Says("engine.NoID", "a target has no id"), + Because: core.Says("engine.NoIDWhy", "every target needs a stable id, it anchors the seed and links to the manifest")} } if seen[t.ID] { return format.Descriptor{}, &RecipeError{Setting: SettingID, - Detail: fmt.Sprintf("target id %q is used twice", t.ID), - Because: "ids identify targets, so a duplicate is an error rather than a silent overwrite"} + Detail: core.Says("engine.IDTwice", "target id %q is used twice", core.A("ID", t.ID)), + Because: core.Says("engine.IDTwiceWhy", "ids identify targets, so a duplicate is an error rather than a silent overwrite")} } seen[t.ID] = true if len(t.Sizes) == 0 { return format.Descriptor{}, &RecipeError{Setting: SettingCount, - Detail: fmt.Sprintf("target %q asks for 0 files", t.ID), - Remedy: "Ask for at least one"} + Detail: core.Says("engine.NoFiles", "target %q asks for 0 files", core.A("Target", t.ID)), + Remedy: core.Says("engine.NoFilesFix", "Ask for at least one")} } desc, err := format.Get(t.Format) @@ -360,7 +366,7 @@ func PlanContext(ctx context.Context, targets []Target, opt Options) ([]PlannedF // here would leave a manifest outside the directory the run was pointed // at, describing files that are not next to it. if opt.ManifestName != "" { - if err := checkFileName(SettingOutputManifest, "the manifest", opt.ManifestName); err != nil { + if err := checkFileName(SettingOutputManifest, core.Says("engine.TheManifest", "the manifest"), opt.ManifestName); err != nil { return nil, err } } @@ -384,7 +390,7 @@ func PlanContext(ctx context.Context, targets []Target, opt Options) ([]PlannedF // anything is written rather than after every file. if explained(targets) { name := instructionsNameOf(opt) - if err := checkFileName(SettingOutputManifest, "the instructions beside the manifest", name); err != nil { + if err := checkFileName(SettingOutputManifest, core.Says("engine.TheInstructions", "the instructions beside the manifest"), name); err != nil { return nil, err } pl.names[collisionKey(name)] = nameOwner{name: name, instructions: true} @@ -392,7 +398,7 @@ func PlanContext(ctx context.Context, targets []Target, opt Options) ([]PlannedF if opt.OutDir == "" { return nil, &RecipeError{Setting: SettingOutDir, - Detail: "the output directory is empty", + Detail: core.Says("engine.NoOutDir", "the output directory is empty"), // "or leave it out to use the current one" used to close this // sentence and it was removed on 2026-08-26. It is true of a // recipe file and of the command line, and it is a thing a person @@ -400,7 +406,7 @@ func PlanContext(ctx context.Context, targets []Target, opt Options) ([]PlannedF // they just did, and all three screens refuse it. One refusal // cannot carry two surfaces' advice, and the half that survives is // the half that is true on both. O125. - Remedy: "Name a directory, for example ./fixtures"} + Remedy: core.Says("engine.NoOutDirFix", "Name a directory, for example ./fixtures")} } for i := range targets { @@ -431,9 +437,9 @@ func PlanContext(ctx context.Context, targets []Target, opt Options) ([]PlannedF // this line "validate --json" carried no "at" for it. return nil, &RecipeError{ Setting: core.TargetAddress(i+1, SettingCount), - Detail: fmt.Sprintf("this run asks for %s across %s", - core.Count(totalFiles, "file", "files"), - core.Count(len(targets), "target", "targets")), + Detail: core.Says("engine.TooManyFiles", "this run asks for %s across %s", + core.A("Files", core.SaysN("engine.Files", "%d file", "%d files", core.A("Count", totalFiles))), + core.A("Targets", core.SaysN("engine.Targets", "%d target", "%d targets", core.A("Count", len(targets))))), Because: core.TooManyFilesWhy, Remedy: core.TooManyFilesFix} } @@ -505,14 +511,15 @@ func Run(ctx context.Context, files []PlannedFile, opt Options) (*Result, error) // separate one that can drift away from it. res.Started = true for _, f := range files { - m.Add(entryFor(f, "", false, nil)) + m.Add(res.entryFor(f, "", false, nil)) } m.Run.Complete = true return res, nil } if err := os.MkdirAll(opt.OutDir, 0o755); err != nil { - return res, fmt.Errorf("cannot create the output directory %s: %w", core.Shown(opt.OutDir), err) + return res, core.Refuse(core.Says("engine.CannotCreateOutDir", "cannot create the output directory %s: %w", + core.A("Dir", core.Shown(opt.OutDir)), core.A("Cause", err))) } // The directory is taken before the manifest name is, and the two are not @@ -533,7 +540,8 @@ func Run(ctx context.Context, files []PlannedFile, opt Options) (*Result, error) if errors.Is(err, fs.ErrExist) { return res, &RunInProgressError{Path: lockPath, Dir: opt.OutDir} } - return res, fmt.Errorf("cannot start a run in %s: %w", core.Shown(opt.OutDir), err) + return res, core.Refuse(core.Says("engine.CannotStart", "cannot start a run in %s: %w", + core.A("Dir", core.Shown(opt.OutDir)), core.A("Cause", err))) } // Given back however this run ends, including one stopped part way: the // signal cancels the context, Run returns, and this runs. What it cannot @@ -581,7 +589,7 @@ func Run(ctx context.Context, files []PlannedFile, opt Options) (*Result, error) for i, r := range written { switch { case r.ok: - m.Add(entryFor(files[i], r.sha, true, nil)) + m.Add(res.entryFor(files[i], r.sha, true, nil)) case r.err == nil: // Never started. A cancelled run leaves these behind and they are // neither a success nor a failure, so they get no entry - which is @@ -596,7 +604,7 @@ func Run(ctx context.Context, files []PlannedFile, opt Options) (*Result, error) // One file failing does not end the run. Nine thousand good // files are worth keeping, and the entry says what went wrong. res.Failures++ - m.Add(entryFor(files[i], "", false, r.err)) + m.Add(res.entryFor(files[i], "", false, r.err)) } } @@ -653,7 +661,8 @@ func reserveManifest(manifestPath, outDir string) (*manifest.Reservation, error) if errors.Is(err, fs.ErrExist) { return nil, &CollisionError{Path: manifestPath, Manifest: true} } - return nil, fmt.Errorf("cannot start a run in %s: %w", core.Shown(outDir), err) + return nil, core.Refuse(core.Says("engine.CannotStart", "cannot start a run in %s: %w", + core.A("Dir", core.Shown(outDir)), core.A("Cause", err))) } // claimRunLock takes the name that says this directory has a run in it. @@ -693,10 +702,10 @@ func releaseRunLock(path string, own os.FileInfo) { _ = core.RemoveOwn(path, own) } -func entryFor(f PlannedFile, sha string, materialized bool, failure error) manifest.File { +func (res *Result) entryFor(f PlannedFile, sha string, materialized bool, failure error) manifest.File { var notes []manifest.Note for _, n := range f.Plan.Notes { - notes = append(notes, manifest.Note{Code: n.Code, Detail: n.Detail}) + notes = append(notes, res.note(n.Code, n.Detail)) } label, _ := f.Plan.Properties[format.PropertyLabelEmbedded].(bool) @@ -727,14 +736,25 @@ func entryFor(f PlannedFile, sha string, materialized bool, failure error) manif e.Failed = true e.Error = failure.Error() e.Materialized = false - e.Notes = append(e.Notes, manifest.Note{ - Code: "generation_failed", - Detail: "This file was not produced. The run carried on and ended with the partial exit code.", - }) + e.Notes = append(e.Notes, res.note("generation_failed", + core.Says("engine.NotProduced", "This file was not produced. The run carried on and ended with the partial exit code."))) } return e } +// note is one note for the manifest, remembered as a sentence once for each +// different thing it says. +func (res *Result) note(code string, said core.Said) manifest.Note { + detail := said.String() + if res.NotesSaid == nil { + res.NotesSaid = map[string]core.Said{} + } + if _, have := res.NotesSaid[detail]; !have { + res.NotesSaid[detail] = said + } + return manifest.Note{Code: code, Detail: detail} +} + func expectationFor(f PlannedFile) manifest.Expected { switch f.Target.Expected { case "": @@ -814,11 +834,13 @@ func renderName(t *Target, d format.Descriptor, index int) (string, error) { // numbering they asked for. if strings.Contains(name, "{") { return "", &RecipeError{Setting: SettingName, - Detail: fmt.Sprintf("target %q has a name template this build does not understand: %q", t.ID, tmpl), - Remedy: fmt.Sprintf("The only placeholder is %s, so a name looks like invoice_%s.pdf", indexToken, indexToken)} + Detail: core.Says("engine.NameTemplateUnknown", "target %q has a name template this build does not understand: %q", + core.A("Target", t.ID), core.A("Template", tmpl)), + Remedy: core.Says("engine.NameTemplateFix", "The only placeholder is %s, so a name looks like invoice_%s.pdf", + core.A("Placeholder", indexToken), core.A("Example", indexToken))} } - if err := checkFileName(SettingName, fmt.Sprintf("target %q", t.ID), name); err != nil { + if err := checkFileName(SettingName, core.Says("engine.Target", "target %q", core.A("Target", t.ID)), name); err != nil { return "", err } return name, nil diff --git a/internal/engine/errors.go b/internal/engine/errors.go index b3e04c34..f36ac80c 100644 --- a/internal/engine/errors.go +++ b/internal/engine/errors.go @@ -2,7 +2,6 @@ package engine import ( "errors" - "fmt" "github.com/donislawdev/TestingFilesGenerator/internal/core" ) @@ -24,9 +23,9 @@ type RecipeError struct { // reading it does not have to take a sentence written for a person back to // pieces. Both may be empty, and then the whole refusal is in Detail and // reads exactly as it did before either field existed. - Detail string - Because string - Remedy string + Detail core.Said + Because core.Said + Remedy core.Said // Setting is which setting the refusal is about, where it is about one. // @@ -64,14 +63,22 @@ func (e *RecipeError) InTheWordsOf(name string) string { if name == "" { name = core.LastSettingSegment(e.Setting) } - message := e.Detail - if e.Because != "" { - message += " - " + e.Because - } - if e.Remedy != "" { - message += ". " + e.Remedy + return core.InTheWordsOf(e.Said().String(), name) +} + +// Said is the whole refusal, for a window that says it in its own language, +// with the setting slot left for the window to fill. +func (e *RecipeError) Said() core.Said { + switch { + case !e.Because.IsZero() && !e.Remedy.IsZero(): + return core.Says("engine.RefusalWhyFix", "%s - %s. %s", + core.A("What", e.Detail), core.A("Why", e.Because), core.A("Fix", e.Remedy)) + case !e.Because.IsZero(): + return core.Says("engine.RefusalWhy", "%s - %s", core.A("What", e.Detail), core.A("Why", e.Because)) + case !e.Remedy.IsZero(): + return core.Says("engine.RefusalFix", "%s. %s", core.A("What", e.Detail), core.A("Fix", e.Remedy)) } - return core.InTheWordsOf(message, name) + return e.Detail } // The three parts a report keeps apart, answering the same names the format and @@ -82,15 +89,15 @@ func (e *RecipeError) InTheWordsOf(name string) string { // spoken here - UnknownPropertyError has answered to them since the recipe // reader started reporting four parts. func (e *RecipeError) What() string { - return core.InTheWordsOf(e.Detail, core.LastSettingSegment(e.Setting)) + return core.InTheWordsOf(e.Detail.String(), core.LastSettingSegment(e.Setting)) } func (e *RecipeError) Why() string { - return core.InTheWordsOf(e.Because, core.LastSettingSegment(e.Setting)) + return core.InTheWordsOf(e.Because.String(), core.LastSettingSegment(e.Setting)) } func (e *RecipeError) Instead() string { - return core.InTheWordsOf(e.Remedy, core.LastSettingSegment(e.Setting)) + return core.InTheWordsOf(e.Remedy.String(), core.LastSettingSegment(e.Setting)) } // AboutSetting lets a window place this message without knowing this type. @@ -202,10 +209,13 @@ type SpaceError struct { Path string } -func (e *SpaceError) Error() string { - return fmt.Sprintf( +func (e *SpaceError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. +func (e *SpaceError) Said() core.Said { + return core.Says("engine.NoSpace", "this run needs %d B and %s has %d B free - nothing was written. Ask for fewer files or a smaller size, or write to another disk by changing the output directory", - e.Needed, core.Shown(e.Path), e.Available) + core.A("Needed", e.Needed), core.A("Dir", core.Shown(e.Path)), core.A("Free", e.Available)) } // RunInProgressError is refusing to start because another run holds this @@ -225,10 +235,13 @@ type RunInProgressError struct { Dir string } -func (e *RunInProgressError) Error() string { - return fmt.Sprintf( +func (e *RunInProgressError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. +func (e *RunInProgressError) Said() core.Said { + return core.Says("engine.RunInProgress", "another run is already writing into %s, so this one will not start. Two runs writing into one directory can write over each other's files without either of them saying so. Wait for it to finish, or generate into a different directory. If nothing is running, that run was killed before it could tidy up - remove %s and try again", - core.Shown(e.Dir), core.Shown(e.Path)) + core.A("Dir", core.Shown(e.Dir)), core.A("Lock", core.Shown(e.Path))) } // CollisionError is refusing to write over something that is already there. @@ -248,18 +261,21 @@ type CollisionError struct { Instructions bool } -func (e *CollisionError) Error() string { +func (e *CollisionError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. +func (e *CollisionError) Said() core.Said { if e.Instructions { - return fmt.Sprintf( + return core.Says("engine.CollisionInstructions", "%s already exists and this run will not write over it. It says what the files of an earlier run are for. Generate into an empty directory, or name this run's manifest something else", - core.Shown(e.Path)) + core.A("Path", core.Shown(e.Path))) } if e.Manifest { - return fmt.Sprintf( + return core.Says("engine.CollisionManifest", "%s already exists and this run will not write over it. It is the only record of what an earlier run wrote, so replacing it would leave those files with nothing to remove them by. Generate into an empty directory, or move the old manifest aside", - core.Shown(e.Path)) + core.A("Path", core.Shown(e.Path))) } - return fmt.Sprintf( + return core.Says("engine.Collision", "%s already exists and this run will not write over it. Generate into an empty directory, or remove the file first", - core.Shown(e.Path)) + core.A("Path", core.Shown(e.Path))) } diff --git a/internal/engine/filename.go b/internal/engine/filename.go index f8803de5..92cfc06d 100644 --- a/internal/engine/filename.go +++ b/internal/engine/filename.go @@ -1,7 +1,6 @@ package engine import ( - "fmt" "path/filepath" "strings" @@ -24,16 +23,16 @@ import ( // Both separators are refused on every system, not just the local one. A name // holding a backslash is legal on Linux and cannot exist on Windows, and a // recipe that only works on the machine it was written on is not portable. -func checkFileName(setting, where, name string) error { +func checkFileName(setting string, where core.Said, name string) error { switch { case name == "": - return &RecipeError{Setting: setting, Detail: fmt.Sprintf("%s produces a file with no name", where)} + return &RecipeError{Setting: setting, Detail: core.Says("engine.NameEmpty", "%s produces a file with no name", core.A("Where", where))} case strings.ContainsAny(name, `/\`): return &RecipeError{Setting: setting, - Detail: fmt.Sprintf("%s produces the name %q, which is a path rather than a file name", where, name), - Because: "names stay inside the output directory, and a separator is refused on every system so that a recipe works everywhere", - Remedy: "Choose the directory with the output setting instead"} + Detail: core.Says("engine.NameIsAPath", "%s produces the name %q, which is a path rather than a file name", core.A("Where", where), core.A("Name", name)), + Because: core.Says("engine.NameIsAPathWhy", "names stay inside the output directory, and a separator is refused on every system so that a recipe works everywhere"), + Remedy: chooseTheDirectory()} // A colon, on every system, for the same reason as a separator. // @@ -51,9 +50,9 @@ func checkFileName(setting, where, name string) error { // somebody else's is worse than one refused on all of them. case strings.Contains(name, ":"): return &RecipeError{Setting: setting, - Detail: fmt.Sprintf("%s produces the name %q, which holds a colon", where, name), - Because: "Windows reads that as a drive or as an alternate data stream rather than as part of the name, so the file arrives called something else or not at all. It is refused on every system so that a recipe means one thing everywhere", - Remedy: "Take the colon out, or ask for the file inside an archive where the name survives"} + Detail: core.Says("engine.NameColon", "%s produces the name %q, which holds a colon", core.A("Where", where), core.A("Name", name)), + Because: core.Says("engine.NameColonWhy", "Windows reads that as a drive or as an alternate data stream rather than as part of the name, so the file arrives called something else or not at all. It is refused on every system so that a recipe means one thing everywhere"), + Remedy: core.Says("engine.NameColonFix", "Take the colon out, or ask for the file inside an archive where the name survives")} // Characters Windows will not put in a file name, refused on every system // for the same reason as the separator and the colon above. @@ -76,9 +75,10 @@ func checkFileName(setting, where, name string) error { case firstForbidden(name) != 0: bad := firstForbidden(name) return &RecipeError{Setting: setting, - Detail: fmt.Sprintf("%s produces the name %q, which holds %s", where, name, describeForbidden(bad)), - Because: "Windows refuses that character in a file name, so the file is not written there at all. It is refused on every system so that a recipe means one thing everywhere", - Remedy: "Take the character out, or ask for the file inside an archive where the name survives"} + Detail: core.Says("engine.NameForbidden", "%s produces the name %q, which holds %s", + core.A("Where", where), core.A("Name", name), core.A("Character", describeForbidden(bad))), + Because: core.Says("engine.NameForbiddenWhy", "Windows refuses that character in a file name, so the file is not written there at all. It is refused on every system so that a recipe means one thing everywhere"), + Remedy: core.Says("engine.NameForbiddenFix", "Take the character out, or ask for the file inside an archive where the name survives")} // Longer than every system this runs on will store, refused on every // system for the same reason as the characters above. @@ -92,10 +92,12 @@ func checkFileName(setting, where, name string) error { // bytes is the one that holds on all three (O239). case len(name) > core.MaxNameBytes: return &RecipeError{Setting: setting, - Detail: fmt.Sprintf("%s produces the name %q, which is %d bytes long", where, name, len(name)), - Because: fmt.Sprintf("Linux stores at most %d bytes in a file name, and a letter outside ASCII takes two to four of them. "+ - "It is refused on every system so that a recipe means one thing everywhere", core.MaxNameBytes), - Remedy: fmt.Sprintf("Shorten it to %d bytes or fewer, or ask for the file inside an archive where the name survives", core.MaxNameBytes)} + Detail: core.Says("engine.NameTooLong", "%s produces the name %q, which is %d bytes long", + core.A("Where", where), core.A("Name", name), core.A("Length", len(name))), + Because: core.Says("engine.NameTooLongWhy", "Linux stores at most %d bytes in a file name, and a letter outside ASCII takes two to four of them. "+ + "It is refused on every system so that a recipe means one thing everywhere", core.A("Most", core.MaxNameBytes)), + Remedy: core.Says("engine.NameTooLongFix", "Shorten it to %d bytes or fewer, or ask for the file inside an archive where the name survives", + core.A("Most", core.MaxNameBytes))} // One reserved device name, and one only. // @@ -119,13 +121,13 @@ func checkFileName(setting, where, name string) error { // describes either. case strings.EqualFold(name, "nul"): return &RecipeError{Setting: setting, - Detail: fmt.Sprintf("%s produces the name %q, which names the null device on Windows rather than a file", where, name), - Because: "writing there succeeds and the bytes go nowhere, so the run would record a file that is not on the disk. It is refused on every system so that a recipe means one thing everywhere", - Remedy: "Give it an extension, nul.txt is an ordinary name, or choose another one"} + Detail: core.Says("engine.NameNul", "%s produces the name %q, which names the null device on Windows rather than a file", core.A("Where", where), core.A("Name", name)), + Because: core.Says("engine.NameNulWhy", "writing there succeeds and the bytes go nowhere, so the run would record a file that is not on the disk. It is refused on every system so that a recipe means one thing everywhere"), + Remedy: core.Says("engine.NameNulFix", "Give it an extension, nul.txt is an ordinary name, or choose another one")} case name == "." || name == "..": - return &RecipeError{Setting: setting, Detail: fmt.Sprintf( - "%s produces the name %q, which names a directory rather than a file", where, name)} + return &RecipeError{Setting: setting, Detail: core.Says("engine.NameIsADirectory", + "%s produces the name %q, which names a directory rather than a file", core.A("Where", where), core.A("Name", name))} // A name Windows stores under a different name than the one it was given. // Refused on every system for the same reason a separator is: a recipe that @@ -142,9 +144,9 @@ func checkFileName(setting, where, name string) error { // host filesystem for exactly this reason. See D10. case strings.HasSuffix(name, ".") || strings.HasSuffix(name, " "): return &RecipeError{Setting: setting, - Detail: fmt.Sprintf("%s produces the name %q, which ends in a dot or a space", where, name), - Because: "Windows stores such a name without it, so the file on disk would not be the file the manifest describes and verify would report both", - Remedy: "Take the last character off, or ask for the file inside an archive where the name survives"} + Detail: core.Says("engine.NameTrailing", "%s produces the name %q, which ends in a dot or a space", core.A("Where", where), core.A("Name", name)), + Because: core.Says("engine.NameTrailingWhy", "Windows stores such a name without it, so the file on disk would not be the file the manifest describes and verify would report both"), + Remedy: core.Says("engine.NameTrailingFix", "Take the last character off, or ask for the file inside an archive where the name survives")} // Judged the same way on every system, like the separator above. Using // filepath here asks the machine this build runs on, and the answer @@ -154,13 +156,18 @@ func checkFileName(setting, where, name string) error { // prevent, arriving through the rule itself. case filepath.IsAbs(name) || core.HasVolumeName(name): return &RecipeError{Setting: setting, - Detail: fmt.Sprintf("%s produces the absolute path %q", where, name), - Because: "a recipe carries no absolute paths, because then it only works on the machine it was written on", - Remedy: "Choose the directory with the output setting instead"} + Detail: core.Says("engine.NameAbsolute", "%s produces the absolute path %q", core.A("Where", where), core.A("Name", name)), + Because: core.Says("engine.NameAbsoluteWhy", "a recipe carries no absolute paths, because then it only works on the machine it was written on"), + Remedy: chooseTheDirectory()} } return nil } +// chooseTheDirectory is the way out of a name that tries to be a place. +func chooseTheDirectory() core.Said { + return core.Says("engine.ChooseTheDirectory", "Choose the directory with the output setting instead") +} + // forbiddenChars are the printable characters Windows refuses in a file name. // // The separator and the colon are not here. They are refused above with their @@ -190,9 +197,9 @@ func firstForbidden(name string) rune { // describeForbidden names a character in a way somebody can act on. A control // character has nothing to show, so it is given as its number instead of being // printed into the middle of a sentence where it would do what it says. -func describeForbidden(r rune) string { +func describeForbidden(r rune) core.Said { if r < 0x20 { - return fmt.Sprintf("a control character, U+%04X", r) + return core.Says("engine.ControlCharacter", "a control character, U+%04X", core.A("Code", r)) } - return fmt.Sprintf("the character %q", string(r)) + return core.Says("engine.Character", "the character %q", core.A("Character", string(r))) } diff --git a/internal/engine/names.go b/internal/engine/names.go index 85b9fad1..a088ceae 100644 --- a/internal/engine/names.go +++ b/internal/engine/names.go @@ -1,7 +1,6 @@ package engine import ( - "fmt" "strings" "golang.org/x/text/unicode/norm" @@ -52,19 +51,19 @@ func claimFileName(names map[string]nameOwner, position int, id, name string) er if owner.manifest { return &RecipeError{ Setting: core.TargetAddress(position, SettingName), - Detail: fmt.Sprintf("target %q produces a file named %s, and that is the name this run gives its manifest", - id, core.Shown(name)), - Because: "both are written into the output directory, so the file would take the name the manifest needs and the run would end with files and nothing to remove them by", - Remedy: "Give the target a name template containing " + indexToken + ", or name the manifest something else", + Detail: core.Says("engine.NameIsTheManifest", "target %q produces a file named %s, and that is the name this run gives its manifest", + core.A("Target", id), core.A("Name", core.Shown(name))), + Because: core.Says("engine.NameIsTheManifestWhy", "both are written into the output directory, so the file would take the name the manifest needs and the run would end with files and nothing to remove them by"), + Remedy: nameTheManifestElse(), } } if owner.instructions { return &RecipeError{ Setting: core.TargetAddress(position, SettingName), - Detail: fmt.Sprintf("target %q produces a file named %s, and that is the name this run gives the instructions beside its manifest", - id, core.Shown(name)), - Because: "both are written into the output directory, and the instructions say what every file of the run is for, so one of the two would be lost", - Remedy: "Give the target a name template containing " + indexToken + ", or name the manifest something else", + Detail: core.Says("engine.NameIsTheInstructions", "target %q produces a file named %s, and that is the name this run gives the instructions beside its manifest", + core.A("Target", id), core.A("Name", core.Shown(name))), + Because: core.Says("engine.NameIsTheInstructionsWhy", "both are written into the output directory, and the instructions say what every file of the run is for, so one of the two would be lost"), + Remedy: nameTheManifestElse(), } } // No address, deliberately, and this is the one refusal here that keeps it. @@ -74,7 +73,14 @@ func claimFileName(names map[string]nameOwner, position int, id, name string) er // needs and what a window cannot place either way. return &RecipeError{ Detail: collisionDetail(owner, id, name), - Remedy: "Give one of them a name template containing " + indexToken} + Remedy: core.Says("engine.NamesClashFix", "Give one of them a name template containing %s", core.A("Placeholder", indexToken))} +} + +// nameTheManifestElse is the way out of a file that takes a name the record +// of the run needs. +func nameTheManifestElse() core.Said { + return core.Says("engine.NameTheManifestElse", "Give the target a name template containing %s, or name the manifest something else", + core.A("Placeholder", indexToken)) } // collisionKey is the spelling two names are compared under. Two names sharing @@ -144,16 +150,17 @@ func collisionKey(name string) string { // collide can print identically on screen, so a refusal that only shows them is // one the reader cannot act on. A refusal has to say what is wrong, what is // allowed and what to do instead. -func collisionDetail(owner nameOwner, id, name string) string { +func collisionDetail(owner nameOwner, id, name string) core.Said { switch { case owner.name == name: - return fmt.Sprintf("targets %q and %q both produce a file named %s", owner.id, id, core.Shown(name)) + return core.Says("engine.NamesClash", "targets %q and %q both produce a file named %s", + core.A("First", owner.id), core.A("Second", id), core.A("Name", core.Shown(name))) // Spelling before case, because normalising does not touch case and so a // pair that survives this one really is a difference of case. case norm.NFC.String(owner.name) == norm.NFC.String(name): - return fmt.Sprintf( + return core.Says("engine.NamesClashSpelling", "targets %q and %q produce the names %s and %s. Those print the same because they are one name spelled two ways, an accented letter against the plain letter with its accent as a separate character. macOS stores both under one name, so one file would be written over the other and the manifest would describe both", - owner.id, id, core.Shown(owner.name), core.Shown(name)) + core.A("First", owner.id), core.A("Second", id), core.A("FirstName", core.Shown(owner.name)), core.A("SecondName", core.Shown(name))) // Lowercasing rather than strings.EqualFold, and a guard caught the // difference on 2026-08-26. EqualFold folds simply, which puts the LONG s // in the same orbit as s - so "maſs.txt" against "mass.txt" was answered @@ -163,17 +170,17 @@ func collisionDetail(owner nameOwner, id, name string) string { // //lint:ignore SA6005 EqualFold is the faster comparison and the wrong one here. It folds simply, which is a wider question than the one this branch asks, and the sentence it leads to would then be false about two names that are not one name in two sizes. case strings.ToLower(norm.NFC.String(owner.name)) == strings.ToLower(norm.NFC.String(name)): - return fmt.Sprintf( + return core.Says("engine.NamesClashCase", "targets %q and %q produce the names %s and %s, which differ only in case. Most filesystems treat those as one file, so one would be written over the other and the manifest would describe both", - owner.id, id, core.Shown(owner.name), core.Shown(name)) + core.A("First", owner.id), core.A("Second", id), core.A("FirstName", core.Shown(owner.name)), core.A("SecondName", core.Shown(name))) default: // The fourth kind, unreachable until collisionKey started folding on // 2026-08-26. It is not a difference of case and not a difference of // spelling, so both sentences above would have been false about it - // and the one it would have fallen into says the two names differ by an // accent, which is worse than saying nothing. - return fmt.Sprintf( + return core.Says("engine.NamesClashFolded", "targets %q and %q produce the names %s and %s. Those are different letters that mean the same one - the sharp s against ss, the long s against s, a ligature against the letters in it. macOS stores both under one name, so one file would be written over the other and the manifest would describe both", - owner.id, id, core.Shown(owner.name), core.Shown(name)) + core.A("First", owner.id), core.A("Second", id), core.A("FirstName", core.Shown(owner.name)), core.A("SecondName", core.Shown(name))) } } diff --git a/internal/engine/parallel.go b/internal/engine/parallel.go index a621194f..1eb30761 100644 --- a/internal/engine/parallel.go +++ b/internal/engine/parallel.go @@ -392,8 +392,8 @@ func writeOne(ctx context.Context, f PlannedFile, outDir string, p *fileProgress // never reaches its final name. if counter.n != f.Plan.Bytes { _ = os.Remove(tmp) - return "", fmt.Errorf("generator for %s produced %d B where the plan said %d B", - f.Desc.ID, counter.n, f.Plan.Bytes) + return "", core.Defect(fmt.Errorf("generator for %s produced %d B where the plan said %d B", + f.Desc.ID, counter.n, f.Plan.Bytes)) } // Given its name only while nobody holds it. Preflight refused every name diff --git a/internal/engine/planmemory.go b/internal/engine/planmemory.go index e9afd64d..3fc2be08 100644 --- a/internal/engine/planmemory.go +++ b/internal/engine/planmemory.go @@ -1,7 +1,6 @@ package engine import ( - "fmt" "runtime" "runtime/metrics" @@ -181,10 +180,10 @@ func (p *planMemory) account(targetIndex, filesSoFar int) error { return &RecipeError{ Setting: core.TargetAddress(targetIndex, SettingCount), - Detail: fmt.Sprintf("the plan for this run has reached %s after %s and the ceiling is %s", - core.HumanBytes(int64(used)), - core.Count(filesSoFar, "file", "files"), - core.HumanBytes(int64(p.ceiling))), + Detail: core.Says("engine.PlanTooLarge", "the plan for this run has reached %s after %s and the ceiling is %s", + core.A("Used", core.Bytes(used)), + core.A("Files", core.SaysN("engine.FilesSoFar", "%d file", "%d files", core.A("Count", filesSoFar))), + core.A("Ceiling", core.Bytes(p.ceiling))), Because: core.PlanTooLargeWhy, Remedy: core.PlanTooLargeFix, } diff --git a/internal/engine/plantarget.go b/internal/engine/plantarget.go index a9f0d9cc..70b664cc 100644 --- a/internal/engine/plantarget.go +++ b/internal/engine/plantarget.go @@ -58,7 +58,7 @@ func (pl *planning) files(ctx context.Context, t *Target, desc format.Descriptor if idx < len(t.SizeMoved) && t.SizeMoved[idx] { p.Notes = append(p.Notes, format.Note{ Code: "size_moved", - Detail: "A size drawn from the range is not one this format can write, so the nearest size it can write was used instead. The file is still inside the range that was asked for.", + Detail: core.Says("engine.SizeMoved", "A size drawn from the range is not one this format can write, so the nearest size it can write was used instead. The file is still inside the range that was asked for."), }) } @@ -115,8 +115,8 @@ func (pl *planning) files(ctx context.Context, t *Target, desc format.Descriptor // change belongs to a target. return &RecipeError{ Setting: core.TargetAddress(position, format.SettingSize), - Detail: fmt.Sprintf("target %q brings the run to a size that is too large to measure", t.ID), - Because: err.Error()} + Detail: core.Says("engine.RunTooLarge", "target %q brings the run to a size that is too large to measure", core.A("Target", t.ID)), + Because: core.SaidOf(err)} } pl.out = append(pl.out, PlannedFile{ diff --git a/internal/engine/preflight.go b/internal/engine/preflight.go index 9ed04f76..668b3a56 100644 --- a/internal/engine/preflight.go +++ b/internal/engine/preflight.go @@ -51,8 +51,8 @@ func preflight(ctx context.Context, files []PlannedFile, opt Options) error { // "missing", "no permission" and "already there". if info, err := os.Stat(opt.OutDir); err == nil && !info.IsDir() { return &RecipeError{Setting: SettingOutDir, - Detail: fmt.Sprintf("the output directory %s is a file, not a directory", core.Shown(opt.OutDir)), - Remedy: "Point the output directory at a directory, or at one that does not exist yet and it will be created"} + Detail: core.Says("engine.OutDirIsAFile", "the output directory %s is a file, not a directory", core.A("Dir", core.Shown(opt.OutDir))), + Remedy: core.Says("engine.OutDirIsAFileFix", "Point the output directory at a directory, or at one that does not exist yet and it will be created")} } // Whether anybody else is writing here comes before every other question diff --git a/internal/engine/record.go b/internal/engine/record.go index fc82a660..6dd375f4 100644 --- a/internal/engine/record.go +++ b/internal/engine/record.go @@ -1,7 +1,6 @@ package engine import ( - "fmt" "os" "path/filepath" @@ -28,8 +27,12 @@ type InstructionsError struct { Err error } -func (e *InstructionsError) Error() string { - return fmt.Sprintf("cannot write the instructions to %s: %v", core.Shown(e.Path), e.Err) +func (e *InstructionsError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. +func (e *InstructionsError) Said() core.Said { + return core.Says("engine.InstructionsNotWritten", "cannot write the instructions to %s: %v", + core.A("Path", core.Shown(e.Path)), core.A("Cause", e.Err)) } func (e *InstructionsError) Unwrap() error { return e.Err } diff --git a/internal/format/archive/archive.go b/internal/format/archive/archive.go index 88a16570..35d426b9 100644 --- a/internal/format/archive/archive.go +++ b/internal/format/archive/archive.go @@ -268,7 +268,7 @@ func Groups(id string, r format.Request) ([]format.Content, error) { if r.SizeFromContents { // Only reachable if a caller sets the flag without contents. Saying so // beats producing an empty archive and calling it the answer. - return nil, fmt.Errorf("%s: the size was left to the contents and there are none", id) + return nil, core.Defect(fmt.Errorf("%s: the size was left to the contents and there are none", id)) } entries, err := intProperty(id, r.Properties, Entries, defaultEntries, 0, maxEntries) @@ -333,8 +333,8 @@ func tooMany(id, key string, asked int) *format.PropertyValueError { Format: id, Key: key, Value: strconv.Itoa(asked), - Reason: fmt.Sprintf("it takes a whole number from 0 to %d", maxEntries), - Remedy: fmt.Sprintf("Ask for %d entries or fewer.", maxEntries), + Reason: core.Says("format.ItTakesAWholeNumberFrom", "it takes a whole number from 0 to %d", core.A("MaxEntries", maxEntries)), + Remedy: core.Says("format.AskForEntriesOrFewer", "Ask for %d entries or fewer.", core.A("MaxEntries", maxEntries)), } } @@ -346,7 +346,7 @@ func sizeProperty(id string, props map[string]string, key string, fallback int64 } n, err := core.ParseSize(raw) if err != nil { - return 0, fmt.Errorf("%s: %s: %w", id, key, err) + return 0, core.Defect(fmt.Errorf("%s: %s: %w", id, key, err)) } return n, nil } @@ -358,10 +358,10 @@ func intProperty(id string, props map[string]string, key string, fallback, min, } n, err := strconv.Atoi(raw) if err != nil { - return 0, fmt.Errorf("%s: %s must be a whole number, got %q", id, key, raw) + return 0, core.Defect(fmt.Errorf("%s: %s must be a whole number, got %q", id, key, raw)) } if n < min || n > max { - return 0, fmt.Errorf("%s: %s must be between %d and %d, got %d", id, key, min, max, n) + return 0, core.Defect(fmt.Errorf("%s: %s must be between %d and %d, got %d", id, key, min, max, n)) } return n, nil } diff --git a/internal/format/archive/compression.go b/internal/format/archive/compression.go index 915e387f..094329e1 100644 --- a/internal/format/archive/compression.go +++ b/internal/format/archive/compression.go @@ -4,6 +4,8 @@ import ( "compress/flate" "github.com/donislawdev/TestingFilesGenerator/internal/format" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // How hard the archive is squeezed, in one vocabulary for both containers. @@ -91,7 +93,7 @@ func ReadCompression(id string, r format.Request, locked bool) (Squeeze, error) // every declared word is put through this function and has to be // accepted. Reason: axes[Compression].Allows(raw), - Remedy: "Ask for " + CompressNone + " to store the files as they are.", + Remedy: core.Says("archive.AskForNone", "Ask for %s to store the files as they are.", core.A("None", core.Choice{Of: Compression, Value: CompressNone})), } } s := Squeeze{Name: raw, Level: level} @@ -104,8 +106,9 @@ func ReadCompression(id string, r format.Request, locked bool) (Squeeze, error) Format: id, Key: Compression, Value: raw, - Reason: "the size is being left to the contents, and how far they compress is only known once they have been compressed", - Remedy: "Give the archive an explicit size, or ask for " + Compression + ": " + CompressNone + ".", + Reason: core.Says("format.TheSizeIsBeingLeftTo", "the size is being left to the contents, and how far they compress is only known once they have been compressed"), + Remedy: core.Says("archive.ExplicitSizeOrNone", "Give the archive an explicit size, or ask for %s: %s.", + core.A("Compression", core.LabelTerm(Compression)), core.A("None", core.Choice{Of: Compression, Value: CompressNone})), } } if locked { @@ -113,8 +116,11 @@ func ReadCompression(id string, r format.Request, locked bool) (Squeeze, error) Format: id, Key: Compression, Value: raw, - Reason: "the archive is locked with a " + Password + ", and a locked entry states its length before its data is written - so a compressed one would have to be held in memory whole", - Remedy: "Ask for " + Compression + ": " + CompressNone + ", or take the " + Password + " off.", + Reason: core.Says("archive.LockedNoCompression", "the archive is locked with a %s, and a locked entry states its length before its data is written - so a compressed one would have to be held in memory whole", + core.A("Password", core.LabelTerm(Password))), + Remedy: core.Says("archive.LockedNoCompressionFix", "Ask for %s: %s, or take the %s off.", + core.A("Compression", core.LabelTerm(Compression)), core.A("None", core.Choice{Of: Compression, Value: CompressNone}), + core.A("Password", core.LabelTerm(Password))), } } return s, nil diff --git a/internal/format/archive/layout.go b/internal/format/archive/layout.go index 4c0a67b7..46ee1df2 100644 --- a/internal/format/archive/layout.go +++ b/internal/format/archive/layout.go @@ -5,6 +5,8 @@ import ( "strings" "github.com/donislawdev/TestingFilesGenerator/internal/format" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // Where the files inside an archive sit, and whether the archive lists the @@ -180,8 +182,9 @@ func ReadLayout(id string, r format.Request) (Layout, error) { Format: id, Key: DirectoryEntries, Value: "true", - Reason: "a flat archive has no directories to list, and " + Depth + " is 0", - Remedy: "Ask for " + Depth + " of 1 or more, or leave " + DirectoryEntries + " off.", + Reason: core.Says("archive.FlatNoDirectories", "a flat archive has no directories to list, and %s is 0", core.A("Depth", core.LabelTerm(Depth))), + Remedy: core.Says("archive.FlatNoDirectoriesFix", "Ask for %s of 1 or more, or leave %s off.", + core.A("Depth", core.LabelTerm(Depth)), core.A("Directories", core.LabelTerm(DirectoryEntries))), } } return Layout{Depth: depth, DirEntries: dirs}, nil @@ -204,8 +207,8 @@ func boolProperty(id string, props map[string]string, key string, fallback bool) Format: id, Key: key, Value: raw, - Reason: "it takes true or false", - Remedy: "Write " + key + ": true or " + key + ": false.", + Reason: core.Says("format.ItTakesTrueOrFalse", "it takes true or false"), + Remedy: core.Says("archive.WriteTrueOrFalse", "Write %s: true or %s: false.", core.A("Key", key), core.A("Again", key)), } } return v, nil diff --git a/internal/format/archive/lock.go b/internal/format/archive/lock.go index 3552f2f0..efc5073d 100644 --- a/internal/format/archive/lock.go +++ b/internal/format/archive/lock.go @@ -205,14 +205,16 @@ func ReadLock(id string, props map[string]string) (Lock, error) { case password != "" && !locked: return Lock{}, &format.PropertyValueError{ Format: id, Key: Encryption, Value: NoEncryption, - Reason: "a password was given and this says the archive is not locked, so one of the two is not what you meant", - Remedy: "Set encryption to " + AES256 + " to lock the archive, or remove the password to leave it open.", + Reason: core.Says("format.APasswordWasGivenAndThis", "a password was given and this says the archive is not locked, so one of the two is not what you meant"), + Remedy: core.Says("archive.SetEncryption", "Set encryption to %s to lock the archive, or remove the password to leave it open.", + core.A("Method", core.Choice{Of: Encryption, Value: AES256})), } case password == "" && locked: return Lock{}, &format.PropertyValueError{ Format: id, Key: Password, Value: "", - Reason: "the archive is set to be locked with " + method + " and there is no password to lock it with", - Remedy: "Give a password, or set encryption to " + NoEncryption + ".", + Reason: core.Says("archive.LockedWithoutPassword", "the archive is set to be locked with %s and there is no password to lock it with", + core.A("Method", core.Choice{Of: Encryption, Value: method})), + Remedy: core.Says("archive.GiveAPassword", "Give a password, or set encryption to %s.", core.A("None", core.Choice{Of: Encryption, Value: NoEncryption})), } } return Lock{Method: method, Password: password}, nil @@ -235,16 +237,16 @@ func (l Lock) NewEntryWriter(w io.Writer, seed uint64, index int, crc uint32) (i return l.newZipCryptoWriter(w, seed, index, crc) } if l.keyLen() == 0 { - return nil, fmt.Errorf("archive: %q is not an encryption this build can write", l.Method) + return nil, core.Defect(fmt.Errorf("archive: %q is not an encryption this build can write", l.Method)) } salt := l.saltFor(seed, index) material, err := pbkdf2.Key(sha1.New, l.Password, salt, iterations, l.keyLen()*2+pwvLen) if err != nil { - return nil, fmt.Errorf("archive: the key could not be derived: %w", err) + return nil, core.Defect(fmt.Errorf("archive: the key could not be derived: %w", err)) } block, err := aes.NewCipher(material[:l.keyLen()]) if err != nil { - return nil, fmt.Errorf("archive: the cipher could not be built: %w", err) + return nil, core.Defect(fmt.Errorf("archive: the cipher could not be built: %w", err)) } if _, err := w.Write(salt); err != nil { return nil, err diff --git a/internal/format/archive/ownership.go b/internal/format/archive/ownership.go index d38d2b6d..f5cdd7f6 100644 --- a/internal/format/archive/ownership.go +++ b/internal/format/archive/ownership.go @@ -4,6 +4,8 @@ import ( "strconv" "github.com/donislawdev/TestingFilesGenerator/internal/format" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // What an archive records about the files it holds, beyond their bytes. @@ -73,8 +75,8 @@ func ReadOwnership(id string, props map[string]string) (Ownership, error) { // file nothing can read. return Ownership{}, &format.PropertyValueError{ Format: id, Key: EntryMode, Value: raw, - Reason: "it is not a permission written as octal digits", - Remedy: "Write it the way chmod takes it, so 644 or 755.", + Reason: core.Says("format.ItIsNotAPermissionWritten", "it is not a permission written as octal digits"), + Remedy: core.Says("format.WriteItTheWayChmodTakes", "Write it the way chmod takes it, so 644 or 755."), } } own.Mode = mode @@ -112,8 +114,8 @@ func ReadOwnership(id string, props map[string]string) (Ownership, error) { // here, and the guard that puts every declared word through this // function is what stops that arriving as an empty reason. Reason: axes[EntryOwner].Allows(raw), - Remedy: "Write it the way the setting is declared, so root or user, " + - "or leave the line out and the entries carry no owner.", + Remedy: core.Says("format.WriteItTheWayTheSetting", "Write it the way the setting is declared, so root or user, "+ + "or leave the line out and the entries carry no owner."), } } return own, nil diff --git a/internal/format/avif/avif.go b/internal/format/avif/avif.go index cafff914..5d37c6d0 100644 --- a/internal/format/avif/avif.go +++ b/internal/format/avif/avif.go @@ -205,10 +205,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { carries := labelled(m.width, label) if r.Label && !carries { p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", - m.width), + Code: "label_omitted", + Detail: core.Says("format.ThePictureIsPxWideAnd", "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", core.A("Width", m.width)), }) } p.Properties[format.PropertyLabelEmbedded] = carries @@ -224,9 +222,9 @@ func (generator) Plan(r format.Request) (format.Plan, error) { // size is the floor that applies. func checkFits(want int64, m memo) error { floor := int64(minimumBytes) - reason := fmt.Sprintf( + reason := core.Says("avif.MinimumReason", "the smallest picture this format draws codes to %d B at worst, and the file always carries a free box, which costs %d B even when it holds nothing", - minimumBytes-boxHeader, boxHeader) + core.A("Coded", minimumBytes-boxHeader), core.A("Box", boxHeader)) // A picture named by hand can be larger than the smallest one, and then its // own size is the floor that applies - which is known here, because naming @@ -234,9 +232,9 @@ func checkFits(want int64, m memo) error { if m.coded != nil { if own := int64(len(m.coded)) + boxHeader; own > floor { floor = own - reason = fmt.Sprintf( + reason = core.Says("avif.MinimumReasonOwn", "a %dx%d picture at quality %d codes to %d B, and the file always carries a free box, which costs %d B even when it holds nothing", - m.width, m.height, m.quality, len(m.coded), boxHeader) + core.A("Width", m.width), core.A("Height", m.height), core.A("Quality", m.quality), core.A("Coded", len(m.coded)), core.A("Box", boxHeader)) } } @@ -248,7 +246,7 @@ func checkFits(want int64, m memo) error { Requested: want, Minimum: floor, Reason: reason, - Hint: fmt.Sprintf("Ask for %d B or more, or set a smaller width and height, or a lower quality", floor), + Hint: core.Says("format.AskForBOrMoreOr", "Ask for %d B or more, or set a smaller width and height, or a lower quality", core.A("Floor", floor)), } } @@ -374,11 +372,11 @@ func checkJointLimits(w, h int) error { return err } for _, j := range d.JointLimits { - if bad := j.Allows(int64(w), int64(h)); bad != "" { + if bad := j.Allows("avif", int64(w), int64(h)); !bad.IsZero() { return &format.PropertyValueError{ - Format: "avif", Key: j.Of + " and " + j.By, + Format: "avif", Key: j.Of + " and " + j.By, Subject: j.Subject(), Value: fmt.Sprintf("%dx%d", w, h), - Reason: bad + ". Ask for a smaller pair", + Reason: core.Says("format.JointSmallerPair", "%s. Ask for a smaller pair", core.A("Why", bad)), } } } @@ -392,10 +390,10 @@ func quality(props map[string]string) (int, error) { } n, err := strconv.Atoi(raw) if err != nil { - return 0, fmt.Errorf("avif: quality must be a whole number, got %q", raw) + return 0, core.Defect(fmt.Errorf("avif: quality must be a whole number, got %q", raw)) } if n < minQuality || n > maxQuality { - return 0, fmt.Errorf("avif: quality must be between %d and %d, got %d", minQuality, maxQuality, n) + return 0, core.Defect(fmt.Errorf("avif: quality must be between %d and %d, got %d", minQuality, maxQuality, n)) } return n, nil } @@ -403,7 +401,7 @@ func quality(props map[string]string) (int, error) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("avif: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("avif: the plan was not produced by this generator")) } select { @@ -427,8 +425,8 @@ func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { if pad < boxHeader { // The ladder's ceiling for this picture was too low. Said out loud // rather than written short, because a short file is a wrong file. - return fmt.Errorf("avif: a %dx%d picture coded to %d B and the file was to be %d B, which leaves no room for the free box every one of these carries", - m.width, m.height, len(coded), m.total) + return core.Defect(fmt.Errorf("avif: a %dx%d picture coded to %d B and the file was to be %d B, which leaves no room for the free box every one of these carries", + m.width, m.height, len(coded), m.total)) } if _, err := w.Write(coded); err != nil { diff --git a/internal/format/avif/codec.go b/internal/format/avif/codec.go index 9a6500f9..618b0146 100644 --- a/internal/format/avif/codec.go +++ b/internal/format/avif/codec.go @@ -7,6 +7,8 @@ import ( "image" "github.com/gen2brain/gav1d/avif" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) const ( @@ -36,7 +38,7 @@ func encode(m image.Image, quality int) ([]byte, error) { Speed: encodeSpeed, }) if err != nil { - return nil, fmt.Errorf("avif: the encoder could not code the picture: %w", err) + return nil, core.Defect(fmt.Errorf("avif: the encoder could not code the picture: %w", err)) } return buf.Bytes(), nil } diff --git a/internal/format/bmp/bmp.go b/internal/format/bmp/bmp.go index 140b03eb..08e5a8b6 100644 --- a/internal/format/bmp/bmp.go +++ b/internal/format/bmp/bmp.go @@ -134,10 +134,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "BMP", Requested: r.Bytes, Minimum: bare, - Reason: fmt.Sprintf( - "a %dx%d picture is %d B of pixels once each row is rounded up to four bytes, and the %d B header sits in front of it", - w, h, pixelBytes(w, h), headers), - Hint: fmt.Sprintf("Ask for %d B or more, or set a smaller width and height", bare), + Reason: core.Says("format.AXPictureIsBOf", "a %dx%d picture is %d B of pixels once each row is rounded up to four bytes, and the %d B header sits in front of it", core.A("W", w), core.A("H", h), core.A("PixelBytes", pixelBytes(w, h)), core.A("Headers", headers)), + Hint: core.Says("format.AskForBOrMoreOr2", "Ask for %d B or more, or set a smaller width and height", core.A("Bare", bare)), } } if r.Bytes > maxFileBytes { @@ -145,8 +143,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "BMP", Requested: r.Bytes, Maximum: maxFileBytes, - Reason: "a BMP states its own size in a four byte field, so the format cannot describe a file this large", - Hint: "Ask for 4 GiB or less, or pick a format with no size field of its own such as gif.", + Reason: core.Says("format.ABMPStatesItsOwnSize", "a BMP states its own size in a four byte field, so the format cannot describe a file this large"), + Hint: core.Says("format.AskFor4GiBOrLess", "Ask for 4 GiB or less, or pick a format with no size field of its own such as gif."), } } @@ -168,10 +166,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { labelled := r.Label && imagelabel.Fits(w, len(label)) if r.Label && !labelled { p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", - w), + Code: "label_omitted", + Detail: core.Says("format.ThePictureIsPxWideAnd", "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", core.A("Width", w)), }) } p.Properties[format.PropertyLabelEmbedded] = labelled @@ -282,7 +278,7 @@ func isqrt(n uint64) uint64 { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("bmp: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("bmp: the plan was not produced by this generator")) } select { diff --git a/internal/format/csvfile/csv.go b/internal/format/csvfile/csv.go index e6267b84..9d5898b8 100644 --- a/internal/format/csvfile/csv.go +++ b/internal/format/csvfile/csv.go @@ -158,7 +158,7 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Requested: r.Bytes, Minimum: min, Reason: reasonForMinimum(d), - Hint: fmt.Sprintf("Ask for %d B or more.", min), + Hint: core.Says("format.AskForBOrMore", "Ask for %d B or more.", core.A("Min", min)), } } @@ -189,17 +189,17 @@ func (generator) Plan(r format.Request) (format.Plan, error) { // reasonForMinimum says what the floor is made of, which changes with the // dialect. A file with no header pays for rows alone, and saying "a header and // whole rows" there would name something the file does not have. -func reasonForMinimum(d dialect) string { +func reasonForMinimum(d dialect) core.Said { if !d.header { - return "a table holds whole rows, and one of them needs that much" + return core.Says("csv.MinimumRows", "a table holds whole rows, and one of them needs that much") } - return "a table holds a header and whole rows, and one of each needs that much" + return core.Says("csv.MinimumHeader", "a table holds a header and whole rows, and one of each needs that much") } func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("csv: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("csv: the plan was not produced by this generator")) } if m.dia.header { diff --git a/internal/format/csvfile/dialect.go b/internal/format/csvfile/dialect.go index 8f1105e1..348d5731 100644 --- a/internal/format/csvfile/dialect.go +++ b/internal/format/csvfile/dialect.go @@ -15,11 +15,12 @@ package csvfile import ( "bytes" - "fmt" "strconv" "strings" "github.com/donislawdev/TestingFilesGenerator/internal/format" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // Setting names. Public names, so they are spelled once. @@ -215,7 +216,7 @@ func parseDialect(props map[string]string) (dialect, error) { sep, known := delimiters[v] if !known { return dialect{}, badValue(Delimiter, v, - "it has to be "+strings.Join(delimiterIDs, ", ")) + core.Says("csv.HasToBeOneOf", "it has to be %s", core.A("Values", core.Choices{Of: Delimiter, Values: delimiterIDs}))) } d.delimiterID, d.sep = v, sep } @@ -223,7 +224,7 @@ func parseDialect(props map[string]string) (dialect, error) { if v, ok := props[LineEnding]; ok && v != "" { eol, known := lineEndings[v] if !known { - return dialect{}, badValue(LineEnding, v, "it has to be lf or crlf") + return dialect{}, badValue(LineEnding, v, core.Says("format.ItHasToBeLfOr", "it has to be lf or crlf")) } d.lineEndingID, d.eol = v, eol } @@ -235,7 +236,7 @@ func parseDialect(props map[string]string) (dialect, error) { case "false": d.header = false default: - return dialect{}, badValue(Header, v, "it has to be true or false") + return dialect{}, badValue(Header, v, core.Says("format.ItHasToBeTrueOr", "it has to be true or false")) } } @@ -243,7 +244,7 @@ func parseDialect(props map[string]string) (dialect, error) { q, known := quoteStyles[v] if !known { return dialect{}, badValue(QuoteStyle, v, - "it has to be "+strings.Join(quoteStyleIDs, ", ")) + core.Says("csv.HasToBeOneOf", "it has to be %s", core.A("Values", core.Choices{Of: QuoteStyle, Values: quoteStyleIDs}))) } d.quotes = q } @@ -271,16 +272,16 @@ func columnsFrom(props map[string]string, fallback int) (int, error) { } n, err := strconv.Atoi(v) if err != nil { - return 0, badValue(Columns, v, "it has to be a whole number") + return 0, badValue(Columns, v, core.Says("format.ItHasToBeAWhole", "it has to be a whole number")) } if n < minColumns || n > maxColumns { return 0, badValue(Columns, v, - fmt.Sprintf("it has to be between %d and %d", minColumns, maxColumns)) + core.Says("format.ItHasToBeBetweenAnd", "it has to be between %d and %d", core.A("MinColumns", minColumns), core.A("MaxColumns", maxColumns))) } return n, nil } -func badValue(key, val, why string) error { +func badValue(key, val string, why core.Said) error { return &format.PropertyValueError{Format: "csv", Key: key, Value: val, Reason: why} } diff --git a/internal/format/docx/docx.go b/internal/format/docx/docx.go index 40a78092..5fbd9258 100644 --- a/internal/format/docx/docx.go +++ b/internal/format/docx/docx.go @@ -132,9 +132,9 @@ func refusal(err error, want int64, shape opc.Shape, paragraphs int) error { Format: "DOCX", Requested: big.Want, Maximum: big.Ceiling, - Reason: "an Office file is a ZIP archive, and this build works out its size before it writes it " + - "in a way that cannot account for the zip64 records a larger one needs", - Hint: "Ask for less than 4 GiB, or split the content across several files.", + Reason: core.Says("format.AnOfficeFileIsAZIP", "an Office file is a ZIP archive, and this build works out its size before it writes it "+ + "in a way that cannot account for the zip64 records a larger one needs"), + Hint: core.Says("format.AskForLessThan4GiB", "Ask for less than 4 GiB, or split the content across several files."), } } var gap *opc.Unreachable @@ -143,20 +143,16 @@ func refusal(err error, want int64, shape opc.Shape, paragraphs int) error { Format: "DOCX", Requested: want, Minimum: gap.Above, - Reason: fmt.Sprintf( - "the archive comment carries at most %d B and the smallest extra part costs %d B, so nothing between those two is reachable", - opc.CommentLimit, shape.FillerOverhead), - Hint: fmt.Sprintf("Ask for %d B or less, or %d B or more.", gap.Below, gap.Above), + Reason: core.Says("format.TheArchiveCommentCarriesAtMost", "the archive comment carries at most %d B and the smallest extra part costs %d B, so nothing between those two is reachable", core.A("CommentLimit", opc.CommentLimit), core.A("FillerOverhead", shape.FillerOverhead)), + Hint: core.Says("format.AskForBOrLessOr", "Ask for %d B or less, or %d B or more.", core.A("Below", gap.Below), core.A("Above", gap.Above)), } } return &format.BelowMinimumError{ Format: "DOCX", Requested: want, Minimum: shape.Bare, - Reason: fmt.Sprintf( - "a document of %s already packages to that much, and a Word file cannot leave out its content types or its relationships", - core.Count(paragraphs, "paragraph", "paragraphs")), - Hint: fmt.Sprintf("Ask for %d B or more, or set fewer paragraphs", shape.Bare), + Reason: core.Says("format.ADocumentOfAlreadyPackagesTo", "a document of %s already packages to that much, and a Word file cannot leave out its content types or its relationships", core.A("Count", core.SaysN("docx.Paragraphs", "%d paragraph", "%d paragraphs", core.A("Count", paragraphs)))), + Hint: core.Says("format.AskForBOrMoreOr3", "Ask for %d B or more, or set fewer paragraphs", core.A("Bare", shape.Bare)), } } @@ -172,7 +168,7 @@ func asUnreachable(err error, target **opc.Unreachable) bool { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("docx: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("docx: the plan was not produced by this generator")) } return opc.Write(ctx, w, m.pkg) } @@ -220,10 +216,10 @@ func intProperty(props map[string]string, key string, fallback int) (int, error) } n, err := strconv.Atoi(raw) if err != nil { - return 0, fmt.Errorf("docx: %s must be a whole number, got %q", key, raw) + return 0, core.Defect(fmt.Errorf("docx: %s must be a whole number, got %q", key, raw)) } if n < minParagraphs || n > maxParagraphs { - return 0, fmt.Errorf("docx: %s must be between %d and %d, got %d", key, minParagraphs, maxParagraphs, n) + return 0, core.Defect(fmt.Errorf("docx: %s must be between %d and %d, got %d", key, minParagraphs, maxParagraphs, n)) } return n, nil } diff --git a/internal/format/format.go b/internal/format/format.go index 035bbbd2..ac4dd495 100644 --- a/internal/format/format.go +++ b/internal/format/format.go @@ -209,113 +209,6 @@ type Property struct { Long bool } -// JointLimit is a rule binding two settings that neither of them can state -// alone. -// -// A Property bounds one value. PNG needs more than that: each side of a picture -// may go up to twenty thousand pixels and the two multiplied may not pass forty -// megapixels, because the picture is held in memory while it is encoded. That -// rule had nowhere to live, so it lived in the generator and in a sentence of -// prose - and "tfg formats png" offered a pair it then refused. -// -// A sentence is enough for a person and not for anything else. AR9 has the -// registry as the one place a consumer asks what a format accepts, and a window -// drawing two number fields from Min and Max would offer twenty thousand in -// both and produce a request the run rejects. Declaring it is what lets the -// refusal, the printed description and a future field all come from one place. -// -// Kept to a product of two because that is the shape every case in Tier 1 has - -// pixels, samples times channels, pages times page size. A general expression -// would be a language to learn, and this is a line to read. -type JointLimit struct { - // Of and By are the two settings multiplied together. - Of, By string - // Max is the largest their product may be, counted in the same units the - // settings themselves use. - Max int64 - // Unit is what the product is reported in, and Per is how many of the - // settings' own units make one of it. Pixels are counted in millions when - // spoken about, so Unit is "megapixels" and Per is a million - "400 - // megapixels and the limit is 40" is a sentence somebody can act on and - // "400000000 and the limit is 40000000" is not. Per of nought means one. - Unit string - Per int64 - // Base is what the product itself counts, for the sentence that has to be - // exact: "pixels", "cells". - // - // It exists because the readable form above cannot always be used. Rounding - // two counts that differ can land them on one number, and then the refusal - // says "they come to 40 megapixels and the limit is 40" - measured on - // 2026-09-22 for a picture of 20000x2001, which is 40 020 000 pixels - // against a limit of 40 000 000. A person reading that has been told - // nothing. See Allows. - Base string - // Why is the reason, in the words the refusal uses. - Why string -} - -// Allows reports whether a pair of values satisfies the limit, and says what is -// wrong when it does not. -// -// Both values are expected to have passed their own Property first, and this -// does not check that again. of*by is not guarded against overflow because the -// declarations that reach it cap each side at twenty thousand, so the product -// is nine orders of magnitude below the range of the type. A guard here would -// be a branch nothing could ever reach, and an unreachable branch reads as a -// protection somebody is relying on. -func (j JointLimit) Allows(of, by int64) (bad string) { - got := of * by - if got <= j.Max { - return "" - } - asked, allowed := j.readably(got) - return fmt.Sprintf("together they come to %s and the limit is %s, because %s", - asked, allowed, j.Why) -} - -// readably is the pair of counts as a person reads them, and it never puts two -// different counts on one number. -// -// The rounded form is offered first, because that is the one worth reading: -// "400 megapixels and the limit is 40 megapixels" is a sentence somebody can -// act on. It is stood down when both counts round to the same text, which is -// not a corner case - it is what a request just over the limit looks like. -// Measured on 2026-09-22 (O232): 20000x2001 is 40 020 000 pixels, the limit is -// 40 000 000, and the sentence read "they come to 40 megapixels and the limit -// is 40". Two identical numbers, and no way to tell how far over it was. -// -// Both halves carry the unit now. Only the first one did, so the limit was a -// bare number taking its noun from four words earlier. -// -// Adding decimal places was the other way out and it does not work: 40 000 001 -// against 40 000 000 collides at every fixed number of places. -func (j JointLimit) readably(got int64) (asked, allowed string) { - if j.per() > 1 && got/j.per() != j.Max/j.per() { - return fmt.Sprintf("%d %s", got/j.per(), j.Unit), - fmt.Sprintf("%d %s", j.Max/j.per(), j.Unit) - } - return core.Exactly(got) + " " + j.Base, core.Exactly(j.Max) + " " + j.Base -} - -// Describe is the rule as one sentence, for the format list and for a window. -func (j JointLimit) Describe() string { - return fmt.Sprintf("%s times %s cannot pass %d %s, because %s", - j.Of, j.By, j.Most(), j.Unit, j.Why) -} - -// Most is the ceiling counted in Unit - 40 for forty million pixels in -// megapixels - which is the number a sentence about this limit says. Here -// rather than worked out by whoever words the sentence, so the window saying it -// in another language divides the same way. -func (j JointLimit) Most() int64 { return j.Max / j.per() } - -func (j JointLimit) per() int64 { - if j.Per == 0 { - return 1 - } - return j.Per -} - // Descriptor is everything a format announces about itself. A format missing // any of it fails the registry test rather than shipping half implemented. type Descriptor struct { @@ -396,20 +289,35 @@ type NotAContainerError struct { } // What happened, what can do it instead, and what to do about it. -func (e *NotAContainerError) What() string { - return fmt.Sprintf("%s holds no other files, so it cannot take contains", e.Format) +func (e *NotAContainerError) What() string { return e.what().String() } + +func (e *NotAContainerError) what() core.Said { + return core.Says("format.NotAContainer", "%s holds no other files, so it cannot take contains", core.A("Format", e.Format)) } -func (e *NotAContainerError) Why() string { - return "the formats that can are " + strings.Join(e.Containers, ", ") +func (e *NotAContainerError) Why() string { return e.why().String() } + +func (e *NotAContainerError) why() core.Said { + return core.Says("format.NotAContainerWhy", "the formats that can are %s", core.A("Containers", strings.Join(e.Containers, ", "))) } -func (e *NotAContainerError) Instead() string { return "Drop contains, or change the format" } +func (e *NotAContainerError) Instead() string { return e.instead().String() } -func (e *NotAContainerError) Error() string { - return fmt.Sprintf( - "%s holds no other files, so it cannot take contains - the formats that can are %s. Drop contains, or change the format", - e.Format, strings.Join(e.Containers, ", ")) +func (e *NotAContainerError) instead() core.Said { + return core.Says("format.NotAContainerFix", "Drop contains, or change the format") +} + +// Parts is what happened, why and what to do instead, for a reader that lays +// them out apart and in its own language. +func (e *NotAContainerError) Parts() (what, why, instead core.Said) { + return e.what(), e.why(), e.instead() +} + +func (e *NotAContainerError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *NotAContainerError) Said() core.Said { + return core.Says("format.NotAContainerWhole", "%s - %s. %s", core.A("What", e.what()), core.A("Why", e.why()), core.A("Fix", e.instead())) } // ContentsConflictError is contains stated beside format properties saying the @@ -420,10 +328,18 @@ type ContentsConflictError struct { Keys []string } -func (e *ContentsConflictError) Error() string { - return fmt.Sprintf( - "%s: contains and the %s propert%s both say what the archive holds. Keep contains and drop the properties, or the other way round", - e.Format, strings.Join(e.Keys, ", "), plural(len(e.Keys))) +func (e *ContentsConflictError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. +func (e *ContentsConflictError) Said() core.Said { + if len(e.Keys) == 1 { + return core.Says("format.ContentsConflictOne", + "%s: contains and the %s property both say what the archive holds. Keep contains and drop the properties, or the other way round", + core.A("Format", e.Format), core.A("Key", e.Keys[0])) + } + return core.Says("format.ContentsConflict", + "%s: contains and the %s properties both say what the archive holds. Keep contains and drop the properties, or the other way round", + core.A("Format", e.Format), core.A("Keys", strings.Join(e.Keys, ", "))) } // NestingUnsupportedError is a container asked to hold its own format. @@ -434,17 +350,13 @@ type NestingUnsupportedError struct { Format string } -func (e *NestingUnsupportedError) Error() string { - return fmt.Sprintf( - "%s cannot hold %s yet - an archive inside an archive needs a depth limit first. Hold a different format, or build the inner archive as its own target", - e.Format, e.Format) -} +func (e *NestingUnsupportedError) Error() string { return e.Said().String() } -func plural(n int) string { - if n == 1 { - return "y" - } - return "ies" +// Said is the refusal, for a window that says it in its own language. +func (e *NestingUnsupportedError) Said() core.Said { + return core.Says("format.NestingUnsupported", + "%s cannot hold %s yet - an archive inside an archive needs a depth limit first. Hold a different format, or build the inner archive as its own target", + core.A("Format", e.Format), core.A("Inner", e.Format)) } // Containers lists the formats that accept contains, for a message that tells @@ -468,17 +380,16 @@ func Containers() []string { // The sentence is built from the declaration, so every format refuses in the // same words and a new format gets the wording by declaring rather than by // writing it again. -func (p Property) Allows(raw string) (bad string) { +func (p Property) Allows(raw string) (bad core.Said) { switch p.Kind { case PropertyInt: n, err := strconv.ParseInt(raw, 10, 64) if err != nil { - return "it takes a whole number" + p.unitSuffix() + return p.takesWholeNumber() } if p.Min != 0 || p.Max != 0 { if n < p.Min || n > p.Max { - return fmt.Sprintf("it takes a whole number%s from %d to %d", - p.unitSuffix(), p.Min, p.Max) + return p.takesRange() } } case PropertyChoice: @@ -494,10 +405,10 @@ func (p Property) Allows(raw string) (bad string) { // declaring the set here. for _, c := range p.Choices { if raw == c { - return "" + return core.Said{} } } - return "it takes one of: " + strings.Join(p.Choices, ", ") + return core.Says("format.TakesOneOf", "it takes one of: %s", core.A("Values", strings.Join(p.Choices, ", "))) case PropertyBool: // Exact for the same reason, and it costs a user nothing: a recipe // writing `header: True` arrives as "true" already, because the reader @@ -506,18 +417,35 @@ func (p Property) Allows(raw string) (bad string) { switch raw { case "true", "false": default: - return "it takes true or false" + return core.Says("format.TakesTrueOrFalse", "it takes true or false") } case PropertySize: if _, err := core.ParseSize(raw); err != nil { - return "it takes " + sizePhrase + return core.Says("format.TakesSize", "it takes a size such as 2mb, or a plain byte count") } case PropertyText: // Named rather than left out. Free text has no shape to check, so // anything is allowed - and a kind added later reddens the linter // instead of arriving here and being accepted without a word. } - return "" + return core.Said{} +} + +// takesWholeNumber and takesRange are what a whole number setting takes, with +// what it counts when the declaration says. +func (p Property) takesWholeNumber() core.Said { + if p.Unit == "" { + return core.Says("format.TakesWholeNumber", "it takes a whole number") + } + return core.Says("format.TakesWholeNumberOf", "it takes a whole number of %s", core.A("Unit", core.UnitTerm(p.Unit))) +} + +func (p Property) takesRange() core.Said { + if p.Unit == "" { + return core.Says("format.TakesRange", "it takes a whole number from %d to %d", core.A("Least", p.Min), core.A("Most", p.Max)) + } + return core.Says("format.TakesRangeOf", "it takes a whole number of %s from %d to %d", + core.A("Unit", core.UnitTerm(p.Unit)), core.A("Least", p.Min), core.A("Most", p.Max)) } // sizePhrase is what a size setting takes, written once. @@ -693,7 +621,7 @@ func (d Descriptor) CheckEachProperty(props map[string]string) []error { // because that is what an unset flag and an empty recipe entry both // look like by the time they arrive here. if raw := props[k]; raw != "" { - if why := p.Allows(raw); why != "" { + if why := p.Allows(raw); !why.IsZero() { bad = append(bad, &PropertyValueError{ Format: d.ID, Key: k, Value: raw, Reason: why, Remedy: p.Instead(), }) @@ -712,11 +640,11 @@ func (d Descriptor) CheckEachProperty(props map[string]string) []error { // today. The wording stops at "works it out" rather than naming the size, // because a property added tomorrow may work it out from something else and // the sentence has to stay true without anybody checking it. -func (p Property) Instead() string { +func (p Property) Instead() core.Said { if p.Default != "" { - return fmt.Sprintf("write a value it takes, such as %s, or leave the line out", p.Default) + return core.Says("format.WriteAValueSuchAs", "write a value it takes, such as %s, or leave the line out", core.A("Default", p.Default)) } - return "write a value it takes, or leave the line out and the format works it out" + return core.Says("format.WriteAValue", "write a value it takes, or leave the line out and the format works it out") } // CheckProperties refuses any key the format does not declare, and any value @@ -779,8 +707,9 @@ type Request struct { type Note struct { // Code is the machine readable reason, for the manifest. Code string - // Detail is one English sentence, for a person. - Detail string + // Detail is one sentence for a person. The manifest carries its English, + // and a window says it in its own language. + Detail core.Said } // Plan is the answer to "what exactly will be produced", worked out without @@ -848,8 +777,8 @@ type BelowMinimumError struct { Format string Requested int64 Minimum int64 - Reason string - Hint string + Reason core.Said + Hint core.Said } // What happened, why the minimum exists, and what to do instead. @@ -861,16 +790,21 @@ type BelowMinimumError struct { // for belongs beside the minimum rather than after the reason. A guard asks // that the sentence still carries the why and the fix, so the two cannot drift. func (e *BelowMinimumError) What() string { - return fmt.Sprintf("%s cannot be smaller than %d B. Requested: %d B", e.Format, e.Minimum, e.Requested) + return core.Says("format.BelowMinimumWhat", "%s cannot be smaller than %d B. Requested: %d B", + core.A("Format", e.Format), core.A("Minimum", e.Minimum), core.A("Requested", e.Requested)).String() } -func (e *BelowMinimumError) Why() string { return e.Reason } +func (e *BelowMinimumError) Why() string { return e.Reason.String() } + +func (e *BelowMinimumError) Instead() string { return e.Hint.String() } -func (e *BelowMinimumError) Instead() string { return e.Hint } +func (e *BelowMinimumError) Error() string { return e.Said().String() } -func (e *BelowMinimumError) Error() string { - return fmt.Sprintf("%s cannot be smaller than %d B - %s. Requested: %d B. %s", - e.Format, e.Minimum, e.Reason, e.Requested, e.Hint) +// Said is the whole refusal, for a window that says it in its own language. +func (e *BelowMinimumError) Said() core.Said { + return core.Says("format.BelowMinimum", "%s cannot be smaller than %d B - %s. Requested: %d B. %s", + core.A("Format", e.Format), core.A("Minimum", e.Minimum), core.A("Why", e.Reason), + core.A("Requested", e.Requested), core.A("Fix", e.Hint)) } // SettingSize is the recipe key this refusal is about. @@ -910,21 +844,26 @@ type AboveMaximumError struct { Format string Requested int64 Maximum int64 - Reason string - Hint string + Reason core.Said + Hint core.Said } func (e *AboveMaximumError) What() string { - return fmt.Sprintf("%s cannot be larger than %d B. Requested: %d B", e.Format, e.Maximum, e.Requested) + return core.Says("format.AboveMaximumWhat", "%s cannot be larger than %d B. Requested: %d B", + core.A("Format", e.Format), core.A("Maximum", e.Maximum), core.A("Requested", e.Requested)).String() } -func (e *AboveMaximumError) Why() string { return e.Reason } +func (e *AboveMaximumError) Why() string { return e.Reason.String() } -func (e *AboveMaximumError) Instead() string { return e.Hint } +func (e *AboveMaximumError) Instead() string { return e.Hint.String() } -func (e *AboveMaximumError) Error() string { - return fmt.Sprintf("%s cannot be larger than %d B - %s. Requested: %d B. %s", - e.Format, e.Maximum, e.Reason, e.Requested, e.Hint) +func (e *AboveMaximumError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *AboveMaximumError) Said() core.Said { + return core.Says("format.AboveMaximum", "%s cannot be larger than %d B - %s. Requested: %d B. %s", + core.A("Format", e.Format), core.A("Maximum", e.Maximum), core.A("Why", e.Reason), + core.A("Requested", e.Requested), core.A("Fix", e.Hint)) } func (e *AboveMaximumError) AboutSetting() string { return SettingSize } @@ -935,6 +874,9 @@ type UnknownFormatError struct { Known []string } -func (e *UnknownFormatError) Error() string { - return fmt.Sprintf("unknown format %q. Known formats: %v", e.ID, e.Known) +func (e *UnknownFormatError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. +func (e *UnknownFormatError) Said() core.Said { + return core.Says("format.Unknown", "unknown format %q. Known formats: %v", core.A("Format", e.ID), core.A("Known", e.Known)) } diff --git a/internal/format/gif/animation.go b/internal/format/gif/animation.go index a1bccea9..3a62e598 100644 --- a/internal/format/gif/animation.go +++ b/internal/format/gif/animation.go @@ -17,6 +17,8 @@ import ( stdgif "image/gif" "io" "strconv" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // frameCount reads the frames setting, which is the one thing about a GIF that @@ -28,10 +30,10 @@ func frameCount(props map[string]string) (int, error) { } n, err := strconv.Atoi(raw) if err != nil { - return 0, fmt.Errorf("gif: frames must be a whole number, got %q", raw) + return 0, core.Defect(fmt.Errorf("gif: frames must be a whole number, got %q", raw)) } if n < minFrames || n > maxFrames { - return 0, fmt.Errorf("gif: frames must be between %d and %d, got %d", minFrames, maxFrames, n) + return 0, core.Defect(fmt.Errorf("gif: frames must be between %d and %d, got %d", minFrames, maxFrames, n)) } return n, nil } diff --git a/internal/format/gif/gif.go b/internal/format/gif/gif.go index 35d63cde..3a6c1900 100644 --- a/internal/format/gif/gif.go +++ b/internal/format/gif/gif.go @@ -173,11 +173,12 @@ func checkJointLimits(w, h int) error { return err } for _, j := range d.JointLimits { - if bad := j.Allows(int64(w), int64(h)); bad != "" { + if bad := j.Allows("gif", int64(w), int64(h)); !bad.IsZero() { return &format.PropertyValueError{ - Format: "gif", Key: j.Of + " and " + j.By, - Value: fmt.Sprintf("%dx%d", w, h), - Reason: bad + fmt.Sprintf(". Each side may go up to %d, but not both at once - ask for a smaller pair", maxDimension), + Format: "gif", Key: j.Of + " and " + j.By, Subject: j.Subject(), + Value: fmt.Sprintf("%dx%d", w, h), + Reason: core.Says("format.JointEachSide", "%s. Each side may go up to %d, but not both at once - ask for a smaller pair", + core.A("Why", bad), core.A("Most", maxDimension)), } } } @@ -223,10 +224,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { labelled := r.Label && imagelabel.Fits(w, len(label)) if r.Label && !labelled { p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", - w), + Code: "label_omitted", + Detail: core.Says("format.ThePictureIsPxWideAnd", "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", core.A("Width", w)), }) } p.Properties[format.PropertyLabelEmbedded] = labelled @@ -253,8 +252,8 @@ func settlePadding(m *memo, want, bare int64) error { Format: "GIF", Requested: want, Minimum: bare, - Reason: fmt.Sprintf("a %dx%d picture already encodes to that much before any padding", m.width, m.height), - Hint: fmt.Sprintf("Ask for %d B or more, or set a smaller width and height", bare), + Reason: core.Says("format.AXPictureAlreadyEncodesTo", "a %dx%d picture already encodes to that much before any padding", core.A("Width", m.width), core.A("Height", m.height)), + Hint: core.Says("format.AskForBOrMoreOr2", "Ask for %d B or more, or set a smaller width and height", core.A("Bare", bare)), } case delta < smallestCarryingComment: @@ -266,20 +265,17 @@ func settlePadding(m *memo, want, bare int64) error { Format: "GIF", Requested: want, Minimum: bare + smallestCarryingComment, - Reason: fmt.Sprintf( - "a %dx%d picture encodes to exactly %d B, an empty comment adds %d B and the smallest comment that carries any padding adds %d B, so nothing else in between is reachable", - m.width, m.height, bare, emptyComment, smallestCarryingComment), - Hint: fmt.Sprintf("Ask for exactly %d B, exactly %d B, or %d B or more.", - bare, bare+emptyComment, bare+smallestCarryingComment), + Reason: core.Says("format.AXPictureEncodesToExactly", "a %dx%d picture encodes to exactly %d B, an empty comment adds %d B and the smallest comment that carries any padding adds %d B, so nothing else in between is reachable", core.A("Width", m.width), core.A("Height", m.height), core.A("Bare", bare), core.A("EmptyComment", emptyComment), core.A("SmallestCarryingComment", smallestCarryingComment)), + Hint: core.Says("format.AskForExactlyBExactlyB", "Ask for exactly %d B, exactly %d B, or %d B or more.", core.A("Bare", bare), core.A("Bare2", bare+emptyComment), core.A("Bare3", bare+smallestCarryingComment)), } } m.comment = true m.blocks, m.payload = commentShape(delta) if m.payload < m.blocks { - return fmt.Errorf( + return core.Defect(fmt.Errorf( "gif: %d B of padding does not divide into %d sub blocks carrying %d B - this is a bug in the size arithmetic, not in the request", - delta, m.blocks, m.payload) + delta, m.blocks, m.payload)) } return nil } @@ -304,7 +300,7 @@ func commentShape(delta int64) (blocks, payload int64) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("gif: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("gif: the plan was not produced by this generator")) } select { @@ -327,13 +323,13 @@ func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { if err := settlePadding(&m, p.Bytes, m.body+trailerSize); err != nil { // Unreachable unless ladderCeiling is wrong, and then saying so // beats writing a file of the wrong length. - return fmt.Errorf("gif: %w - ladderCeiling is wrong", err) + return core.Defect(fmt.Errorf("gif: %w - ladderCeiling is wrong", err)) } } else if holder.written != m.body { - return fmt.Errorf("gif: the picture encoded to %d B where planning said %d B", holder.written, m.body) + return core.Defect(fmt.Errorf("gif: the picture encoded to %d B where planning said %d B", holder.written, m.body)) } if holder.tail[0] != 0x3B { - return fmt.Errorf("gif: the encoded stream does not end with the trailer") + return core.Defect(fmt.Errorf("gif: the encoded stream does not end with the trailer")) } if m.comment { diff --git a/internal/format/htmlfile/html.go b/internal/format/htmlfile/html.go index b2f4c50f..745ec27e 100644 --- a/internal/format/htmlfile/html.go +++ b/internal/format/htmlfile/html.go @@ -88,7 +88,9 @@ func structureOf(props map[string]string) (string, error) { } return "", &format.PropertyValueError{ Format: "html", Key: settingStructure, Value: v, - Reason: "it has to be " + structureDocument + " or " + structureFragment, + Reason: core.Says("html.HasToBeStructure", "it has to be %s or %s", + core.A("Document", core.Choice{Of: settingStructure, Value: structureDocument}), + core.A("Fragment", core.Choice{Of: settingStructure, Value: structureFragment})), } } @@ -169,16 +171,16 @@ func (generator) Plan(r format.Request) (format.Plan, error) { min := minimumFor(shape) if r.Bytes < min { - reason := "a page holds a head, a body and whole blocks, and one of each needs that much" + reason := core.Says("html.MinimumPage", "a page holds a head, a body and whole blocks, and one of each needs that much") if shape == structureFragment { - reason = "a fragment holds whole blocks, and one of them needs that much" + reason = core.Says("html.MinimumFragment", "a fragment holds whole blocks, and one of them needs that much") } return format.Plan{}, &format.BelowMinimumError{ Format: "HTML", Requested: r.Bytes, Minimum: min, Reason: reason, - Hint: fmt.Sprintf("Ask for %d B or more.", min), + Hint: core.Says("format.AskForBOrMore", "Ask for %d B or more.", core.A("Min", min)), } } @@ -240,17 +242,15 @@ func labelledHead(shape string, r format.Request, min int64) (head, heading stri return head, heading, nil } return prologueFor(shape), "", &format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The label needs %d B and this file has no room for it beside a whole block. Its name and the manifest still identify it.", - len(heading)), + Code: "label_omitted", + Detail: core.Says("format.TheLabelNeedsBAndThis", "The label needs %d B and this file has no room for it beside a whole block. Its name and the manifest still identify it.", core.A("Length", len(heading))), } } func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("html: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("html: the plan was not produced by this generator")) } head := m.head + m.labelLine diff --git a/internal/format/ico/ico.go b/internal/format/ico/ico.go index b2a3b882..3d3a1240 100644 --- a/internal/format/ico/ico.go +++ b/internal/format/ico/ico.go @@ -155,10 +155,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { labelled := label != "" && imagelabel.Fits(w, len(label)) if r.Label && !labelled { p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The icon is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", - w), + Code: "label_omitted", + Detail: core.Says("format.TheIconIsPxWideAnd", "The icon is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", core.A("W", w)), }) } p.Properties[format.PropertyLabelEmbedded] = labelled @@ -174,10 +172,8 @@ func reachable(want, bare int64, w, h int, embed string, picture int) error { Format: "ICO", Requested: want, Minimum: bare, - Reason: fmt.Sprintf( - "a %dx%d icon holding a %s is %d B of picture, and the %d B directory sits in front of it", - w, h, embed, picture, preamble), - Hint: fmt.Sprintf("Ask for %d B or more, set a smaller width and height, or set embed=%s", bare, embedPNG), + Reason: core.Says("format.AXIconHoldingAIs", "a %dx%d icon holding a %s is %d B of picture, and the %d B directory sits in front of it", core.A("W", w), core.A("H", h), core.A("Embed", embed), core.A("Picture", picture), core.A("Preamble", preamble)), + Hint: core.Says("format.AskForBOrMoreSet", "Ask for %d B or more, set a smaller width and height, or set embed=%s", core.A("Bare", bare), core.A("EmbedPNG", embedPNG)), } } if want > maxFileBytes { @@ -185,8 +181,8 @@ func reachable(want, bare int64, w, h int, embed string, picture int) error { Format: "ICO", Requested: want, Maximum: maxFileBytes, - Reason: "an icon states where its picture starts in a four byte field, so the format cannot describe a file this large", - Hint: "Ask for 4 GiB or less, or pick a format with no offset field of its own such as gif.", + Reason: core.Says("format.AnIconStatesWhereItsPicture", "an icon states where its picture starts in a four byte field, so the format cannot describe a file this large"), + Hint: core.Says("format.AskFor4GiBOrLess2", "Ask for 4 GiB or less, or pick a format with no offset field of its own such as gif."), } } return nil @@ -203,7 +199,7 @@ func embedding(props map[string]string) (string, error) { } return "", &format.PropertyValueError{ Format: "ico", Key: "embed", Value: raw, - Reason: fmt.Sprintf("it has to be %s or %s", embedBMP, embedPNG), + Reason: core.Says("format.ItHasToBeOr", "it has to be %s or %s", core.A("EmbedBMP", embedBMP), core.A("EmbedPNG", embedPNG)), } } @@ -243,7 +239,7 @@ func chooseSize(r format.Request, label, embed string) (int, int, error) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("ico: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("ico: the plan was not produced by this generator")) } select { diff --git a/internal/format/imagedim/imagedim.go b/internal/format/imagedim/imagedim.go index 82d22b70..c20045a4 100644 --- a/internal/format/imagedim/imagedim.go +++ b/internal/format/imagedim/imagedim.go @@ -31,10 +31,11 @@ package imagedim import ( - "fmt" "strconv" "github.com/donislawdev/TestingFilesGenerator/internal/format" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // Setting names. Public names, so they are spelled once. @@ -109,13 +110,13 @@ func Value(formatID, key string, props map[string]string, largest, fallback int) if err != nil { return 0, &format.PropertyValueError{ Format: formatID, Key: key, Value: raw, - Reason: "it has to be a whole number of " + Unit, + Reason: core.Says("format.HasToBeWholeNumberOf", "it has to be a whole number of %s", core.A("Unit", core.UnitTerm(Unit))), } } if n < Smallest || n > largest { return 0, &format.PropertyValueError{ Format: formatID, Key: key, Value: raw, - Reason: fmt.Sprintf("it has to be between %d and %d", Smallest, largest), + Reason: core.Says("format.ItHasToBeBetweenAnd", "it has to be between %d and %d", core.A("MinColumns", Smallest), core.A("MaxColumns", largest)), } } return n, nil diff --git a/internal/format/jointlimit.go b/internal/format/jointlimit.go new file mode 100644 index 00000000..4a441282 --- /dev/null +++ b/internal/format/jointlimit.go @@ -0,0 +1,124 @@ +package format + +import ( + "fmt" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" +) + +// JointLimit is a rule binding two settings that neither of them can state +// alone. +// +// A Property bounds one value. PNG needs more than that: each side of a picture +// may go up to twenty thousand pixels and the two multiplied may not pass forty +// megapixels, because the picture is held in memory while it is encoded. That +// rule had nowhere to live, so it lived in the generator and in a sentence of +// prose - and "tfg formats png" offered a pair it then refused. +// +// A sentence is enough for a person and not for anything else. AR9 has the +// registry as the one place a consumer asks what a format accepts, and a window +// drawing two number fields from Min and Max would offer twenty thousand in +// both and produce a request the run rejects. Declaring it is what lets the +// refusal, the printed description and a future field all come from one place. +// +// Kept to a product of two because that is the shape every case in Tier 1 has - +// pixels, samples times channels, pages times page size. A general expression +// would be a language to learn, and this is a line to read. +type JointLimit struct { + // Of and By are the two settings multiplied together. + Of, By string + // Max is the largest their product may be, counted in the same units the + // settings themselves use. + Max int64 + // Unit is what the product is reported in, and Per is how many of the + // settings' own units make one of it. Pixels are counted in millions when + // spoken about, so Unit is "megapixels" and Per is a million - "400 + // megapixels and the limit is 40" is a sentence somebody can act on and + // "400000000 and the limit is 40000000" is not. Per of nought means one. + Unit string + Per int64 + // Base is what the product itself counts, for the sentence that has to be + // exact: "pixels", "cells". + // + // It exists because the readable form above cannot always be used. Rounding + // two counts that differ can land them on one number, and then the refusal + // says "they come to 40 megapixels and the limit is 40" - measured on + // 2026-09-22 for a picture of 20000x2001, which is 40 020 000 pixels + // against a limit of 40 000 000. A person reading that has been told + // nothing. See Allows. + Base string + // Why is the reason, in the words the refusal uses. + Why string +} + +// Allows reports whether a pair of values satisfies the limit, and says what is +// wrong when it does not. +// +// Both values are expected to have passed their own Property first, and this +// does not check that again. of*by is not guarded against overflow because the +// declarations that reach it cap each side at twenty thousand, so the product +// is nine orders of magnitude below the range of the type. A guard here would +// be a branch nothing could ever reach, and an unreachable branch reads as a +// protection somebody is relying on. +// +// The format is whose limit it is, so a window finds the reason in its own +// words under the key it keeps it by. +func (j JointLimit) Allows(format string, of, by int64) (bad core.Said) { + got := of * by + if got <= j.Max { + return core.Said{} + } + asked, allowed := j.readably(got) + return core.Says("format.JointTooMuch", "together they come to %s and the limit is %s, because %s", + core.A("Asked", asked), core.A("Limit", allowed), + core.A("Why", core.Term{Key: core.JointKey(core.FormatOwner(format), j.Of, j.By), Text: j.Why})) +} + +// Subject is the pair of settings this limit binds, named for a refusal. +func (j JointLimit) Subject() core.Said { + return core.Says("format.TwoSettings", "%s and %s", core.A("Of", core.LabelTerm(j.Of)), core.A("By", core.LabelTerm(j.By))) +} + +// readably is the pair of counts as a person reads them, and it never puts two +// different counts on one number. +// +// The rounded form is offered first, because that is the one worth reading: +// "400 megapixels and the limit is 40 megapixels" is a sentence somebody can +// act on. It is stood down when both counts round to the same text, which is +// not a corner case - it is what a request just over the limit looks like. +// Measured on 2026-09-22 (O232): 20000x2001 is 40 020 000 pixels, the limit is +// 40 000 000, and the sentence read "they come to 40 megapixels and the limit +// is 40". Two identical numbers, and no way to tell how far over it was. +// +// Both halves carry the unit now. Only the first one did, so the limit was a +// bare number taking its noun from four words earlier. +// +// Adding decimal places was the other way out and it does not work: 40 000 001 +// against 40 000 000 collides at every fixed number of places. +func (j JointLimit) readably(got int64) (asked, allowed core.Said) { + if j.per() > 1 && got/j.per() != j.Max/j.per() { + return core.Says("format.CountOf", "%d %s", core.A("Number", got/j.per()), core.A("Unit", core.UnitTerm(j.Unit))), + core.Says("format.CountOf", "%d %s", core.A("Number", j.Max/j.per()), core.A("Unit", core.UnitTerm(j.Unit))) + } + return core.Says("format.ExactlyOf", "%s %s", core.A("Number", core.Exactly(got)), core.A("Unit", core.UnitTerm(j.Base))), + core.Says("format.ExactlyOf", "%s %s", core.A("Number", core.Exactly(j.Max)), core.A("Unit", core.UnitTerm(j.Base))) +} + +// Describe is the rule as one sentence, for the format list and for a window. +func (j JointLimit) Describe() string { + return fmt.Sprintf("%s times %s cannot pass %d %s, because %s", + j.Of, j.By, j.Most(), j.Unit, j.Why) +} + +// Most is the ceiling counted in Unit - 40 for forty million pixels in +// megapixels - which is the number a sentence about this limit says. Here +// rather than worked out by whoever words the sentence, so the window saying it +// in another language divides the same way. +func (j JointLimit) Most() int64 { return j.Max / j.per() } + +func (j JointLimit) per() int64 { + if j.Per == 0 { + return 1 + } + return j.Per +} diff --git a/internal/format/jpg/jpg.go b/internal/format/jpg/jpg.go index 815cecc9..c73a001a 100644 --- a/internal/format/jpg/jpg.go +++ b/internal/format/jpg/jpg.go @@ -159,11 +159,12 @@ func checkJointLimits(w, h int) error { return err } for _, j := range d.JointLimits { - if bad := j.Allows(int64(w), int64(h)); bad != "" { + if bad := j.Allows("jpg", int64(w), int64(h)); !bad.IsZero() { return &format.PropertyValueError{ - Format: "jpg", Key: j.Of + " and " + j.By, - Value: fmt.Sprintf("%dx%d", w, h), - Reason: bad + fmt.Sprintf(". Each side may go up to %d, but not both at once - ask for a smaller pair", maxDimension), + Format: "jpg", Key: j.Of + " and " + j.By, Subject: j.Subject(), + Value: fmt.Sprintf("%dx%d", w, h), + Reason: core.Says("format.JointEachSide", "%s. Each side may go up to %d, but not both at once - ask for a smaller pair", + core.A("Why", bad), core.A("Most", maxDimension)), } } } @@ -238,20 +239,16 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "JPG", Requested: r.Bytes, Minimum: floor, - Reason: fmt.Sprintf( - "a %dx%d picture at quality %d encodes to %d B and always carries a comment segment, which costs %d B even when empty", - w, h, quality, bare, comOverhead), - Hint: fmt.Sprintf("Ask for %d B or more, or set a smaller width and height, or a lower quality", floor), + Reason: core.Says("format.AXPictureAtQualityEncodes", "a %dx%d picture at quality %d encodes to %d B and always carries a comment segment, which costs %d B even when empty", core.A("W", w), core.A("H", h), core.A("Quality", quality), core.A("Bare", bare), core.A("ComOverhead", comOverhead)), + Hint: core.Says("format.AskForBOrMoreOr", "Ask for %d B or more, or set a smaller width and height, or a lower quality", core.A("Floor", floor)), } } m.segments, m.padPayload = segmentsFor(r.Bytes - bare) if r.Label && !imagelabel.Fits(w, len(label)) { p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", - w), + Code: "label_omitted", + Detail: core.Says("format.ThePictureIsPxWideAnd", "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", core.A("Width", w)), }) } p.Properties[format.PropertyLabelEmbedded] = r.Label && imagelabel.Fits(w, len(label)) @@ -262,7 +259,7 @@ func (generator) Plan(r format.Request) (format.Plan, error) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("jpg: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("jpg: the plan was not produced by this generator")) } select { @@ -289,8 +286,8 @@ func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { return err } if skip.written+soiSize != m.body { - return fmt.Errorf("jpg: the picture encoded to %d B where planning said %d B", - skip.written+soiSize, m.body) + return core.Defect(fmt.Errorf("jpg: the picture encoded to %d B where planning said %d B", + skip.written+soiSize, m.body)) } return nil } @@ -355,10 +352,10 @@ func qualityOf(props map[string]string) (int, error) { } n, err := strconv.Atoi(raw) if err != nil { - return 0, fmt.Errorf("jpg: quality must be a whole number, got %q", raw) + return 0, core.Defect(fmt.Errorf("jpg: quality must be a whole number, got %q", raw)) } if n < minQuality || n > maxQuality { - return 0, fmt.Errorf("jpg: quality must be between %d and %d, got %d", minQuality, maxQuality, n) + return 0, core.Defect(fmt.Errorf("jpg: quality must be between %d and %d, got %d", minQuality, maxQuality, n)) } return n, nil } @@ -503,7 +500,7 @@ func writeComments(ctx context.Context, w io.Writer, seed uint64, segments int, n++ } if n > maxCOMPayload { - return fmt.Errorf("jpg: a comment segment of %d B was planned and the format allows %d", n, maxCOMPayload) + return core.Defect(fmt.Errorf("jpg: a comment segment of %d B was planned and the format allows %d", n, maxCOMPayload)) } head[2] = byte((n + 2) >> 8) @@ -575,7 +572,7 @@ func (h *headSkipper) Write(p []byte) (int, error) { func (h *headSkipper) check() error { if len(h.head) != soiSize || h.head[0] != 0xFF || h.head[1] != 0xD8 { - return fmt.Errorf("jpg: the encoder did not begin with the start of image marker, got % x", h.head) + return core.Defect(fmt.Errorf("jpg: the encoder did not begin with the start of image marker, got % x", h.head)) } return nil } diff --git a/internal/format/jsonfile/json.go b/internal/format/jsonfile/json.go index 3bb3040e..78286763 100644 --- a/internal/format/jsonfile/json.go +++ b/internal/format/jsonfile/json.go @@ -115,10 +115,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "JSON", Requested: r.Bytes, Minimum: min, - Reason: fmt.Sprintf( - "a document holds whole records, and one %s record with every value type needs that much", - s.name), - Hint: fmt.Sprintf("Ask for %d B or more.", min), + Reason: core.Says("format.ADocumentHoldsWholeRecordsAnd", "a document holds whole records, and one %s record with every value type needs that much", core.A("Name", s.name)), + Hint: core.Says("format.AskForBOrMore", "Ask for %d B or more.", core.A("Min", min)), } } @@ -142,7 +140,7 @@ func (generator) Plan(r format.Request) (format.Plan, error) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("json: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("json: the plan was not produced by this generator")) } if err := core.WriteAll(w, []byte(m.s.prologue)); err != nil { diff --git a/internal/format/jsonfile/style.go b/internal/format/jsonfile/style.go index 315e9634..75fb302b 100644 --- a/internal/format/jsonfile/style.go +++ b/internal/format/jsonfile/style.go @@ -14,6 +14,10 @@ // JSON that no parser will complain about. package jsonfile +import ( + "github.com/donislawdev/TestingFilesGenerator/internal/core" +) + import "github.com/donislawdev/TestingFilesGenerator/internal/format" // Setting name. A public name, so it is spelled once. @@ -162,7 +166,10 @@ func parseStyle(props map[string]string) (style, error) { if !known { return style{}, &format.PropertyValueError{ Format: "json", Key: Formatting, Value: v, - Reason: "it has to be " + Indented + ", " + Minified + " or " + RecordPerLine, + Reason: core.Says("json.HasToBeStyle", "it has to be %s, %s or %s", + core.A("Indented", core.Choice{Of: Formatting, Value: Indented}), + core.A("Minified", core.Choice{Of: Formatting, Value: Minified}), + core.A("RecordPerLine", core.Choice{Of: Formatting, Value: RecordPerLine})), } } return s, nil diff --git a/internal/format/jxl/codec.go b/internal/format/jxl/codec.go index 8ea867d2..f56e3bb3 100644 --- a/internal/format/jxl/codec.go +++ b/internal/format/jxl/codec.go @@ -7,6 +7,8 @@ import ( "image" "github.com/gen2brain/jxl" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) const ( @@ -61,7 +63,7 @@ func encode(m image.Image, quality int) ([]byte, error) { Threads: encodeThreads, }) if err != nil { - return nil, fmt.Errorf("jxl: the encoder could not code the picture: %w", err) + return nil, core.Defect(fmt.Errorf("jxl: the encoder could not code the picture: %w", err)) } return buf.Bytes(), nil } diff --git a/internal/format/jxl/jxl.go b/internal/format/jxl/jxl.go index 34c3c503..dd8102f7 100644 --- a/internal/format/jxl/jxl.go +++ b/internal/format/jxl/jxl.go @@ -248,10 +248,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { carries := labelled(m.width, label) if r.Label && !carries { p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", - m.width), + Code: "label_omitted", + Detail: core.Says("format.ThePictureIsPxWideAnd", "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", core.A("Width", m.width)), }) } p.Properties[format.PropertyLabelEmbedded] = carries @@ -267,9 +265,9 @@ func (generator) Plan(r format.Request) (format.Plan, error) { // size is the floor that applies. func checkFits(want int64, m memo) error { floor := int64(minimumBytes) - reason := fmt.Sprintf( + reason := core.Says("jxl.MinimumReason", "the smallest picture this format draws codes to %d B at worst, the container around it costs %d B, and the file always carries a free box, which costs %d B even when it holds nothing", - minimumBytes-containerOverhead-boxHeader, containerOverhead, boxHeader) + core.A("Coded", minimumBytes-containerOverhead-boxHeader), core.A("Container", containerOverhead), core.A("Box", boxHeader)) // A picture named by hand can be larger than the smallest one, and then its // own size is the floor that applies - which is known here, because naming @@ -277,9 +275,10 @@ func checkFits(want int64, m memo) error { if m.coded != nil { if own := int64(len(m.coded) + containerOverhead + boxHeader); own > floor { floor = own - reason = fmt.Sprintf( + reason = core.Says("jxl.MinimumReasonOwn", "a %dx%d picture at quality %d codes to %d B, the container around it costs %d B, and the file always carries a free box, which costs %d B even when it holds nothing", - m.width, m.height, m.quality, len(m.coded), containerOverhead, boxHeader) + core.A("Width", m.width), core.A("Height", m.height), core.A("Quality", m.quality), core.A("Coded", len(m.coded)), + core.A("Container", containerOverhead), core.A("Box", boxHeader)) } } @@ -291,7 +290,7 @@ func checkFits(want int64, m memo) error { Requested: want, Minimum: floor, Reason: reason, - Hint: fmt.Sprintf("Ask for %d B or more, or set a smaller width and height, or a lower quality", floor), + Hint: core.Says("format.AskForBOrMoreOr", "Ask for %d B or more, or set a smaller width and height, or a lower quality", core.A("Floor", floor)), } } @@ -416,11 +415,11 @@ func checkJointLimits(w, h int) error { return err } for _, j := range d.JointLimits { - if bad := j.Allows(int64(w), int64(h)); bad != "" { + if bad := j.Allows("jxl", int64(w), int64(h)); !bad.IsZero() { return &format.PropertyValueError{ - Format: "jxl", Key: j.Of + " and " + j.By, + Format: "jxl", Key: j.Of + " and " + j.By, Subject: j.Subject(), Value: fmt.Sprintf("%dx%d", w, h), - Reason: bad + ". Ask for a smaller pair", + Reason: core.Says("format.JointSmallerPair", "%s. Ask for a smaller pair", core.A("Why", bad)), } } } @@ -434,10 +433,10 @@ func quality(props map[string]string) (int, error) { } n, err := strconv.Atoi(raw) if err != nil { - return 0, fmt.Errorf("jxl: quality must be a whole number, got %q", raw) + return 0, core.Defect(fmt.Errorf("jxl: quality must be a whole number, got %q", raw)) } if n < minQuality || n > maxQuality { - return 0, fmt.Errorf("jxl: quality must be between %d and %d, got %d", minQuality, maxQuality, n) + return 0, core.Defect(fmt.Errorf("jxl: quality must be between %d and %d, got %d", minQuality, maxQuality, n)) } return n, nil } @@ -445,7 +444,7 @@ func quality(props map[string]string) (int, error) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("jxl: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("jxl: the plan was not produced by this generator")) } select { @@ -469,8 +468,8 @@ func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { if pad < boxHeader { // The ladder's ceiling for this picture was too low. Said out loud // rather than written short, because a short file is a wrong file. - return fmt.Errorf("jxl: a %dx%d picture coded to %d B, the container costs %d B and the file was to be %d B, which leaves no room for the free box every one of these carries", - m.width, m.height, len(coded), containerOverhead, m.total) + return core.Defect(fmt.Errorf("jxl: a %dx%d picture coded to %d B, the container costs %d B and the file was to be %d B, which leaves no room for the free box every one of these carries", + m.width, m.height, len(coded), containerOverhead, m.total)) } if _, err := w.Write(signature[:]); err != nil { diff --git a/internal/format/logfile/log.go b/internal/format/logfile/log.go index 70d27e56..3b95fb8e 100644 --- a/internal/format/logfile/log.go +++ b/internal/format/logfile/log.go @@ -143,9 +143,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "LOG", Requested: r.Bytes, Minimum: min, - Reason: fmt.Sprintf( - "a log holds whole entries and one entry in the %s shape needs that much", opt.shape.id), - Hint: fmt.Sprintf("Ask for %d B or more, or choose a shorter entry_format.", min), + Reason: core.Says("format.ALogHoldsWholeEntriesAnd", "a log holds whole entries and one entry in the %s shape needs that much", core.A("ID", opt.shape.id)), + Hint: core.Says("format.AskForBOrMoreOr4", "Ask for %d B or more, or choose a shorter entry_format.", core.A("Min", min)), } } @@ -192,10 +191,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { m.labelLine = line } else { p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The label line needs %d B and this file has no room for it beside a whole entry. Its name and the manifest still identify it.", - len(line)), + Code: "label_omitted", + Detail: core.Says("format.TheLabelLineNeedsBAnd", "The label line needs %d B and this file has no room for it beside a whole entry. Its name and the manifest still identify it.", core.A("Length", len(line))), }) } } @@ -208,7 +205,7 @@ func (generator) Plan(r format.Request) (format.Plan, error) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("log: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("log: the plan was not produced by this generator")) } remaining := p.Bytes diff --git a/internal/format/logfile/options.go b/internal/format/logfile/options.go index a22caedf..077eda0b 100644 --- a/internal/format/logfile/options.go +++ b/internal/format/logfile/options.go @@ -14,6 +14,8 @@ import ( "strconv" "github.com/donislawdev/TestingFilesGenerator/internal/format" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) const ( @@ -115,7 +117,7 @@ func readShape(props map[string]string, o *options) error { } s, known := shapes[v] if !known { - return badValue("entry_format", v, "it is not one of the shapes this format writes") + return badValue("entry_format", v, core.Says("format.ItIsNotOneOfThe", "it is not one of the shapes this format writes")) } o.shape = s return nil @@ -132,7 +134,7 @@ func readLineEnding(props map[string]string, o *options) error { case "crlf": o.eol = "\r\n" default: - return badValue("line_ending", v, "it has to be lf or crlf") + return badValue("line_ending", v, core.Says("format.ItHasToBeLfOr", "it has to be lf or crlf")) } o.lineEnding = v return nil @@ -144,7 +146,7 @@ func readTimestamps(props map[string]string, o *options) error { return nil } if v != "advancing" && v != "fixed" { - return badValue("timestamps", v, "it has to be advancing or fixed") + return badValue("timestamps", v, core.Says("format.ItHasToBeAdvancingOr", "it has to be advancing or fixed")) } o.timestamps = v o.advancing = v == "advancing" @@ -158,17 +160,17 @@ func readRate(props map[string]string, o *options) error { } n, err := strconv.Atoi(v) if err != nil { - return fmt.Errorf("log: rate must be a whole number of entries per second, got %q", v) + return core.Defect(fmt.Errorf("log: rate must be a whole number of entries per second, got %q", v)) } if n < minRate || n > maxRate { - return fmt.Errorf("log: rate must be between %d and %d entries per second, got %d", minRate, maxRate, n) + return core.Defect(fmt.Errorf("log: rate must be between %d and %d entries per second, got %d", minRate, maxRate, n)) } // A rate somebody CHOSE while the clock is held still would change nothing // at all, so it is said out loud rather than ignored. A rate sitting at its // default was not chosen - see asked. if _, chosen := asked(props, "rate", strconv.Itoa(defaultRate)); chosen && !o.advancing { - return conflict("rate and timestamps", v, - "a rate says how fast entries arrive and timestamps=fixed puts them all at one instant") + return conflict("rate", "timestamps", v, + core.Says("format.ARateSaysHowFastEntries", "a rate says how fast entries arrive and timestamps=fixed puts them all at one instant")) } o.rate = n return nil @@ -181,9 +183,9 @@ func readMethods(props map[string]string, o *options) error { } set, known := methodSets[v] if !known { - return badValue("methods", v, "it has to be get, read or mixed") + return badValue("methods", v, core.Says("format.ItHasToBeGetRead", "it has to be get, read or mixed")) } - if err := refuseUnlessWeb(props, o.shape, "methods", "get", v, "request method"); err != nil { + if err := refuseUnlessWeb(props, o.shape, "methods", "get", v, core.Says("log.RequestMethod", "request method")); err != nil { return err } o.methodMix, o.methods = v, set @@ -197,14 +199,14 @@ func readStatusMix(props map[string]string, o *options) error { } set, known := statusSets[v] if !known { - return badValue("status_mix", v, "it has to be realistic, success, client-errors or server-errors") + return badValue("status_mix", v, core.Says("format.ItHasToBeRealisticSuccess", "it has to be realistic, success, client-errors or server-errors")) } // JSON lines carry a status of their own, so this one applies there too - it // is the address and the method that have no place outside a web shape. if _, chosen := asked(props, "status_mix", "realistic"); chosen && !o.shape.web && o.shape.id != "json-lines" { - return conflict("status_mix and entry_format", v, - "the "+o.shape.id+" shape carries no response code") + return conflict("status_mix", "entry_format", v, + core.Says("log.ShapeNoResponseCode", "the %s shape carries no response code", core.A("Shape", core.Choice{Of: "entry_format", Value: o.shape.id}))) } o.statusMix, o.statuses = v, set return nil @@ -224,15 +226,15 @@ func readLevelMix(props map[string]string, o *options) error { } set, known := levelSets[v] if !known { - return badValue("level_mix", v, "it has to be realistic, quiet, errors or debug") + return badValue("level_mix", v, core.Says("format.ItHasToBeRealisticQuiet", "it has to be realistic, quiet, errors or debug")) } // Only a real choice can disagree with the shape - see asked. A window // sends this key on every run, so refusing whenever it arrived would put // the four shapes without a level out of reach from the window entirely, // which is the defect reported from a screenshot on 2026-08-31. if _, chosen := asked(props, "level_mix", "realistic"); chosen && !o.shape.levelled { - return conflict("level_mix and entry_format", v, - "the "+o.shape.id+" shape carries no severity") + return conflict("level_mix", "entry_format", v, + core.Says("log.ShapeNoSeverity", "the %s shape carries no severity", core.A("Shape", core.Choice{Of: "entry_format", Value: o.shape.id}))) } o.levelMix, o.levels = v, set return nil @@ -250,9 +252,9 @@ func readIPVersion(props map[string]string, o *options) error { case "mixed": o.ipMixed = true default: - return badValue("ip_version", v, "it has to be v4, v6 or mixed") + return badValue("ip_version", v, core.Says("format.ItHasToBeV4V6", "it has to be v4, v6 or mixed")) } - if err := refuseUnlessWeb(props, o.shape, "ip_version", "v4", v, "client address"); err != nil { + if err := refuseUnlessWeb(props, o.shape, "ip_version", "v4", v, core.Says("log.ClientAddress", "client address")); err != nil { return err } o.ipVersion = v @@ -262,7 +264,7 @@ func readIPVersion(props map[string]string, o *options) error { // refuseUnlessWeb refuses a CHOSEN setting that only means something for a // shape carrying a request. A value left at its default was not chosen and so // cannot disagree with anything - see asked. -func refuseUnlessWeb(props map[string]string, s *shape, key, def, val, what string) error { +func refuseUnlessWeb(props map[string]string, s *shape, key, def, val string, what core.Said) error { if _, chosen := asked(props, key, def); !chosen { return nil } @@ -271,11 +273,12 @@ func refuseUnlessWeb(props map[string]string, s *shape, key, def, val, what stri // needsWeb refuses a setting that only means something for a shape carrying a // request. -func needsWeb(s *shape, key, val, what string) error { +func needsWeb(s *shape, key, val string, what core.Said) error { if s.web { return nil } - return conflict(key+" and entry_format", val, "the "+s.id+" shape carries no "+what) + return conflict(key, "entry_format", val, core.Says("log.ShapeCarriesNo", "the %s shape carries no %s", + core.A("Shape", core.Choice{Of: "entry_format", Value: s.id}), core.A("What", what))) } func value(props map[string]string, key string) (string, bool) { @@ -311,15 +314,16 @@ func asked(props map[string]string, key, def string) (string, bool) { return v, true } -func badValue(key, val, why string) error { +func badValue(key, val string, why core.Said) error { return &format.PropertyValueError{Format: "log", Key: key, Value: val, Reason: why} } // conflict names both settings, because naming one of a pair leaves the reader // to guess which of the two to change. -func conflict(keys, val, why string) error { +func conflict(one, other, val string, why core.Said) error { return &format.PropertyValueError{ - Format: "log", Key: keys, Value: val, - Reason: why + ". Drop one of the two, or change the other", + Format: "log", Key: one + " and " + other, Value: val, + Subject: core.Says("format.TwoSettings", "%s and %s", core.A("Of", core.LabelTerm(one)), core.A("By", core.LabelTerm(other))), + Reason: core.Says("log.ConflictDrop", "%s. Drop one of the two, or change the other", core.A("Why", why)), } } diff --git a/internal/format/md/md.go b/internal/format/md/md.go index 63869846..92baaa17 100644 --- a/internal/format/md/md.go +++ b/internal/format/md/md.go @@ -89,8 +89,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "MD", Requested: r.Bytes, Minimum: 0, - Reason: "a file cannot hold fewer than zero bytes", - Hint: "Ask for 0 B or more.", + Reason: core.Says("format.AFileCannotHoldFewerThan", "a file cannot hold fewer than zero bytes"), + Hint: core.Says("format.AskFor0BOrMore", "Ask for 0 B or more."), } } @@ -125,10 +125,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { // What the label COSTS here, not how long it reads - the two // differ by a factor of two in UTF-16. p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The label needs %d B and the file is %d B, so this file carries no label. Its name and the manifest still identify it.", - codec.Cost(int64(len(line))), r.Bytes), + Code: "label_omitted", + Detail: core.Says("format.TheLabelNeedsBAndThe", "The label needs %d B and the file is %d B, so this file carries no label. Its name and the manifest still identify it.", core.A("Length", codec.Cost(int64(len(line)))), core.A("Bytes", r.Bytes)), }) } } @@ -141,7 +139,7 @@ func (generator) Plan(r format.Request) (format.Plan, error) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("md: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("md: the plan was not produced by this generator")) } // The mark is bytes rather than text, so it goes out as itself, and @@ -327,7 +325,7 @@ func fillProse(ctx context.Context, w io.Writer, rng *rand.Rand, remaining int64 // A round emitting nothing would spin for ever, and a run that hangs // cannot be told apart from a very large file. if len(buf) == 0 { - return fmt.Errorf("md: made no progress with %d B still owed", remaining) + return core.Defect(fmt.Errorf("md: made no progress with %d B still owed", remaining)) } if err := writeAll(w, buf); err != nil { diff --git a/internal/format/opc/opc.go b/internal/format/opc/opc.go index eb19d4e7..026df7c6 100644 --- a/internal/format/opc/opc.go +++ b/internal/format/opc/opc.go @@ -206,7 +206,7 @@ func Settle(parts []Part, shape Shape, want int64) (Package, error) { case delta == 0: return p, nil case delta < 0: - return Package{}, fmt.Errorf("opc: %d B is below the %d B floor", want, shape.Bare) + return Package{}, core.Defect(fmt.Errorf("opc: %d B is below the %d B floor", want, shape.Bare)) case delta <= CommentLimit: p.Comment = int(delta) return p, nil @@ -237,10 +237,10 @@ func write(ctx context.Context, w io.Writer, p Package, withPadding bool) error zw := zip.NewWriter(w) if p.Comment > 0 { if p.Comment > CommentLimit { - return fmt.Errorf("opc: a comment of %d B is past the %d B a reader accepts", p.Comment, CommentLimit) + return core.Defect(fmt.Errorf("opc: a comment of %d B is past the %d B a reader accepts", p.Comment, CommentLimit)) } if err := zw.SetComment(comment(p.Comment)); err != nil { - return fmt.Errorf("opc: the archive comment was refused: %w", err) + return core.Defect(fmt.Errorf("opc: the archive comment was refused: %w", err)) } } diff --git a/internal/format/pdf/info.go b/internal/format/pdf/info.go index 026d7842..18cdcb1e 100644 --- a/internal/format/pdf/info.go +++ b/internal/format/pdf/info.go @@ -13,6 +13,8 @@ import ( "unicode/utf8" "github.com/donislawdev/TestingFilesGenerator/internal/format" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) const ( @@ -66,8 +68,8 @@ func readInfo(props map[string]string) (docInfo, error) { if raw := props[k]; !utf8.ValidString(raw) { return docInfo{}, &format.PropertyValueError{ Format: "pdf", Key: k, Value: strings.ToValidUTF8(raw, "?"), - Reason: "it takes text, and this value is not valid UTF-8", - Remedy: "Write the value in UTF-8.", + Reason: core.Says("format.ItTakesTextAndThisValue", "it takes text, and this value is not valid UTF-8"), + Remedy: core.Says("format.WriteTheValueInUTF8", "Write the value in UTF-8."), } } } @@ -115,15 +117,15 @@ func readDate(props map[string]string, key, fallback string) (date, error) { if m == nil { return date{}, &format.PropertyValueError{ Format: "pdf", Key: key, Value: raw, - Reason: "it takes a date written as 2024-02-29, 2024-02-29T13:45:00 or 2024-02-29T13:45:00+02:00, or none", - Remedy: "Write the date in one of those three ways, with no fraction of a second.", + Reason: core.Says("format.ItTakesADateWrittenAs", "it takes a date written as 2024-02-29, 2024-02-29T13:45:00 or 2024-02-29T13:45:00+02:00, or none"), + Remedy: core.Says("format.WriteTheDateInOneOf", "Write the date in one of those three ways, with no fraction of a second."), } } if !exists(m) { return date{}, &format.PropertyValueError{ Format: "pdf", Key: key, Value: raw, - Reason: "no calendar has that day, time or zone", - Remedy: "Write a date that exists, such as 2024-02-29T13:45:00+02:00.", + Reason: core.Says("format.NoCalendarHasThatDayTime", "no calendar has that day, time or zone"), + Remedy: core.Says("format.WriteADateThatExistsSuch", "Write a date that exists, such as 2024-02-29T13:45:00+02:00."), } } pdf := "D:" + m[1] + m[2] + m[3] + m[4] + m[5] + m[6] diff --git a/internal/format/pdf/minimum.go b/internal/format/pdf/minimum.go index 8c3a82dc..42ebb1d4 100644 --- a/internal/format/pdf/minimum.go +++ b/internal/format/pdf/minimum.go @@ -3,27 +3,25 @@ package pdf // The smallest document this format produces, and how a refusal explains it. import ( - "fmt" - "strings" + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // documentWords names the document a refusal is about, the way a person would: // "1 page A4 document", "3 page mixed size upright and landscape document". // The words for the settings nobody changed are the words this refusal always // used. -func documentWords(o options) string { - size := o.sizeName - if size == mixed { - size = "mixed size" +func documentWords(o options) core.Said { + size := core.Says("pdf.Size", "%s", core.A("Size", core.Choice{Of: "page_size", Value: o.sizeName})) + if o.sizeName == mixed { + size = core.Says("pdf.MixedSize", "mixed size") } - way := "" switch o.orientation { case orientLandscape: - way = " landscape" + return core.Says("pdf.DocumentLandscape", "%d page %s landscape document", core.A("Pages", o.pages), core.A("Size", size)) case mixed: - way = " upright and landscape" + return core.Says("pdf.DocumentMixed", "%d page %s upright and landscape document", core.A("Pages", o.pages), core.A("Size", size)) } - return fmt.Sprintf("%d page %s%s document", o.pages, size, way) + return core.Says("pdf.Document", "%d page %s document", core.A("Pages", o.pages), core.A("Size", size)) } // carrying and cleanHint keep the message about the minimum honest. The @@ -31,30 +29,30 @@ func documentWords(o options) string { // said about it, so a user who left the label on, or typed a title, and hits // the limit needs to be told why the number they were shown is not the number // they got. -func carrying(label bool, o options) string { - var what []string +func carrying(label bool, o options) core.Said { + var what core.Conjoined if label { - what = append(what, "the self describing label") + what = append(what, core.Says("pdf.TheLabel", "the self describing label")) } if o.info.asked { - what = append(what, "the document properties that were set") + what = append(what, core.Says("pdf.TheProperties", "the document properties that were set")) } if len(what) == 0 { - return "" + return core.Said{} } - return " carrying " + strings.Join(what, " and ") + return core.Says("pdf.Carrying", " carrying %s", core.A("What", what)) } -func cleanHint(label bool, o options) string { +func cleanHint(label bool, o options) core.Said { switch { case label && o.info.asked: - return ", ask for fewer pages by setting pages to 1, shorten the document properties, or drop the label" + return core.Says("pdf.HintAll", ", ask for fewer pages by setting pages to 1, shorten the document properties, or drop the label") case label: - return ", ask for fewer pages by setting pages to 1, or drop the label" + return core.Says("pdf.HintLabel", ", ask for fewer pages by setting pages to 1, or drop the label") case o.info.asked: - return ", ask for fewer pages by setting pages to 1, or shorten the document properties" + return core.Says("pdf.HintProperties", ", ask for fewer pages by setting pages to 1, or shorten the document properties") } - return " or ask for fewer pages by setting pages to 1" + return core.Says("pdf.HintPages", " or ask for fewer pages by setting pages to 1") } // minimumBytes is the smallest document this generator can produce: one A4 diff --git a/internal/format/pdf/padding.go b/internal/format/pdf/padding.go index e4f3fde1..5b6352e9 100644 --- a/internal/format/pdf/padding.go +++ b/internal/format/pdf/padding.go @@ -46,7 +46,7 @@ func writeComment(ctx context.Context, w io.Writer, seed uint64, n int64) error return nil } if n < minComment { - return fmt.Errorf("pdf: %d B of padding cannot be written as a comment", n) + return core.Defect(fmt.Errorf("pdf: %d B of padding cannot be written as a comment", n)) } // Line length is fixed so the block stays readable in an editor. The last @@ -79,7 +79,7 @@ func writeComment(ctx context.Context, w io.Writer, seed uint64, n int64) error size = lineLen - minComment } if size < minComment { - return fmt.Errorf("pdf: %d B left over, which cannot form a comment line", size) + return core.Defect(fmt.Errorf("pdf: %d B left over, which cannot form a comment line", size)) } buf = append(buf, '%') for i := int64(0); i < size-2; i++ { diff --git a/internal/format/pdf/pdf.go b/internal/format/pdf/pdf.go index f2affa84..e47abe2d 100644 --- a/internal/format/pdf/pdf.go +++ b/internal/format/pdf/pdf.go @@ -89,9 +89,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "PDF", Requested: r.Bytes, Minimum: floor, - Reason: fmt.Sprintf("a %s%s already needs that much before any padding", - documentWords(opts), carrying(r.Label, opts)), - Hint: fmt.Sprintf("Ask for %d B or more%s.", floor, cleanHint(r.Label, opts)), + Reason: core.Says("format.AAlreadyNeedsThatMuchBefore", "a %s%s already needs that much before any padding", core.A("Opts", documentWords(opts)), core.A("Carrying", carrying(r.Label, opts))), + Hint: core.Says("format.AskForBOrMore2", "Ask for %d B or more%s.", core.A("Floor", floor), core.A("CleanHint", cleanHint(r.Label, opts))), } case r.Bytes < bare+minComment: // A comment is a per cent sign and a newline at the very least, so @@ -100,10 +99,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "PDF", Requested: r.Bytes, Minimum: bare + minComment, - Reason: fmt.Sprintf( - "this document is exactly %d B and the shortest comment that could pad it is %d B, so one byte more than the document is the single size in between that cannot be reached", - bare, minComment), - Hint: fmt.Sprintf("Ask for exactly %d B or for %d B or more.", bare, bare+minComment), + Reason: core.Says("format.ThisDocumentIsExactlyBAnd", "this document is exactly %d B and the shortest comment that could pad it is %d B, so one byte more than the document is the single size in between that cannot be reached", core.A("Bare", bare), core.A("MinComment", minComment)), + Hint: core.Says("format.AskForExactlyBOrFor", "Ask for exactly %d B or for %d B or more.", core.A("Bare", bare), core.A("Bare2", bare+minComment)), } default: m.padLen = r.Bytes - bare @@ -161,7 +158,7 @@ func described(m memo, labelled bool) map[string]any { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("pdf: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("pdf: the plan was not produced by this generator")) } select { diff --git a/internal/format/pdf/settings.go b/internal/format/pdf/settings.go index a38baf86..4a5b4b4e 100644 --- a/internal/format/pdf/settings.go +++ b/internal/format/pdf/settings.go @@ -9,6 +9,8 @@ import ( "strings" "github.com/donislawdev/TestingFilesGenerator/internal/format" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) const ( @@ -87,7 +89,8 @@ func readOptions(props map[string]string) (options, error) { } if raw := props["orientation"]; raw != "" { if raw == mixed && pages < 2 { - return options{}, needsTwoPages("orientation", "choose portrait or landscape") + return options{}, needsTwoPages("orientation", core.Says("pdf.ChooseOneWay", "choose %s or %s", + core.A("Portrait", core.Choice{Of: "orientation", Value: orientPortrait}), core.A("Landscape", core.Choice{Of: "orientation", Value: orientLandscape}))) } o.orientation = raw } @@ -110,10 +113,10 @@ func pageCount(props map[string]string) (int, error) { } n, err := strconv.Atoi(raw) if err != nil { - return 0, fmt.Errorf("pdf: pages must be a whole number, got %q", raw) + return 0, core.Defect(fmt.Errorf("pdf: pages must be a whole number, got %q", raw)) } if n < 1 || n > maxPages { - return 0, fmt.Errorf("pdf: pages must be between 1 and %d, got %d", maxPages, n) + return 0, core.Defect(fmt.Errorf("pdf: pages must be between 1 and %d, got %d", maxPages, n)) } return n, nil } @@ -127,7 +130,7 @@ func paperSizes(props map[string]string, pages int) ([]pageSize, string, error) } if raw == mixed { if pages < 2 { - return nil, "", needsTwoPages("page_size", "choose one size such as a4") + return nil, "", needsTwoPages("page_size", core.Says("pdf.ChooseOneSize", "choose one size such as a4")) } cycle := make([]pageSize, 0, len(mixedSizes)) for _, k := range mixedSizes { @@ -141,7 +144,7 @@ func paperSizes(props map[string]string, pages int) ([]pageSize, string, error) for k := range pageSizes { names = append(names, k) } - return nil, "", fmt.Errorf("pdf: page_size %q is not one of: %s", raw, strings.Join(sorted(names), ", ")) + return nil, "", core.Defect(fmt.Errorf("pdf: page_size %q is not one of: %s", raw, strings.Join(sorted(names), ", "))) } return []pageSize{s}, s.name, nil } @@ -152,13 +155,13 @@ func paperSizes(props map[string]string, pages int) ([]pageSize, string, error) // size and one way up, with a manifest saying it is mixed. Somebody testing how // a reader copes with pages that differ would get a pass from a document where // nothing differs. -func needsTwoPages(key, other string) *format.PropertyValueError { +func needsTwoPages(key string, other core.Said) *format.PropertyValueError { return &format.PropertyValueError{ Format: "pdf", Key: key, Value: mixed, - Reason: "mixed pages need at least two pages and this document has one", - Remedy: fmt.Sprintf("Set pages to 2 or more, or %s.", other), + Reason: core.Says("format.MixedPagesNeedAtLeastTwo", "mixed pages need at least two pages and this document has one"), + Remedy: core.Says("format.SetPagesTo2OrMore", "Set pages to 2 or more, or %s.", core.A("Other", other)), } } @@ -172,7 +175,7 @@ func rotation(props map[string]string) (int, error) { case "0", "90", "180", "270": return strconv.Atoi(raw) } - return 0, fmt.Errorf("pdf: rotate %q is not one of: 0, 90, 180, 270", raw) + return 0, core.Defect(fmt.Errorf("pdf: rotate %q is not one of: 0, 90, 180, 270", raw)) } func sorted(in []string) []string { diff --git a/internal/format/png/png.go b/internal/format/png/png.go index 7ad54fc6..54097fc2 100644 --- a/internal/format/png/png.go +++ b/internal/format/png/png.go @@ -143,11 +143,12 @@ func checkJointLimits(w, h int) error { return err } for _, j := range d.JointLimits { - if bad := j.Allows(int64(w), int64(h)); bad != "" { + if bad := j.Allows("png", int64(w), int64(h)); !bad.IsZero() { return &format.PropertyValueError{ - Format: "png", Key: j.Of + " and " + j.By, - Value: fmt.Sprintf("%dx%d", w, h), - Reason: bad + fmt.Sprintf(". Each side may go up to %d, but not both at once - ask for a smaller pair", maxDimension), + Format: "png", Key: j.Of + " and " + j.By, Subject: j.Subject(), + Value: fmt.Sprintf("%dx%d", w, h), + Reason: core.Says("format.JointEachSide", "%s. Each side may go up to %d, but not both at once - ask for a smaller pair", + core.A("Why", bad), core.A("Most", maxDimension)), } } } @@ -201,8 +202,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "PNG", Requested: r.Bytes, Minimum: bare, - Reason: fmt.Sprintf("a %dx%d picture already encodes to that much before any padding", w, h), - Hint: fmt.Sprintf("Ask for %d B or more, or set a smaller width and height", bare), + Reason: core.Says("format.AXPictureAlreadyEncodesTo", "a %dx%d picture already encodes to that much before any padding", core.A("Width", w), core.A("Height", h)), + Hint: core.Says("format.AskForBOrMoreOr2", "Ask for %d B or more, or set a smaller width and height", core.A("Bare", bare)), } case r.Bytes < bare+chunkOverhead: @@ -213,10 +214,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "PNG", Requested: r.Bytes, Minimum: bare + chunkOverhead, - Reason: fmt.Sprintf( - "a %dx%d picture encodes to exactly %d B, and the padding chunk that makes up any difference costs %d B on its own, so nothing between those two is reachable", - w, h, bare, chunkOverhead), - Hint: fmt.Sprintf("Ask for exactly %d B or for %d B or more.", bare, bare+chunkOverhead), + Reason: core.Says("format.AXPictureEncodesToExactly2", "a %dx%d picture encodes to exactly %d B, and the padding chunk that makes up any difference costs %d B on its own, so nothing between those two is reachable", core.A("W", w), core.A("H", h), core.A("Bare", bare), core.A("ChunkOverhead", chunkOverhead)), + Hint: core.Says("format.AskForExactlyBOrFor", "Ask for exactly %d B or for %d B or more.", core.A("Bare", bare), core.A("Bare2", bare+chunkOverhead)), } default: @@ -232,18 +231,16 @@ func (generator) Plan(r format.Request) (format.Plan, error) { // reachable total rather than as the chunk limit is what makes // the hint below something a person can act on. Maximum: bare + chunkOverhead + maxChunkData, - Reason: "the padding needed is larger than one chunk can carry, and this generator writes only one", - Hint: "Ask for a smaller size, or set larger dimensions so the picture itself carries more of it.", + Reason: core.Says("format.ThePaddingNeededIsLargerThan", "the padding needed is larger than one chunk can carry, and this generator writes only one"), + Hint: core.Says("format.AskForASmallerSizeOr", "Ask for a smaller size, or set larger dimensions so the picture itself carries more of it."), } } } if r.Label && !imagelabel.Fits(w, len(label)) { p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", - w), + Code: "label_omitted", + Detail: core.Says("format.ThePictureIsPxWideAnd", "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", core.A("Width", w)), }) } p.Properties[format.PropertyLabelEmbedded] = r.Label && imagelabel.Fits(w, len(label)) @@ -254,7 +251,7 @@ func (generator) Plan(r format.Request) (format.Plan, error) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("png: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("png: the plan was not produced by this generator")) } select { @@ -281,15 +278,15 @@ func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { if m.padData < 0 || m.padData > maxChunkData { // Unreachable unless ladderCeiling is wrong, and then it is better // to say so than to write a file of the wrong length. - return fmt.Errorf( + return core.Defect(fmt.Errorf( "png: the picture encoded to %d B, which leaves %d B of padding for a %d B file - ladderCeiling is wrong", - m.body, m.padData, p.Bytes) + m.body, m.padData, p.Bytes)) } } else if holder.written != m.body { - return fmt.Errorf("png: the picture encoded to %d B where planning said %d B", holder.written, m.body) + return core.Defect(fmt.Errorf("png: the picture encoded to %d B where planning said %d B", holder.written, m.body)) } if string(holder.tail[4:8]) != "IEND" { - return fmt.Errorf("png: the encoded stream does not end with IEND") + return core.Defect(fmt.Errorf("png: the encoded stream does not end with IEND")) } if m.withPad { diff --git a/internal/format/pptx/pptx.go b/internal/format/pptx/pptx.go index 090c3a0d..3c9e7066 100644 --- a/internal/format/pptx/pptx.go +++ b/internal/format/pptx/pptx.go @@ -134,9 +134,9 @@ func refusal(err error, want int64, shape opc.Shape, slides int) error { Format: "PPTX", Requested: big.Want, Maximum: big.Ceiling, - Reason: "an Office file is a ZIP archive, and this build works out its size before it writes it " + - "in a way that cannot account for the zip64 records a larger one needs", - Hint: "Ask for less than 4 GiB, or split the content across several files.", + Reason: core.Says("format.AnOfficeFileIsAZIP", "an Office file is a ZIP archive, and this build works out its size before it writes it "+ + "in a way that cannot account for the zip64 records a larger one needs"), + Hint: core.Says("format.AskForLessThan4GiB", "Ask for less than 4 GiB, or split the content across several files."), } } var gap *opc.Unreachable @@ -145,27 +145,23 @@ func refusal(err error, want int64, shape opc.Shape, slides int) error { Format: "PPTX", Requested: want, Minimum: gap.Above, - Reason: fmt.Sprintf( - "the archive comment carries at most %d B and the smallest extra part costs %d B, so nothing between those two is reachable", - opc.CommentLimit, shape.FillerOverhead), - Hint: fmt.Sprintf("Ask for %d B or less, or %d B or more.", gap.Below, gap.Above), + Reason: core.Says("format.TheArchiveCommentCarriesAtMost", "the archive comment carries at most %d B and the smallest extra part costs %d B, so nothing between those two is reachable", core.A("CommentLimit", opc.CommentLimit), core.A("FillerOverhead", shape.FillerOverhead)), + Hint: core.Says("format.AskForBOrLessOr", "Ask for %d B or less, or %d B or more.", core.A("Below", gap.Below), core.A("Above", gap.Above)), } } return &format.BelowMinimumError{ Format: "PPTX", Requested: want, Minimum: shape.Bare, - Reason: fmt.Sprintf( - "a deck of %s already packages to that much - a presentation needs a master, a layout and a theme whether it shows them or not", - core.Count(slides, "slide", "slides")), - Hint: fmt.Sprintf("Ask for %d B or more, or set fewer slides", shape.Bare), + Reason: core.Says("format.ADeckOfAlreadyPackagesTo", "a deck of %s already packages to that much - a presentation needs a master, a layout and a theme whether it shows them or not", core.A("Count", core.SaysN("pptx.Slides", "%d slide", "%d slides", core.A("Count", slides)))), + Hint: core.Says("format.AskForBOrMoreOr5", "Ask for %d B or more, or set fewer slides", core.A("Bare", shape.Bare)), } } func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("pptx: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("pptx: the plan was not produced by this generator")) } return opc.Write(ctx, w, m.pkg) } @@ -334,10 +330,10 @@ func intProperty(props map[string]string, key string, fallback int) (int, error) } n, err := strconv.Atoi(raw) if err != nil { - return 0, fmt.Errorf("pptx: %s must be a whole number, got %q", key, raw) + return 0, core.Defect(fmt.Errorf("pptx: %s must be a whole number, got %q", key, raw)) } if n < minSlides || n > maxSlides { - return 0, fmt.Errorf("pptx: %s must be between %d and %d, got %d", key, minSlides, maxSlides, n) + return 0, core.Defect(fmt.Errorf("pptx: %s must be between %d and %d, got %d", key, minSlides, maxSlides, n)) } return n, nil } diff --git a/internal/format/refusals.go b/internal/format/refusals.go index c0a971a6..8127ce1d 100644 --- a/internal/format/refusals.go +++ b/internal/format/refusals.go @@ -16,7 +16,6 @@ package format // but says something different about what to do next. import ( - "fmt" "strings" "github.com/donislawdev/TestingFilesGenerator/internal/core" @@ -47,10 +46,10 @@ type UnsupportedSetting struct { Name string // Why is the reason, in the words the refusal uses. It is about the file // format, not about this build. - Why string + Why core.Said // Instead is what to do about it, and it is allowed to be empty when there // is genuinely nothing else to do. - Instead string + Instead core.Said } // UnsupportedSettingError is a declared setting this format cannot carry. @@ -63,26 +62,38 @@ type UnsupportedSetting struct { type UnsupportedSettingError struct { Format string Key string - Reason string - Remedy string + Reason core.Said + Remedy core.Said } // AboutSetting is the key this refusal is about, so a form can mark the box. func (e *UnsupportedSettingError) AboutSetting() string { return e.Key } -func (e *UnsupportedSettingError) What() string { - return fmt.Sprintf("%s cannot take %q", e.Format, e.Key) +func (e *UnsupportedSettingError) What() string { return e.what().String() } + +func (e *UnsupportedSettingError) what() core.Said { + return core.Says("format.CannotTake", "%s cannot take %q", core.A("Format", e.Format), core.A("Key", e.Key)) } -func (e *UnsupportedSettingError) Why() string { return e.Reason } +func (e *UnsupportedSettingError) Why() string { return e.Reason.String() } + +func (e *UnsupportedSettingError) Instead() string { return e.Remedy.String() } + +// Parts is what happened, why and what to do instead, for a reader that lays +// them out apart and in its own language. +func (e *UnsupportedSettingError) Parts() (what, why, instead core.Said) { + return e.what(), e.Reason, e.Remedy +} -func (e *UnsupportedSettingError) Instead() string { return e.Remedy } +func (e *UnsupportedSettingError) Error() string { return e.Said().String() } -func (e *UnsupportedSettingError) Error() string { - if e.Remedy == "" { - return e.What() + " - " + e.Reason +// Said is the whole refusal, for a window that says it in its own language. +func (e *UnsupportedSettingError) Said() core.Said { + if e.Remedy.IsZero() { + return core.Says("format.CannotTakeWhy", "%s - %s", core.A("What", e.what()), core.A("Why", e.Reason)) } - return e.What() + " - " + e.Reason + ". " + e.Remedy + return core.Says("format.CannotTakeWhyFix", "%s - %s. %s", + core.A("What", e.what()), core.A("Why", e.Reason), core.A("Fix", e.Remedy)) } // cannotCarry says whether this format has declared the key as one it cannot @@ -112,35 +123,51 @@ type UnknownPropertyError struct { func (e *UnknownPropertyError) AboutSetting() string { return e.Key } // Why this is refused, for a report that keeps the four parts of D6 apart. -func (e *UnknownPropertyError) Why() string { - return "a format takes only the settings it declares, and one it does not know would be dropped on the way" +func (e *UnknownPropertyError) Why() string { return e.why().String() } + +func (e *UnknownPropertyError) why() core.Said { + return core.Says("format.UnknownPropertyWhy", "a format takes only the settings it declares, and one it does not know would be dropped on the way") } // Instead is what to do about it, named from the declaration. -func (e *UnknownPropertyError) Instead() string { +func (e *UnknownPropertyError) Instead() string { return e.instead().String() } + +func (e *UnknownPropertyError) instead() core.Said { if len(e.Known) == 0 { - return "remove the line" + return core.Says("format.RemoveTheLine", "remove the line") } - return "use one of: " + strings.Join(e.Known, ", ") + return core.Says("format.UseOneOf", "use one of: %s", core.A("Known", strings.Join(e.Known, ", "))) } // What happened, without the list of names. Kept apart from Error so a report // with four parts does not print the names twice - once in the sentence and // again in what to do instead. -func (e *UnknownPropertyError) What() string { +func (e *UnknownPropertyError) What() string { return e.what().String() } + +func (e *UnknownPropertyError) what() core.Said { if len(e.Known) == 0 { - return fmt.Sprintf("%s takes no properties, so %q is not one of them", e.Format, e.Key) + return core.Says("format.TakesNoProperties", "%s takes no properties, so %q is not one of them", + core.A("Format", e.Format), core.A("Key", e.Key)) } - return fmt.Sprintf("%s does not have a property called %q", e.Format, e.Key) + return core.Says("format.NoSuchProperty", "%s does not have a property called %q", core.A("Format", e.Format), core.A("Key", e.Key)) +} + +// Parts is what happened, why and what to do instead, for a reader that lays +// them out apart and in its own language. +func (e *UnknownPropertyError) Parts() (what, why, instead core.Said) { + return e.what(), e.why(), e.instead() } // Error is the whole thing in one sentence, unchanged to the character - it is // what the one-target path from the command line flags prints. -func (e *UnknownPropertyError) Error() string { +func (e *UnknownPropertyError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *UnknownPropertyError) Said() core.Said { if len(e.Known) == 0 { - return e.What() + return e.what() } - return e.What() + ". It takes: " + strings.Join(e.Known, ", ") + return core.Says("format.NoSuchPropertyKnown", "%s. It takes: %s", core.A("What", e.what()), core.A("Known", strings.Join(e.Known, ", "))) } // PropertyValueError is a declared key given a value the declaration forbids. @@ -155,7 +182,7 @@ type PropertyValueError struct { Format string Key string Value string - Reason string + Reason core.Said // Remedy is what to do about it, built from the declaration. It is carried // here rather than worked out by whoever reports this, because a refusal in // this tool has four parts - what happened, why, what is allowed, what to do @@ -166,7 +193,11 @@ type PropertyValueError struct { // Named Remedy rather than Instead because the accessor below has to be // called Instead - that is the name the other refusals in this package use // for the same part, and the reader that asks for all three asks by name. - Remedy string + Remedy core.Said + // Subject is the setting named as a sentence, where the key is not one + // setting a window has a box for - a pair whose product has a ceiling is + // keyed "width and height". Zero means the key names it. + Subject core.Said } // What happened, why the declaration forbids it, and what to do instead. @@ -174,13 +205,47 @@ type PropertyValueError struct { // Instead is the part Error leaves out on purpose - see the field - so this is // the only way a report gets all four parts of D6 for the refusal a person hits // most often, by typing a number. -func (e *PropertyValueError) What() string { - return fmt.Sprintf("%s: %s cannot be %q", e.Format, e.Key, e.Value) +func (e *PropertyValueError) What() string { return e.what().String() } + +func (e *PropertyValueError) what() core.Said { + return core.Says("format.CannotBe", "%s: %s cannot be %q", core.A("Format", e.Format), core.A("Key", e.Key), core.A("Value", e.Value)) } -func (e *PropertyValueError) Why() string { return core.InTheWordsOf(e.Reason, e.Key) } +func (e *PropertyValueError) Why() string { return core.InTheWordsOf(e.Reason.String(), e.Key) } -func (e *PropertyValueError) Instead() string { return e.Remedy } +func (e *PropertyValueError) Instead() string { return e.Remedy.String() } + +// Parts is what happened, why and what to do instead, for a reader that lays +// them out apart and in its own language. +func (e *PropertyValueError) Parts() (what, why, instead core.Said) { + return e.what(), e.Reason, e.Remedy +} + +// Said is the refusal, for a window that says it in its own language. The +// setting is the slot, so the window names it by the label above its box. +func (e *PropertyValueError) Said() core.Said { return e.said(e.Reason) } + +func (e *PropertyValueError) said(why any) core.Said { + return core.Says("format.ValueRefused", "%s: {setting} cannot be %q - %s", + core.A("Format", e.Format), core.A("Value", e.Value), core.A("Why", why)) +} + +// Bare is the refusal without the format in front of it, for a reader that +// names the format another way - the text the command line prints after +// "format: ". +func (e *PropertyValueError) Bare() core.Said { + return core.Says("format.ValueRefusedBare", "%s cannot be %q - %s", + core.A("Subject", e.SettingSaid()), core.A("Value", e.Value), core.A("Why", e.Reason)) +} + +// SettingSaid is the setting this refusal is about, named for a window that +// has no box to put it under. +func (e *PropertyValueError) SettingSaid() core.Said { + if !e.Subject.IsZero() { + return e.Subject + } + return core.Says("format.Setting", "%s", core.A("Name", core.LabelTerm(e.Key))) +} func (e *PropertyValueError) Error() string { return e.InTheWordsOf(e.Key) @@ -198,8 +263,7 @@ func (e *PropertyValueError) InTheWordsOf(name string) string { if name == "" { name = e.Key } - return fmt.Sprintf("%s: %s cannot be %q - %s", - e.Format, name, e.Value, core.InTheWordsOf(e.Reason, name)) + return e.said(core.InTheWordsOf(e.Reason.String(), name)).InTheWordsOf(name) } // AboutSetting is the property this refusal is about, so a window can put the diff --git a/internal/format/stated.go b/internal/format/stated.go index 0874cbdb..50ff9edf 100644 --- a/internal/format/stated.go +++ b/internal/format/stated.go @@ -46,7 +46,7 @@ func statedProblem(owner string, known map[string]Property, names []string, key, if value == "" { return nil } - if why := p.Allows(value); why != "" { + if why := p.Allows(value); !why.IsZero() { return &PropertyValueError{Format: owner, Key: key, Value: value, Reason: why, Remedy: p.Instead()} } return nil diff --git a/internal/format/svgfile/svg.go b/internal/format/svgfile/svg.go index ff6882f0..819d765a 100644 --- a/internal/format/svgfile/svg.go +++ b/internal/format/svgfile/svg.go @@ -261,8 +261,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "SVG", Requested: r.Bytes, Minimum: min, - Reason: "a drawing holds a declaration, a root element and whole shapes, and one of each needs that much", - Hint: fmt.Sprintf("Ask for %d B or more.", min), + Reason: core.Says("format.ADrawingHoldsADeclarationA", "a drawing holds a declaration, a root element and whole shapes, and one of each needs that much"), + Hint: core.Says("format.AskForBOrMore", "Ask for %d B or more.", core.A("Min", min)), } } @@ -287,19 +287,15 @@ func (generator) Plan(r format.Request) (format.Plan, error) { // separate sentence from the one below. Both leave the file named // by its own name and by the manifest. p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The drawing is %d px tall and the label needs the %d px strip along the bottom, so this file carries no visible label. Its name and the manifest still identify it.", - h, TextBand), + Code: "label_omitted", + Detail: core.Says("format.TheDrawingIsPxTallAnd", "The drawing is %d px tall and the label needs the %d px strip along the bottom, so this file carries no visible label. Its name and the manifest still identify it.", core.A("H", h), core.A("TextBand", TextBand)), }) case int64(len(line))+min <= r.Bytes: m.labelLine = line default: p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The label needs %d B and this file has no room for it beside a whole shape. Its name and the manifest still identify it.", - len(line)), + Code: "label_omitted", + Detail: core.Says("format.TheLabelNeedsBAndThis2", "The label needs %d B and this file has no room for it beside a whole shape. Its name and the manifest still identify it.", core.A("Length", len(line))), }) } } @@ -312,7 +308,7 @@ func (generator) Plan(r format.Request) (format.Plan, error) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("svg: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("svg: the plan was not produced by this generator")) } head := declaration + rootOpen(m.width, m.height) + m.labelLine diff --git a/internal/format/targz/compress.go b/internal/format/targz/compress.go index 430ec6a5..5c743543 100644 --- a/internal/format/targz/compress.go +++ b/internal/format/targz/compress.go @@ -2,11 +2,12 @@ package targz import ( "context" - "fmt" "io" "github.com/donislawdev/TestingFilesGenerator/internal/format" "github.com/donislawdev/TestingFilesGenerator/internal/format/archive" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // Settling the padding of a COMPRESSED archive, which cannot be done by @@ -91,9 +92,8 @@ func settleCompressed(ctx context.Context, m memo) (memo, error) { // depth to worry about. func settleRound(ctx context.Context, bare memo, target, filler int64, left int) (memo, error) { if left == 0 { - return bare, fmt.Errorf( - "targz: the padding of this compressed archive does not settle after %d rounds. "+ - "Ask for a different size, or for compression: none", solveRounds) + return bare, core.Refuse(core.Says("format.TargzThePaddingOfThisCompressed", "targz: the padding of this compressed archive does not settle after %d rounds. "+ + "Ask for a different size, or for compression: none", core.A("SolveRounds", solveRounds))) } if err := ctx.Err(); err != nil { return bare, err @@ -191,8 +191,8 @@ func belowMinimum(target, floor int64) error { Format: "TAR.GZ", Requested: target, Minimum: floor, - Reason: "that is what the contents come to once they are compressed, so nothing can be taken away", - Hint: fmt.Sprintf("Ask for %d B or more, or hold fewer or smaller files.", floor), + Reason: core.Says("format.ThatIsWhatTheContentsCome", "that is what the contents come to once they are compressed, so nothing can be taken away"), + Hint: core.Says("format.AskForBOrMoreOr11", "Ask for %d B or more, or hold fewer or smaller files.", core.A("Floor", floor)), } } diff --git a/internal/format/targz/framing.go b/internal/format/targz/framing.go index 80561f13..ae850cf4 100644 --- a/internal/format/targz/framing.go +++ b/internal/format/targz/framing.go @@ -39,6 +39,8 @@ import ( "compress/gzip" "fmt" "io" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // framing is what a level zero gzip stream costs beyond its content. @@ -97,17 +99,17 @@ func measureFraming() (framing, error) { return framing{}, err } if want := f.base + 3*f.perBlock; three != want { - return framing{}, fmt.Errorf( + return framing{}, core.Defect(fmt.Errorf( "targz: this build of Go frames a gzip stream in a shape this tool does not understand. "+ "Three blocks of content cost %d B where the two measured before them predict %d B, "+ "so the size of an archive cannot be worked out without building it", - three, want) + three, want)) } if empty != f.base { - return framing{}, fmt.Errorf( + return framing{}, core.Defect(fmt.Errorf( "targz: this build of Go frames an empty gzip stream at %d B where the measurement says %d B, "+ "so the size of an archive cannot be worked out without building it", - empty, f.base) + empty, f.base)) } return f, nil } diff --git a/internal/format/targz/size.go b/internal/format/targz/size.go index b07ff10f..472a88fc 100644 --- a/internal/format/targz/size.go +++ b/internal/format/targz/size.go @@ -185,8 +185,8 @@ func pad(m *memo, p *format.Plan, target int64, label string, groups []format.Co Format: "TAR.GZ", Requested: target, Minimum: bare, - Reason: fmt.Sprintf("an archive holding %s already needs that much, and nothing is compressed", describeGroups(groups)), - Hint: fmt.Sprintf("Ask for %d B or more, or hold fewer or smaller files.", bare), + Reason: core.Says("format.AnArchiveHoldingAlreadyNeedsThat", "an archive holding %s already needs that much, and nothing is compressed", core.A("Groups", describeGroups(groups))), + Hint: core.Says("format.AskForBOrMoreOr6", "Ask for %d B or more, or hold fewer or smaller files.", core.A("Bare", bare)), } } @@ -203,9 +203,9 @@ func pad(m *memo, p *format.Plan, target int64, label string, groups []format.Co Format: "TAR.GZ", Requested: target, Minimum: bare + 2, - Reason: "the padding that would make up the difference cannot be one byte long, " + - "so this size sits in a gap just above the smallest archive", - Hint: fmt.Sprintf("Ask for %d B or more, or keep the label on and any size from %d B works.", bare+2, bare), + Reason: core.Says("format.ThePaddingThatWouldMakeUp", "the padding that would make up the difference cannot be one byte long, "+ + "so this size sits in a gap just above the smallest archive"), + Hint: core.Says("format.AskForBOrMoreOr7", "Ask for %d B or more, or keep the label on and any size from %d B works.", core.A("Bare", bare+2), core.A("Bare2", bare)), } } m.comment, m.withExtra, m.extraLen = comment, withExtra, extra @@ -217,7 +217,7 @@ func pad(m *memo, p *format.Plan, target int64, label string, groups []format.Co // exact size by itself - the comment closes the last few bytes. size, header, ok := solveFiller(tarLength(*m), target, label) if !ok { - return fmt.Errorf("targz: no arrangement of padding reaches exactly %d B", target) + return core.Defect(fmt.Errorf("targz: no arrangement of padding reaches exactly %d B", target)) } m.withFiller = true m.fillerSize = size @@ -285,7 +285,7 @@ func solveFiller(base, target int64, label string) (size int64, header headerPad func build(ctx context.Context, w io.Writer, m memo) error { zw, err := gzip.NewWriterLevel(w, m.squeeze.Level) if err != nil { - return fmt.Errorf("targz: the archive could not be started: %w", err) + return core.Defect(fmt.Errorf("targz: the archive could not be started: %w", err)) } zw.Comment = m.comment // The extra field is where padding rides. It precedes everything, like @@ -306,7 +306,7 @@ func build(ctx context.Context, w io.Writer, m memo) error { return err } if err := writeDirectory(tw, dir, m.own); err != nil { - return fmt.Errorf("targz: the directory %q could not be named: %w", dir, err) + return core.Defect(fmt.Errorf("targz: the directory %q could not be named: %w", dir, err)) } } @@ -318,7 +318,7 @@ func build(ctx context.Context, w io.Writer, m memo) error { func(dst io.Writer) error { return c.desc.Generator.Write(ctx, dst, c.plan) }); err != nil { - return fmt.Errorf("targz: the %s file inside could not be written: %w", c.desc.ID, err) + return core.Defect(fmt.Errorf("targz: the %s file inside could not be written: %w", c.desc.ID, err)) } } diff --git a/internal/format/targz/targz.go b/internal/format/targz/targz.go index 624823b0..4db1c0dd 100644 --- a/internal/format/targz/targz.go +++ b/internal/format/targz/targz.go @@ -134,14 +134,14 @@ func init() { Unsupported: []format.UnsupportedSetting{ { Name: archive.Password, - Why: "neither tar nor gzip has any encryption in it, so there is no field in either " + - "one to put a password in", - Instead: "Use zip for an archive with a password, or leave this one open.", + Why: core.Says("format.NeitherTarNorGzipHasAny", "neither tar nor gzip has any encryption in it, so there is no field in either "+ + "one to put a password in"), + Instead: core.Says("format.UseZipForAnArchiveWith", "Use zip for an archive with a password, or leave this one open."), }, { Name: archive.Encryption, - Why: "neither tar nor gzip has any encryption in it, so there is nothing to choose between", - Instead: "Use zip for an archive with a password, or leave this one open.", + Why: core.Says("format.NeitherTarNorGzipHasAny2", "neither tar nor gzip has any encryption in it, so there is nothing to choose between"), + Instead: core.Says("format.UseZipForAnArchiveWith", "Use zip for an archive with a password, or leave this one open."), }, }, Container: true, @@ -273,7 +273,7 @@ func planChildren(r format.Request, groups []format.Content, layout archive.Layo Label: r.Label, }) if err != nil { - return nil, fmt.Errorf("targz: the %s file inside cannot be made: %w", g.Format, err) + return nil, core.Defect(fmt.Errorf("targz: the %s file inside cannot be made: %w", g.Format, err)) } numbered[g.Format]++ out = append(out, child{ @@ -320,8 +320,8 @@ func settleSize(m *memo, r format.Request) (target int64, label string, err erro } size = measured } - return 0, "", fmt.Errorf( - "targz: the size of this archive and the size written in its label do not settle. Give an explicit size") + return 0, "", core.Defect(fmt.Errorf( + "targz: the size of this archive and the size written in its label do not settle. Give an explicit size")) } // describe builds the plan and the properties that reach the manifest. @@ -381,19 +381,19 @@ func contentSummary(groups []format.Content) []map[string]any { } // describeGroups is the same thing for a person reading an error. -func describeGroups(groups []format.Content) string { - parts := make([]string, 0, len(groups)) +func describeGroups(groups []format.Content) core.Said { + parts := make(core.Conjoined, 0, len(groups)) for _, g := range groups { - kind := strings.ToUpper(g.Format) - parts = append(parts, fmt.Sprintf("%s of %d B", core.Count(g.Count, kind+" file", kind+" files"), g.Bytes)) + parts = append(parts, core.SaysN("format.GroupOf", "%d %s file of %d B", "%d %s files of %d B", + core.A("Count", g.Count), core.A("Kind", strings.ToUpper(g.Format)), core.A("Bytes", g.Bytes))) } - return strings.Join(parts, " and ") + return core.Says("format.Groups", "%s", core.A("Groups", parts)) } func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("targz: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("targz: the plan was not produced by this generator")) } if m.squeeze.On() { return writeCompressed(ctx, w, m) diff --git a/internal/format/textenc/textenc.go b/internal/format/textenc/textenc.go index 0c1339ba..39432d9e 100644 --- a/internal/format/textenc/textenc.go +++ b/internal/format/textenc/textenc.go @@ -29,6 +29,8 @@ import ( "unicode/utf8" "github.com/donislawdev/TestingFilesGenerator/internal/format" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // Setting names. Public names, so they are spelled once. @@ -90,7 +92,10 @@ func Parse(formatID string, props map[string]string) (Codec, error) { default: return Codec{}, &format.PropertyValueError{ Format: formatID, Key: Setting, Value: v, - Reason: "it has to be " + UTF8 + ", " + UTF16LE + " or " + UTF16BE, + Reason: core.Says("textenc.HasToBeEncoding", "it has to be %s, %s or %s", + core.A("UTF8", core.Choice{Of: Setting, Value: UTF8}), + core.A("UTF16LE", core.Choice{Of: Setting, Value: UTF16LE}), + core.A("UTF16BE", core.Choice{Of: Setting, Value: UTF16BE})), } } } @@ -104,7 +109,7 @@ func Parse(formatID string, props map[string]string) (Codec, error) { default: return Codec{}, &format.PropertyValueError{ Format: formatID, Key: SettingBOM, Value: v, - Reason: "it has to be true or false", + Reason: core.Says("format.ItHasToBeTrueOr", "it has to be true or false"), } } } @@ -166,18 +171,15 @@ func (c Codec) Check(formatName string, requested int64) error { if requested < mark { return &format.BelowMinimumError{ Format: formatName, Requested: requested, Minimum: mark, - Reason: fmt.Sprintf( - "a byte order mark is %d B and the file has to hold it", mark), - Hint: fmt.Sprintf("Ask for %d B or more, or turn the %s setting off.", mark, SettingBOM), + Reason: core.Says("format.AByteOrderMarkIsB", "a byte order mark is %d B and the file has to hold it", core.A("Mark", mark)), + Hint: core.Says("format.AskForBOrMoreOr8", "Ask for %d B or more, or turn the %s setting off.", core.A("Mark", mark), core.A("SettingBOM", SettingBOM)), } } if next, ok := c.fits(requested); !ok { return &format.BelowMinimumError{ Format: formatName, Requested: requested, Minimum: next, - Reason: fmt.Sprintf( - "%s stores two bytes for every character, so a whole file always has an even number of them", - c.name), - Hint: fmt.Sprintf("Ask for %d B or %d B.", requested-1, next), + Reason: core.Says("format.StoresTwoBytesForEveryCharacter", "%s stores two bytes for every character, so a whole file always has an even number of them", core.A("Name", c.name)), + Hint: core.Says("format.AskForBOrB", "Ask for %d B or %d B.", core.A("Requested", requested-1), core.A("Next", next)), } } return nil diff --git a/internal/format/tiff/tiff.go b/internal/format/tiff/tiff.go index 9cb596c1..640c6b0e 100644 --- a/internal/format/tiff/tiff.go +++ b/internal/format/tiff/tiff.go @@ -174,10 +174,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "TIFF", Requested: r.Bytes, Minimum: bare, - Reason: fmt.Sprintf( - "a %dx%d picture is %d B of pixels at three bytes each, and the header, the directory and its values take another %d B", - w, h, pixelBytes(w, h), overhead), - Hint: fmt.Sprintf("Ask for %d B or more, or set a smaller width and height", bare), + Reason: core.Says("format.AXPictureIsBOf2", "a %dx%d picture is %d B of pixels at three bytes each, and the header, the directory and its values take another %d B", core.A("W", w), core.A("H", h), core.A("PixelBytes", pixelBytes(w, h)), core.A("Overhead", overhead)), + Hint: core.Says("format.AskForBOrMoreOr2", "Ask for %d B or more, or set a smaller width and height", core.A("Bare", bare)), } } if r.Bytes > maxFileBytes { @@ -185,8 +183,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "TIFF", Requested: r.Bytes, Maximum: maxFileBytes, - Reason: "a TIFF locates its pixels with a four byte offset, so the format cannot describe a file this large", - Hint: "Ask for 4 GiB or less, or pick a format with no offset of its own such as gif.", + Reason: core.Says("format.ATIFFLocatesItsPixelsWith", "a TIFF locates its pixels with a four byte offset, so the format cannot describe a file this large"), + Hint: core.Says("format.AskFor4GiBOrLess3", "Ask for 4 GiB or less, or pick a format with no offset of its own such as gif."), } } @@ -209,10 +207,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { labelled := r.Label && imagelabel.Fits(w, len(label)) if r.Label && !labelled { p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", - w), + Code: "label_omitted", + Detail: core.Says("format.ThePictureIsPxWideAnd", "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", core.A("Width", w)), }) } p.Properties[format.PropertyLabelEmbedded] = labelled @@ -321,7 +317,7 @@ func isqrt(n uint64) uint64 { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("tiff: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("tiff: the plan was not produced by this generator")) } select { @@ -417,7 +413,7 @@ func writeDirectory(w io.Writer, m memo) error { } if len(buf) != directory+heap { - return fmt.Errorf("tiff: the directory came out %d B and the header says %d B", len(buf), directory+heap) + return core.Defect(fmt.Errorf("tiff: the directory came out %d B and the header says %d B", len(buf), directory+heap)) } _, err := w.Write(buf) return err diff --git a/internal/format/tomlfile/toml.go b/internal/format/tomlfile/toml.go index c2bf8171..b16f0ff2 100644 --- a/internal/format/tomlfile/toml.go +++ b/internal/format/tomlfile/toml.go @@ -106,15 +106,15 @@ func init() { Unsupported: []format.UnsupportedSetting{ { Name: textenc.Setting, - Why: "TOML is UTF-8 by its own specification, so there is no other " + - "encoding for a document to be in", - Instead: "Use yaml, xml, txt or md for a file in another encoding.", + Why: core.Says("format.TOMLIsUTF8ByIts", "TOML is UTF-8 by its own specification, so there is no other "+ + "encoding for a document to be in"), + Instead: core.Says("format.UseYamlXmlTxtOrMd", "Use yaml, xml, txt or md for a file in another encoding."), }, { Name: textenc.SettingBOM, - Why: "both readers on this machine refuse a TOML file that opens with a " + - "byte order mark, even in UTF-8", - Instead: "Use txt or md for a file that opens with a byte order mark.", + Why: core.Says("format.BothReadersOnThisMachineRefuse", "both readers on this machine refuse a TOML file that opens with a "+ + "byte order mark, even in UTF-8"), + Instead: core.Says("format.UseTxtOrMdForA", "Use txt or md for a file that opens with a byte order mark."), }, }, GeneratorVersion: generatorVersion, @@ -136,8 +136,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "TOML", Requested: r.Bytes, Minimum: min, - Reason: "a document holds whole records, and one table of them with every value type needs that much", - Hint: fmt.Sprintf("Ask for %d B or more.", min), + Reason: core.Says("format.ADocumentHoldsWholeRecordsAnd2", "a document holds whole records, and one table of them with every value type needs that much"), + Hint: core.Says("format.AskForBOrMore", "Ask for %d B or more.", core.A("Min", min)), } } @@ -165,10 +165,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { m.comment = line } else { p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The label comment needs %d B and this file has no room for it beside a whole record. Its name and the manifest still identify it.", - len(line)), + Code: "label_omitted", + Detail: core.Says("format.TheLabelCommentNeedsBAnd", "The label comment needs %d B and this file has no room for it beside a whole record. Its name and the manifest still identify it.", core.A("Length", len(line))), }) } } @@ -181,7 +179,7 @@ func (generator) Plan(r format.Request) (format.Plan, error) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("toml: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("toml: the plan was not produced by this generator")) } // The comment is the whole of the prologue, so with no label there is diff --git a/internal/format/txt/txt.go b/internal/format/txt/txt.go index 7b9f0916..f5bc661c 100644 --- a/internal/format/txt/txt.go +++ b/internal/format/txt/txt.go @@ -84,8 +84,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "TXT", Requested: r.Bytes, Minimum: 0, - Reason: "a file cannot hold fewer than zero bytes", - Hint: "Ask for 0 B or more.", + Reason: core.Says("format.AFileCannotHoldFewerThan", "a file cannot hold fewer than zero bytes"), + Hint: core.Says("format.AskFor0BOrMore", "Ask for 0 B or more."), } } @@ -123,10 +123,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { // long it is to read. In UTF-16 those differ by a factor of two, // and a note off by half is worse than no note. p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The label needs %d B and the file is %d B, so this file carries no label. Its name and the manifest still identify it.", - codec.Cost(int64(len(line))), r.Bytes), + Code: "label_omitted", + Detail: core.Says("format.TheLabelNeedsBAndThe", "The label needs %d B and the file is %d B, so this file carries no label. Its name and the manifest still identify it.", core.A("Length", codec.Cost(int64(len(line)))), core.A("Bytes", r.Bytes)), }) } } @@ -139,7 +137,7 @@ func (generator) Plan(r format.Request) (format.Plan, error) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("txt: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("txt: the plan was not produced by this generator")) } // The mark is bytes rather than text, so it goes out as itself. Everything @@ -195,7 +193,7 @@ func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { // hangs is worse than one that fails - nobody can tell it apart from // a very large file. Found by mutation testing, not by reasoning. if len(buf) == 0 { - return fmt.Errorf("txt: made no progress with %d B still owed", remaining) + return core.Defect(fmt.Errorf("txt: made no progress with %d B still owed", remaining)) } if err := writeAll(w, buf); err != nil { diff --git a/internal/format/wav/wav.go b/internal/format/wav/wav.go index 3d0f8e6d..5bc99c71 100644 --- a/internal/format/wav/wav.go +++ b/internal/format/wav/wav.go @@ -162,7 +162,7 @@ func readProperties(r format.Request) (memo, error) { switch m.bits { case 8, 16, 24, 32: default: - return memo{}, fmt.Errorf("wav: bit_depth must be 8, 16, 24 or 32, got %d", m.bits) + return memo{}, core.Defect(fmt.Errorf("wav: bit_depth must be 8, 16, 24 or 32, got %d", m.bits)) } m.content = "tone" @@ -171,7 +171,7 @@ func readProperties(r format.Request) (memo, error) { case "tone", "silence", "noise", "sweep": m.content = v default: - return memo{}, fmt.Errorf("wav: content must be tone, silence, noise or sweep, got %q", v) + return memo{}, core.Defect(fmt.Errorf("wav: content must be tone, silence, noise or sweep, got %q", v)) } } return m, nil @@ -208,10 +208,8 @@ func layout(m *memo, r format.Request, fixed int64) error { Format: "WAV", Requested: r.Bytes, Minimum: fixed + chunkHeader, - Reason: fmt.Sprintf( - "this size needs a padding chunk and the smallest one costs %d B, so nothing between %d and %d B can be reached", - chunkHeader, fixed, fixed+chunkHeader), - Hint: fmt.Sprintf("Ask for exactly %d B or for %d B or more.", fixed, fixed+chunkHeader), + Reason: core.Says("format.ThisSizeNeedsAPaddingChunk", "this size needs a padding chunk and the smallest one costs %d B, so nothing between %d and %d B can be reached", core.A("ChunkHeader", chunkHeader), core.A("Fixed", fixed), core.A("Fixed2", fixed+chunkHeader)), + Hint: core.Says("format.AskForExactlyBOrFor", "Ask for exactly %d B or for %d B or more.", core.A("Bare", fixed), core.A("Bare2", fixed+chunkHeader)), } } return nil @@ -242,8 +240,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "WAV", Requested: r.Bytes, Minimum: fixed, - Reason: "a WAV needs its RIFF header, its format chunk and a data chunk header before a single sample", - Hint: fmt.Sprintf("Ask for %d B or more%s.", fixed, labelHint(r.Label)), + Reason: core.Says("format.AWAVNeedsItsRIFFHeader", "a WAV needs its RIFF header, its format chunk and a data chunk header before a single sample"), + Hint: core.Says("format.AskForBOrMore2", "Ask for %d B or more%s.", core.A("Floor", fixed), core.A("CleanHint", labelHint(r.Label))), } } @@ -252,8 +250,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "WAV", Requested: r.Bytes, Maximum: maxFileBytes, - Reason: "a RIFF file states its own length in a four byte field, so the format cannot describe a file this large", - Hint: "Ask for 4 GiB or less, or pick a format with no length field of its own such as txt.", + Reason: core.Says("format.ARIFFFileStatesItsOwn", "a RIFF file states its own length in a four byte field, so the format cannot describe a file this large"), + Hint: core.Says("format.AskFor4GiBOrLess4", "Ask for 4 GiB or less, or pick a format with no length field of its own such as txt."), } } @@ -280,10 +278,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { } if m.frames == 0 { p.Notes = append(p.Notes, format.Note{ - Code: "no_audio_frames", - Detail: fmt.Sprintf( - "At %d B this file has room for the headers but not for a single audio frame, so it holds no sound. It is still a valid WAV.", - r.Bytes), + Code: "no_audio_frames", + Detail: core.Says("format.AtBThisFileHasRoom", "At %d B this file has room for the headers but not for a single audio frame, so it holds no sound. It is still a valid WAV.", core.A("Bytes", r.Bytes)), }) } return p, nil @@ -292,7 +288,7 @@ func (generator) Plan(r format.Request) (format.Plan, error) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("wav: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("wav: the plan was not produced by this generator")) } total := p.Bytes @@ -399,7 +395,7 @@ func (m memo) writeSamples(ctx context.Context, w io.Writer) error { } } if written != m.dataLen { - return fmt.Errorf("wav: wrote %d B of audio where the plan said %d B", written, m.dataLen) + return core.Defect(fmt.Errorf("wav: wrote %d B of audio where the plan said %d B", written, m.dataLen)) } return nil } @@ -501,17 +497,17 @@ func intProperty(props map[string]string, key string, fallback, min, max int) (i } n, err := strconv.Atoi(raw) if err != nil { - return 0, fmt.Errorf("wav: %s must be a whole number, got %q", key, raw) + return 0, core.Defect(fmt.Errorf("wav: %s must be a whole number, got %q", key, raw)) } if n < min || n > max { - return 0, fmt.Errorf("wav: %s must be between %d and %d, got %d", key, min, max, n) + return 0, core.Defect(fmt.Errorf("wav: %s must be between %d and %d, got %d", key, min, max, n)) } return n, nil } -func labelHint(label bool) string { +func labelHint(label bool) core.Said { if label { - return ", or drop the label" + return core.Says("wav.DropTheLabel", ", or drop the label") } - return "" + return core.Said{} } diff --git a/internal/format/webp/webp.go b/internal/format/webp/webp.go index dacf6821..5a05e9c4 100644 --- a/internal/format/webp/webp.go +++ b/internal/format/webp/webp.go @@ -150,10 +150,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "WEBP", Requested: r.Bytes, Minimum: bare, - Reason: fmt.Sprintf( - "a %dx%d picture is %d B of pixels at three bytes each, and the container and the coding tables take another %d B", - w, h, int64(w)*int64(h)*samplesPerPixel, bare-int64(w)*int64(h)*samplesPerPixel), - Hint: fmt.Sprintf("Ask for %d B or more, or set a smaller width and height", bare), + Reason: core.Says("format.AXPictureIsBOf3", "a %dx%d picture is %d B of pixels at three bytes each, and the container and the coding tables take another %d B", core.A("W", w), core.A("H", h), core.A("W2", int64(w)*int64(h)*samplesPerPixel), core.A("Bare", bare-int64(w)*int64(h)*samplesPerPixel)), + Hint: core.Says("format.AskForBOrMoreOr2", "Ask for %d B or more, or set a smaller width and height", core.A("Bare", bare)), } } if r.Bytes > maxFileBytes { @@ -161,8 +159,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "WEBP", Requested: r.Bytes, Maximum: maxFileBytes, - Reason: "a WebP declares its length in a four byte field, so the format cannot describe a file this large", - Hint: "Ask for 4 GiB or less, or pick a format with no length field of its own such as gif.", + Reason: core.Says("format.AWebPDeclaresItsLengthIn", "a WebP declares its length in a four byte field, so the format cannot describe a file this large"), + Hint: core.Says("format.AskFor4GiBOrLess5", "Ask for 4 GiB or less, or pick a format with no length field of its own such as gif."), } } @@ -186,10 +184,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { labelled := r.Label && imagelabel.Fits(w, len(label)) if r.Label && !labelled { p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", - w), + Code: "label_omitted", + Detail: core.Says("format.ThePictureIsPxWideAnd", "The picture is %d px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it.", core.A("Width", w)), }) } p.Properties[format.PropertyLabelEmbedded] = labelled @@ -315,7 +311,7 @@ func isqrt(n uint64) uint64 { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("webp: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("webp: the plan was not produced by this generator")) } select { @@ -344,7 +340,7 @@ func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { // leave a file every reader mistrusts, so it is an error rather than a // silent short write. if written != stream { - return fmt.Errorf("webp: the bitstream came to %d B where the header promised %d B", written, stream) + return core.Defect(fmt.Errorf("webp: the bitstream came to %d B where the header promised %d B", written, stream)) } if stream%2 == 1 { if err := writeAll(w, []byte{0}); err != nil { diff --git a/internal/format/xlsx/xlsx.go b/internal/format/xlsx/xlsx.go index f02b7b65..e2288f06 100644 --- a/internal/format/xlsx/xlsx.go +++ b/internal/format/xlsx/xlsx.go @@ -162,11 +162,11 @@ func checkJointLimits(rows, columns int) error { return err } for _, j := range d.JointLimits { - if bad := j.Allows(int64(rows), int64(columns)); bad != "" { + if bad := j.Allows("xlsx", int64(rows), int64(columns)); !bad.IsZero() { return &format.PropertyValueError{ Format: "xlsx", Key: j.Of + " and " + j.By, Value: fmt.Sprintf("%dx%d", rows, columns), - Reason: bad + ". Ask for fewer rows or fewer columns", + Reason: core.Says("xlsx.JointFewer", "%s. Ask for fewer rows or fewer columns", core.A("Why", bad)), } } } @@ -182,9 +182,9 @@ func refusal(err error, want int64, shape opc.Shape, rows, columns int) error { Format: "XLSX", Requested: big.Want, Maximum: big.Ceiling, - Reason: "an Office file is a ZIP archive, and this build works out its size before it writes it " + - "in a way that cannot account for the zip64 records a larger one needs", - Hint: "Ask for less than 4 GiB, or split the content across several files.", + Reason: core.Says("format.AnOfficeFileIsAZIP", "an Office file is a ZIP archive, and this build works out its size before it writes it "+ + "in a way that cannot account for the zip64 records a larger one needs"), + Hint: core.Says("format.AskForLessThan4GiB", "Ask for less than 4 GiB, or split the content across several files."), } } var gap *opc.Unreachable @@ -195,27 +195,23 @@ func refusal(err error, want int64, shape opc.Shape, rows, columns int) error { Format: "XLSX", Requested: want, Minimum: gap.Above, - Reason: fmt.Sprintf( - "the archive comment carries at most %d B and the smallest extra part costs %d B, so nothing between those two is reachable", - opc.CommentLimit, shape.FillerOverhead), - Hint: fmt.Sprintf("Ask for %d B or less, or %d B or more.", gap.Below, gap.Above), + Reason: core.Says("format.TheArchiveCommentCarriesAtMost", "the archive comment carries at most %d B and the smallest extra part costs %d B, so nothing between those two is reachable", core.A("CommentLimit", opc.CommentLimit), core.A("FillerOverhead", shape.FillerOverhead)), + Hint: core.Says("format.AskForBOrLessOr", "Ask for %d B or less, or %d B or more.", core.A("Below", gap.Below), core.A("Above", gap.Above)), } } return &format.BelowMinimumError{ Format: "XLSX", Requested: want, Minimum: shape.Bare, - Reason: fmt.Sprintf( - "a sheet of %s by %s already packages to that much, and a workbook cannot leave out its content types or its relationships", - core.Count(rows, "row", "rows"), core.Count(columns, "column", "columns")), - Hint: fmt.Sprintf("Ask for %d B or more, or set fewer rows", shape.Bare), + Reason: core.Says("format.ASheetOfByAlreadyPackages", "a sheet of %s by %s already packages to that much, and a workbook cannot leave out its content types or its relationships", core.A("Rows", core.SaysN("xlsx.Rows", "%d row", "%d rows", core.A("Count", rows))), core.A("Columns", core.SaysN("xlsx.Columns", "%d column", "%d columns", core.A("Count", columns)))), + Hint: core.Says("format.AskForBOrMoreOr9", "Ask for %d B or more, or set fewer rows", core.A("Bare", shape.Bare)), } } func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("xlsx: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("xlsx: the plan was not produced by this generator")) } return opc.Write(ctx, w, m.pkg) } @@ -285,10 +281,10 @@ func intProperty(props map[string]string, key string, fallback, min, max int) (i } n, err := strconv.Atoi(raw) if err != nil { - return 0, fmt.Errorf("xlsx: %s must be a whole number, got %q", key, raw) + return 0, core.Defect(fmt.Errorf("xlsx: %s must be a whole number, got %q", key, raw)) } if n < min || n > max { - return 0, fmt.Errorf("xlsx: %s must be between %d and %d, got %d", key, min, max, n) + return 0, core.Defect(fmt.Errorf("xlsx: %s must be between %d and %d, got %d", key, min, max, n)) } return n, nil } diff --git a/internal/format/xmlfile/xml.go b/internal/format/xmlfile/xml.go index 9e32e82d..b20605ad 100644 --- a/internal/format/xmlfile/xml.go +++ b/internal/format/xmlfile/xml.go @@ -144,8 +144,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "XML", Requested: r.Bytes, Minimum: min, - Reason: "a document holds a declaration, a root element and whole records, and one of each needs that much", - Hint: fmt.Sprintf("Ask for %d B or more.", min), + Reason: core.Says("format.ADocumentHoldsADeclarationA", "a document holds a declaration, a root element and whole records, and one of each needs that much"), + Hint: core.Says("format.AskForBOrMore", "Ask for %d B or more.", core.A("Min", min)), } } // Half of all sizes are unreachable in UTF-16, and a refusal has to name @@ -181,10 +181,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { m.comment = line } else { p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The label comment needs %d B and this file has no room for it beside a whole record. Its name and the manifest still identify it.", - codec.Cost(int64(len(line)))), + Code: "label_omitted", + Detail: core.Says("format.TheLabelCommentNeedsBAnd", "The label comment needs %d B and this file has no room for it beside a whole record. Its name and the manifest still identify it.", core.A("Length", codec.Cost(int64(len(line))))), }) } } @@ -197,7 +195,7 @@ func (generator) Plan(r format.Request) (format.Plan, error) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("xml: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("xml: the plan was not produced by this generator")) } // The mark is bytes rather than text, so it goes out as itself. Everything @@ -359,7 +357,8 @@ func checkMark(c textenc.Codec) error { Format: "xml", Key: textenc.SettingBOM, Value: "false", - Reason: "XML in " + c.Name() + " has to open with a byte order mark, so this needs bom=true or encoding=" + textenc.UTF8, + Reason: core.Says("xml.NeedsBOM", "XML in %s has to open with a byte order mark, so this needs bom=true or encoding=%s", + core.A("Encoding", c.Name()), core.A("UTF8", textenc.UTF8)), } } diff --git a/internal/format/yamlfile/yaml.go b/internal/format/yamlfile/yaml.go index 08331f01..7d4d4752 100644 --- a/internal/format/yamlfile/yaml.go +++ b/internal/format/yamlfile/yaml.go @@ -154,8 +154,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { Format: "YAML", Requested: r.Bytes, Minimum: min, - Reason: "a document holds a root key and whole records, and one of each needs that much", - Hint: fmt.Sprintf("Ask for %d B or more.", min), + Reason: core.Says("format.ADocumentHoldsARootKey", "a document holds a root key and whole records, and one of each needs that much"), + Hint: core.Says("format.AskForBOrMore", "Ask for %d B or more.", core.A("Min", min)), } } @@ -185,10 +185,8 @@ func (generator) Plan(r format.Request) (format.Plan, error) { m.comment = line } else { p.Notes = append(p.Notes, format.Note{ - Code: "label_omitted", - Detail: fmt.Sprintf( - "The label comment needs %d B and this file has no room for it beside a whole record. Its name and the manifest still identify it.", - len(line)), + Code: "label_omitted", + Detail: core.Says("format.TheLabelCommentNeedsBAnd", "The label comment needs %d B and this file has no room for it beside a whole record. Its name and the manifest still identify it.", core.A("Length", len(line))), }) } } @@ -201,7 +199,7 @@ func (generator) Plan(r format.Request) (format.Plan, error) { func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("yaml: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("yaml: the plan was not produced by this generator")) } head := m.comment + rootOpen diff --git a/internal/format/zip/children.go b/internal/format/zip/children.go index 3e3d2bfc..d446f201 100644 --- a/internal/format/zip/children.go +++ b/internal/format/zip/children.go @@ -54,7 +54,7 @@ func planChildren(r format.Request, groups []format.Content, layout archive.Layo Label: r.Label, }) if err != nil { - return nil, fmt.Errorf("zip: the %s file inside cannot be made: %w", g.Format, err) + return nil, core.Refuse(core.Says("format.ZipTheFileInsideCannotBe", "zip: the %s file inside cannot be made: %w", core.A("Format", g.Format), core.A("Err", err))) } numbered[g.Format]++ out = append(out, child{ diff --git a/internal/format/zip/compress.go b/internal/format/zip/compress.go index dac48083..ead06580 100644 --- a/internal/format/zip/compress.go +++ b/internal/format/zip/compress.go @@ -2,10 +2,11 @@ package zip import ( stdzip "archive/zip" - "fmt" "io" "github.com/donislawdev/TestingFilesGenerator/internal/format" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // Everything about squeezing a zip, kept together and kept out of zip.go. @@ -41,9 +42,9 @@ func padCompressed(m *memo, p *format.Plan, r format.Request, groups []format.Co Format: "ZIP", Requested: r.Bytes, Minimum: withFiller, - Reason: fmt.Sprintf("an archive holding %s needs that much before anything is squeezed, "+ - "and how far it squeezes is not known until it has been", describeGroups(groups)), - Hint: fmt.Sprintf("Ask for %d B or more, or hold fewer or smaller files.", withFiller), + Reason: core.Says("format.AnArchiveHoldingNeedsThatMuch", "an archive holding %s needs that much before anything is squeezed, "+ + "and how far it squeezes is not known until it has been", core.A("Groups", describeGroups(groups))), + Hint: core.Says("format.AskForBOrMoreOr6", "Ask for %d B or more, or hold fewer or smaller files.", core.A("Bare", withFiller)), } } p.Properties["padding_entry"] = fillerName diff --git a/internal/format/zip/directories.go b/internal/format/zip/directories.go index 646e14a3..680df209 100644 --- a/internal/format/zip/directories.go +++ b/internal/format/zip/directories.go @@ -3,10 +3,11 @@ package zip import ( stdzip "archive/zip" "context" - "fmt" "io/fs" "github.com/donislawdev/TestingFilesGenerator/internal/format/archive" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // writeDirectories names the directories the archive was asked to list. @@ -50,7 +51,7 @@ func writeDirectory(zw *stdzip.Writer, name string) error { h := &stdzip.FileHeader{Name: name, Method: stdzip.Store} h.SetMode(fs.ModeDir | 0o755) if _, err := zw.CreateHeader(h); err != nil { - return fmt.Errorf("zip: the directory %q could not be named: %w", name, err) + return core.Refuse(core.Says("format.ZipTheDirectoryCouldNotBe", "zip: the directory %q could not be named: %w", core.A("Name", name), core.A("Err", err))) } return nil } diff --git a/internal/format/zip/filler.go b/internal/format/zip/filler.go index d65b072f..7e1bde98 100644 --- a/internal/format/zip/filler.go +++ b/internal/format/zip/filler.go @@ -3,10 +3,11 @@ package zip import ( stdzip "archive/zip" "context" - "fmt" "io" "github.com/donislawdev/TestingFilesGenerator/internal/format" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // writeFillerEntry puts the padding entry into the archive. @@ -105,9 +106,9 @@ func writeCompressedFiller(ctx context.Context, zw *stdzip.Writer, m memo, withC Format: "ZIP", Requested: m.target, Minimum: m.target - size, - Reason: "these contents come out of the compressor larger than they went in, " + - "so the archive cannot be made this small once they are squeezed", - Hint: fmt.Sprintf("Ask for %d B or more, or ask for compression: none.", m.target-size), + Reason: core.Says("format.TheseContentsComeOutOfThe", "these contents come out of the compressor larger than they went in, "+ + "so the archive cannot be made this small once they are squeezed"), + Hint: core.Says("format.AskForBOrMoreOr10", "Ask for %d B or more, or ask for compression: none.", core.A("Target", m.target-size)), } } if err := writeFiller(ctx, entry, m.seed, size); err != nil { diff --git a/internal/format/zip/zip.go b/internal/format/zip/zip.go index 1f43c771..73a4f687 100644 --- a/internal/format/zip/zip.go +++ b/internal/format/zip/zip.go @@ -223,9 +223,9 @@ func withinZip32(total int64) error { Format: "ZIP", Requested: total, Maximum: zip32Ceiling, - Reason: "an archive this large would need the zip64 records, and this build works out an " + - "archive's size before it writes it in a way that cannot account for them", - Hint: "Ask for less than 4 GiB, or split the contents across several archives.", + Reason: core.Says("format.AnArchiveThisLargeWouldNeed", "an archive this large would need the zip64 records, and this build works out an "+ + "archive's size before it writes it in a way that cannot account for them"), + Hint: core.Says("format.AskForLessThan4GiB2", "Ask for less than 4 GiB, or split the contents across several archives."), } } @@ -272,8 +272,8 @@ func settleSize(m *memo, r format.Request) (target, bare int64, label string, er size = measured } if !settled { - return 0, 0, "", fmt.Errorf( - "zip: the size of this archive and the size written in its label do not settle. Give an explicit size") + return 0, 0, "", core.Defect(fmt.Errorf( + "zip: the size of this archive and the size written in its label do not settle. Give an explicit size")) } label = m.comment bare = size @@ -335,8 +335,8 @@ func pad(m *memo, p *format.Plan, r format.Request, target, bare int64, label st Format: "ZIP", Requested: target, Minimum: bare, - Reason: fmt.Sprintf("an archive holding %s already needs that much", describeGroups(groups)), - Hint: fmt.Sprintf("Ask for %d B or more, or hold fewer or smaller files.", bare), + Reason: core.Says("format.AnArchiveHoldingAlreadyNeedsThat2", "an archive holding %s already needs that much", core.A("Groups", describeGroups(groups))), + Hint: core.Says("format.AskForBOrMoreOr6", "Ask for %d B or more, or hold fewer or smaller files.", core.A("Bare", bare)), } // Exactly the bare size, and nothing to add - but only when the entries // are stored. A squeezed archive comes out SHORTER than its stored @@ -383,8 +383,8 @@ func pad(m *memo, p *format.Plan, r format.Request, target, bare int64, label st Format: "ZIP", Requested: r.Bytes, Minimum: withFiller, - Reason: "the padding entry the archive needs at this size does not fit", - Hint: fmt.Sprintf("Ask for %d B or more.", withFiller), + Reason: core.Says("format.ThePaddingEntryTheArchiveNeeds", "the padding entry the archive needs at this size does not fit"), + Hint: core.Says("format.AskForBOrMore", "Ask for %d B or more.", core.A("Min", withFiller)), } } p.Properties["padding_entry"] = fillerName @@ -406,19 +406,19 @@ func contentSummary(groups []format.Content) []map[string]any { } // describeGroups is the same thing for a person reading an error. -func describeGroups(groups []format.Content) string { - parts := make([]string, 0, len(groups)) +func describeGroups(groups []format.Content) core.Said { + parts := make(core.Conjoined, 0, len(groups)) for _, g := range groups { - kind := strings.ToUpper(g.Format) - parts = append(parts, fmt.Sprintf("%s of %d B", core.Count(g.Count, kind+" file", kind+" files"), g.Bytes)) + parts = append(parts, core.SaysN("format.GroupOf", "%d %s file of %d B", "%d %s files of %d B", + core.A("Count", g.Count), core.A("Kind", strings.ToUpper(g.Format)), core.A("Bytes", g.Bytes))) } - return strings.Join(parts, " and ") + return core.Says("format.Groups", "%s", core.A("Groups", parts)) } func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { - return fmt.Errorf("zip: the plan was not produced by this generator") + return core.Defect(fmt.Errorf("zip: the plan was not produced by this generator")) } return build(ctx, w, m, true) } @@ -539,7 +539,7 @@ func plaintextCRC(ctx context.Context, m memo, withContents bool, write func(io. func build(ctx context.Context, w io.Writer, m memo, withContents bool) error { zw := stdzip.NewWriter(w) if err := zw.SetComment(m.comment); err != nil { - return fmt.Errorf("zip: the archive comment was refused: %w", err) + return core.Defect(fmt.Errorf("zip: the archive comment was refused: %w", err)) } // squeezed is how many bytes the entries actually came to once compressed. @@ -571,7 +571,7 @@ func build(ctx context.Context, w io.Writer, m memo, withContents bool) error { return c.desc.Generator.Write(ctx, w, c.plan) }) if err != nil { - return fmt.Errorf("zip: the %s file inside could not be checksummed: %w", c.desc.ID, err) + return core.Defect(fmt.Errorf("zip: the %s file inside could not be checksummed: %w", c.desc.ID, err)) } entry, shut, err := openEntry(zw, m, entryPlan{ name: c.name, plain: c.plan.Bytes, index: i, withContents: withContents, crc: crc, @@ -581,7 +581,7 @@ func build(ctx context.Context, w io.Writer, m memo, withContents bool) error { } if withContents { if err := c.desc.Generator.Write(ctx, entry, c.plan); err != nil { - return fmt.Errorf("zip: the %s file inside could not be written: %w", c.desc.ID, err) + return core.Defect(fmt.Errorf("zip: the %s file inside could not be written: %w", c.desc.ID, err)) } if err := shut(); err != nil { return err diff --git a/internal/guard/archivecompression_test.go b/internal/guard/archivecompression_test.go index 6d17cf3b..6644fda6 100644 --- a/internal/guard/archivecompression_test.go +++ b/internal/guard/archivecompression_test.go @@ -256,7 +256,7 @@ func assertNamesBothHalves(t *testing.T, id string, err error, halves ...string) id, err, err) return } - said := refusal.Reason + " " + refusal.Remedy + said := refusal.Reason.String() + " " + refusal.Remedy.String() for _, half := range halves { if !strings.Contains(said, half) { t.Errorf("%s: the refusal never names %q, so a reader cannot tell which half to change:\n %s", diff --git a/internal/guard/archivedepth_test.go b/internal/guard/archivedepth_test.go index 73e7aa19..1b15433b 100644 --- a/internal/guard/archivedepth_test.go +++ b/internal/guard/archivedepth_test.go @@ -127,7 +127,7 @@ func TestAskingForDirectoryEntriesInAFlatArchiveNamesBothSettings(t *testing.T) if !errors.As(err, &refusal) { t.Fatalf("the refusal is %T, so it does not carry the four things a refusal owes a reader", err) } - said := refusal.Reason + " " + refusal.Remedy + said := refusal.Reason.String() + " " + refusal.Remedy.String() for _, half := range []string{archive.Depth, archive.DirectoryEntries} { if !strings.Contains(said, half) { t.Errorf("the refusal never names %q, so a reader cannot tell which half to change:\n %s", diff --git a/internal/guard/archivewording_test.go b/internal/guard/archivewording_test.go index 8ecd2612..4b6f2d27 100644 --- a/internal/guard/archivewording_test.go +++ b/internal/guard/archivewording_test.go @@ -218,7 +218,7 @@ func refusedInTheRegistrysWords(t *testing.T, err error, key, bad string) { // that rather than letting it happen: a test whose evidence has evaporated // reports success in exactly the same words as a test that worked. want := p.Allows(bad) - if want == "" { + if want.IsZero() { t.Fatalf("%s=%q is a value the declaration now allows, so this test no longer has "+ "a refusal to read. Pick a value the declaration does not contain.", key, bad) } @@ -233,7 +233,7 @@ func refusedInTheRegistrysWords(t *testing.T, err error, key, bad string) { "so nothing downstream can say which field to point at: %v", key, bad, err, err) } - if value.Reason != want { + if value.Reason.String() != want.String() { t.Errorf("%s=%q is refused in words of its own rather than the declaration's.\n"+ " refusal says: %q\n"+ " declaration says: %q\n"+ diff --git a/internal/guard/catalogue_test.go b/internal/guard/catalogue_test.go index 01db378b..761bf61e 100644 --- a/internal/guard/catalogue_test.go +++ b/internal/guard/catalogue_test.go @@ -20,6 +20,7 @@ const localeDir = "../gui/text/locale" // entryInCatalogue is one message as a translation file carries it. type entryInCatalogue struct { Description string `json:"description"` + Hash string `json:"hash"` Other string `json:"other"` One string `json:"one"` Few string `json:"few"` @@ -160,6 +161,17 @@ func TestTheEnglishCatalogueSaysWhatTheCodeSays(t *testing.T) { t.Errorf("%s says %q in the singular in the code and %q in the English catalogue.\n"+ "Run: python tools/gen-locale.py", id, one, entry.One) } + // The hash every other language is held to, worked out the way + // tools/gen-locale.py works it out - the two have to agree, or every + // translation would read as stale or none would. + hashed := english + if one, plural := singular[id]; plural { + hashed = one + "\n" + english + } + if entry.Hash != englishHash(hashed) { + t.Errorf("%s carries hash %q in the English catalogue and its English hashes to %q.\n"+ + "Run: python tools/gen-locale.py", id, entry.Hash, englishHash(hashed)) + } } for id := range have { if _, still := code[id]; !still { @@ -199,6 +211,16 @@ func TestEveryTranslationObeysThePunctuationRule(t *testing.T) { t.Fatal("no file of registry words was found, so half of this guard asserts about nothing") } files = append(files, registry...) + // And the engine's sentences, since 2026-10-05 - a refusal is read in the + // window as surely as a label (docs/OKNO-PO-POLSKU-2026-10-05.md). + said, err := filepath.Glob(filepath.Join(localeDir, text.SaidFolder, "*.json")) + if err != nil { + t.Fatal(err) + } + if len(said) == 0 { + t.Fatal("no file of the engine's sentences was found, so a third of this guard asserts about nothing") + } + files = append(files, said...) if len(files) == 0 { t.Fatal("no translation file was found, so this guard is asserting about nothing") } diff --git a/internal/guard/containerentries_test.go b/internal/guard/containerentries_test.go index d44d74b7..7bf2692a 100644 --- a/internal/guard/containerentries_test.go +++ b/internal/guard/containerentries_test.go @@ -79,11 +79,11 @@ func TestBothWaysOfAskingForEntriesShareOneCeiling(t *testing.T) { // And they have to name the same ceiling, in the same words. Comparing // the reason rather than the whole sentence, because the key differs // on purpose - one says entries and the other says contains. - if propErr != nil && propErr.Reason != containsErr.Reason { + if propErr != nil && propErr.Reason.String() != containsErr.Reason.String() { t.Errorf("%s: the two doors give different reasons\n entries: %s\n contains: %s", d.ID, propErr.Reason, containsErr.Reason) } - if !strings.Contains(containsErr.Reason, itoa(ceiling)) { + if !strings.Contains(containsErr.Reason.String(), itoa(ceiling)) { t.Errorf("%s: the contains refusal does not name the ceiling %d: %q", d.ID, ceiling, containsErr.Reason) } diff --git a/internal/guard/embeddedassets_test.go b/internal/guard/embeddedassets_test.go index 9a156e4a..9be207ae 100644 --- a/internal/guard/embeddedassets_test.go +++ b/internal/guard/embeddedassets_test.go @@ -146,6 +146,17 @@ var ownWork = map[string]bool{ // the same sentences in Polish, translated for this project on 2026-09-29. "github.com/donislawdev/TestingFilesGenerator/internal/gui/text locale/registry/en.json": true, "github.com/donislawdev/TestingFilesGenerator/internal/gui/text locale/registry/pl.json": true, + // The engine's sentences, written out of its code for a translator, and + // the same sentences in Polish, translated for this project on 2026-10-05. + "github.com/donislawdev/TestingFilesGenerator/internal/gui/text locale/said/en.json": true, + "github.com/donislawdev/TestingFilesGenerator/internal/gui/text locale/said/pl.json": true, + // The words the toolkit asks for, given back in the window's language. A + // key is the id the toolkit's code passes to lang.L - an interface's name, + // the way a setting's key is - and nothing is taken from the toolkit's own + // translation files: the descriptions and the Polish were written for this + // project on 2026-10-05. + "github.com/donislawdev/TestingFilesGenerator/internal/gui/text locale/toolkit/en.json": true, + "github.com/donislawdev/TestingFilesGenerator/internal/gui/text locale/toolkit/pl.json": true, } // accountFor requires exactly one registry entry to claim a file. None means diff --git a/internal/guard/extends_test.go b/internal/guard/extends_test.go index 9fa92ab0..b3a72360 100644 --- a/internal/guard/extends_test.go +++ b/internal/guard/extends_test.go @@ -333,7 +333,7 @@ func TestEveryRefusalAboutThePresetSideOfARecipeNamesItsLine(t *testing.T) { t.Errorf("no problem is addressed to %q saying %q. The problems are:\n%v", c.at, c.says, err) } for _, p := range invalid.Problems { - if p.Why == "" || p.Fix == "" { + if p.Why.IsZero() || p.Fix.IsZero() { t.Errorf("the problem %q arrives without all four parts (D6): why=%q fix=%q", p.What, p.Why, p.Fix) } } diff --git a/internal/guard/jointlimit_test.go b/internal/guard/jointlimit_test.go index ac1c5817..f2ed8101 100644 --- a/internal/guard/jointlimit_test.go +++ b/internal/guard/jointlimit_test.go @@ -31,13 +31,13 @@ func TestNoJointLimitRefusalPrintsTheRequestAndTheLimitAsOneNumber(t *testing.T) // the limit is exactly where rounding used to hide the difference. for _, over := range []int64{1, l.Max / 2, l.Max * 9} { got := l.Max + over - bad := l.Allows(got, 1) - if bad == "" { + bad := l.Allows("", got, 1) + if bad.IsZero() { t.Errorf("%s: %d is past the limit of %d and the rule allowed it", l.Of+" times "+l.By, got, l.Max) continue } - counts := countsIn(bad) + counts := countsIn(bad.String()) if len(counts) < 2 { t.Errorf("%s: the refusal does not read as two counts: %q", l.Of+" times "+l.By, bad) diff --git a/internal/guard/jsonlayout_test.go b/internal/guard/jsonlayout_test.go index 9fa02dea..5ab9c252 100644 --- a/internal/guard/jsonlayout_test.go +++ b/internal/guard/jsonlayout_test.go @@ -156,7 +156,7 @@ func TestEachJSONLayoutAnswersForItsOwnMinimum(t *testing.T) { } // The refusal says which layout it is about, because the same size is // legal in another one and a person reading it needs to know that. - if !strings.Contains(below.Reason, layout) { + if !strings.Contains(below.Reason.String(), layout) { t.Errorf("%s: the refusal does not name the layout it is about: %q", layout, below.Reason) } } diff --git a/internal/guard/planmemory_test.go b/internal/guard/planmemory_test.go index 2834fce5..bc8ce87e 100644 --- a/internal/guard/planmemory_test.go +++ b/internal/guard/planmemory_test.go @@ -207,7 +207,7 @@ func TestThePlanCeilingIsTheOneCoreStates(t *testing.T) { } // And the sentence has to name it, or the refusal sends somebody looking // for a number that is written somewhere else. - if !strings.Contains(core.PlanTooLargeWhy, core.HumanBytes(core.MaxPlanBytes)) { + if !strings.Contains(core.PlanTooLargeWhy.String(), core.HumanBytes(core.MaxPlanBytes)) { t.Errorf("the refusal reason does not name the ceiling %s: %s", core.HumanBytes(core.MaxPlanBytes), core.PlanTooLargeWhy) } diff --git a/internal/guard/properties_test.go b/internal/guard/properties_test.go index 7c7a9b69..a9f2cad8 100644 --- a/internal/guard/properties_test.go +++ b/internal/guard/properties_test.go @@ -90,13 +90,13 @@ func TestTheRefusalComesFromTheDeclaration(t *testing.T) { // Both ends. Checking only the low one leaves half the declaration // unguarded, and a mutation disabling the upper bound stayed green until // this line existed. - if bad := pages.Allows("0"); bad == "" { + if bad := pages.Allows("0"); bad.IsZero() { t.Error("zero pages was allowed by a property declared to start at 1") } - if bad := pages.Allows(strconv.FormatInt(pages.Max+1, 10)); bad == "" { + if bad := pages.Allows(strconv.FormatInt(pages.Max+1, 10)); bad.IsZero() { t.Errorf("%d pages was allowed by a property declared to stop at %d", pages.Max+1, pages.Max) } - if bad := pages.Allows(pages.Default); bad != "" { + if bad := pages.Allows(pages.Default); !bad.IsZero() { t.Errorf("pdf advertises pages default %q and then refuses it: %s", pages.Default, bad) } } diff --git a/internal/guard/refusaladdress_test.go b/internal/guard/refusaladdress_test.go index 55c6181b..23d6936f 100644 --- a/internal/guard/refusaladdress_test.go +++ b/internal/guard/refusaladdress_test.go @@ -260,18 +260,18 @@ targets: continue } got = append(got, p.At) - assertAddressResolves(t, known, p.At, p.What) + assertAddressResolves(t, known, p.At, p.What.String()) // And it carries the parts a refusal in this tool is made of - // what happened, why, what to do instead (D6). Asked of every // case here rather than of one recipe, because a path that // leaves one of them empty prints a bare dash where the reason // should be, and that is exactly what the first version of the // property check did on 2026-08-25. - if p.Why == "" || p.Fix == "" { + if p.Why.IsZero() || p.Fix.IsZero() { t.Errorf("the refusal %q at %s has no %s.\n"+ "A refusal here says what happened, why, and what to do instead - a missing\n"+ "part prints as a bare dash rather than as nothing.", - p.What, p.At, missingPart(p.Why, p.Fix)) + p.What, p.At, missingPart(p.Why.String(), p.Fix.String())) } } @@ -415,7 +415,7 @@ targets: // the command line prints - so the two are only comparable once both are // in the same vocabulary. See core.SettingSlot. for _, p := range bad.Problems { - what := core.InTheWordsOf(p.What, core.LastSettingSegment(p.At)) + what := core.InTheWordsOf(p.What.String(), core.LastSettingSegment(p.At)) if !strings.Contains(err.Error(), what) { t.Errorf("the message of the whole refusal no longer mentions %q", what) } diff --git a/internal/guard/refusalwords_test.go b/internal/guard/refusalwords_test.go index a3b7bc68..d1d5fcf1 100644 --- a/internal/guard/refusalwords_test.go +++ b/internal/guard/refusalwords_test.go @@ -36,7 +36,7 @@ func TestASettingIsDescribedTheSameWayEmptyOrRefused(t *testing.T) { want = want[:cut] } got := p.Allows("this is not a size") - if got != "it takes "+want { + if got.String() != "it takes "+want { t.Errorf("%s.%s is described two ways.\n"+ " empty field: %q\n"+ " after a typo: %q\n"+ diff --git a/internal/guard/registrywords_test.go b/internal/guard/registrywords_test.go index 3c59d073..508400b6 100644 --- a/internal/guard/registrywords_test.go +++ b/internal/guard/registrywords_test.go @@ -194,6 +194,15 @@ func TestOneEnglishSentenceIsOneSentenceInEveryLanguage(t *testing.T) { } sort.Strings(keys) for _, key := range keys { + // The name of a list's value agrees with the setting it is a value + // of - orientacja mieszana, rozmiar mieszany, wersje mieszane - and + // its key already names that setting, so one setting has one name + // for each value wherever it is drawn. Two settings sharing an + // English word are two contexts, the way they would be for any + // language with grammatical gender. + if strings.HasPrefix(key, "Choice.") { + continue + } english, translated := words[key].English, entries[key]["other"] if was, seen := said[english]; seen && was != translated { t.Errorf("%s says %q two ways: %q under %s and %q under %s", @@ -453,7 +462,7 @@ func TestThePolishWindowSaysTheRegistriesInPolish(t *testing.T) { continue } about++ - if got, want := text.PresetNote(expanded.Preset.ID, n.About, n.Said), polish[text.NoteKey(expanded.Preset.ID, n.About)]["other"]; got != want { + if got, want := text.PresetNote(expanded.Preset.ID, n.About, n.Said.String()), polish[text.NoteKey(expanded.Preset.ID, n.About)]["other"]; got != want { t.Errorf("the note about %s reads %q in the Polish window, and %q was translated", n.About, got, want) } } diff --git a/internal/guard/said_test.go b/internal/guard/said_test.go new file mode 100644 index 00000000..56e93cbf --- /dev/null +++ b/internal/guard/said_test.go @@ -0,0 +1,548 @@ +package guard + +import ( + "bytes" + "encoding/json" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "testing" + "text/template" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" + "github.com/donislawdev/TestingFilesGenerator/internal/gui/text" +) + +// The engine's sentences in the window's language - docs/OKNO-PO-POLSKU- +// 2026-10-05.md, core/said.go for the sentence and text/said.go for the +// window. The English of every refusal and note lives at the call that says +// it, so these guards read it from there: a translator's copy that agrees with +// the code, every language complete and current, and every sentence written so +// that a translation can be made of it at all. + +// saidDir is where every language keeps the engine's sentences. +var saidDir = filepath.Join(localeDir, text.SaidFolder) + +// writeSaidCatalogue writes said/en.json out of the code. +const writeSaidCatalogue = "TFG_WRITE_SAID_CATALOGUE" + +// saidCall is one sentence as the code says it. +type saidCall struct { + id, one, other string + plural bool + names []string + pkg, at string +} + +// saidID is what an id looks like: the package that says it, then a name. +var saidID = regexp.MustCompile(`^[a-z]+\.[A-Z][A-Za-z0-9]*$`) + +// saidCalls is every core.Says and core.SaysN in the module, with what is +// wrong with how each one is written. +func saidCalls(t *testing.T) ([]saidCall, []string) { + t.Helper() + var calls []saidCall + var wrong []string + root := repoRoot(t) + for _, p := range packages(t) { + for _, path := range p.files { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, path, nil, 0) + if err != nil { + t.Fatalf("parsing %s: %v", path, err) + } + rel, _ := filepath.Rel(root, path) + ast.Inspect(file, func(n ast.Node) bool { + call, ok := n.(*ast.CallExpr) + if !ok { + return true + } + fn := saysCalled(call, file.Name.Name) + if fn == "" { + return true + } + at := filepath.ToSlash(rel) + ":" + strconv.Itoa(fset.Position(call.Pos()).Line) + c, problem := readSaid(call, fn, file.Name.Name) + if problem != "" { + wrong = append(wrong, at+": "+problem) + return true + } + c.pkg, c.at = p.rel, at + calls = append(calls, c) + return true + }) + } + } + return calls, wrong +} + +// saysCalled is "Says" or "SaysN" when the call is one, and empty otherwise. +func saysCalled(call *ast.CallExpr, inPackage string) string { + switch fn := call.Fun.(type) { + case *ast.SelectorExpr: + if x, ok := fn.X.(*ast.Ident); ok && x.Name == "core" && (fn.Sel.Name == "Says" || fn.Sel.Name == "SaysN") { + return fn.Sel.Name + } + case *ast.Ident: + if inPackage == "core" && (fn.Name == "Says" || fn.Name == "SaysN") { + return fn.Name + } + } + return "" +} + +// readSaid is one call read into its parts, or what stops it being read. +func readSaid(call *ast.CallExpr, fn, inPackage string) (saidCall, string) { + var c saidCall + fixed := 2 + if fn == "SaysN" { + fixed, c.plural = 3, true + } + if len(call.Args) < fixed || call.Ellipsis.IsValid() { + return c, "the id and the layout are not all written out at the call" + } + var ok bool + if c.id, ok = literalText(call.Args[0]); !ok { + return c, "the id is not a literal, so no translator can find it" + } + if c.other, ok = literalText(call.Args[fixed-1]); !ok { + return c, c.id + ": the layout is not a literal, so no translator can see it" + } + c.one = c.other + if c.plural { + if c.one, ok = literalText(call.Args[1]); !ok { + return c, c.id + ": the layout for one is not a literal" + } + } + for _, arg := range call.Args[fixed:] { + name, ok := argName(arg, inPackage) + if !ok { + return c, c.id + ": a value is not written as core.A with a literal name" + } + c.names = append(c.names, name) + } + return c, "" +} + +// argName is the name of one core.A at a call. +func argName(e ast.Expr, inPackage string) (string, bool) { + call, ok := e.(*ast.CallExpr) + if !ok || len(call.Args) != 2 { + return "", false + } + switch fn := call.Fun.(type) { + case *ast.SelectorExpr: + x, ok := fn.X.(*ast.Ident) + if !ok || x.Name != "core" || fn.Sel.Name != "A" { + return "", false + } + case *ast.Ident: + if inPackage != "core" || fn.Name != "A" { + return "", false + } + default: + return "", false + } + return literalText(call.Args[0]) +} + +// literalText is a string literal, or literals joined with +, as its text. +func literalText(e ast.Expr) (string, bool) { + switch x := e.(type) { + case *ast.BasicLit: + if x.Kind != token.STRING { + return "", false + } + s, err := strconv.Unquote(x.Value) + return s, err == nil + case *ast.BinaryExpr: + if x.Op != token.ADD { + return "", false + } + a, okA := literalText(x.X) + b, okB := literalText(x.Y) + return a + b, okA && okB + case *ast.ParenExpr: + return literalText(x.X) + } + return "", false +} + +// engineSentences is every sentence by id, failing on anything a translation +// could not be made of. +func engineSentences(t *testing.T) map[string]saidCall { + t.Helper() + calls, wrong := saidCalls(t) + for _, w := range wrong { + t.Error(w) + } + // The damage package alone says nine. Fewer means the walk found nothing, + // and every guard below would compare nothing with nothing. + if len(calls) < 9 { + t.Fatalf("the code says %d sentence(s) through core.Says, so the walk did not find them", len(calls)) + } + out := map[string]saidCall{} + for _, c := range calls { + if problem := laidOutForTranslation(c); problem != "" { + t.Errorf("%s: %s: %s", c.at, c.id, problem) + } + if seen, twice := out[c.id]; twice && (seen.one != c.one || seen.other != c.other || + strings.Join(seen.names, ",") != strings.Join(c.names, ",")) { + t.Errorf("%s and %s say %s two ways - one id is one sentence", seen.at, c.at, c.id) + } + out[c.id] = c + } + return out +} + +// laidOutForTranslation is what is wrong with one sentence, or nothing. +func laidOutForTranslation(c saidCall) string { + if !saidID.MatchString(c.id) { + return "the id is not package.Name" + } + seen := map[string]bool{} + for _, n := range c.names { + if seen[n] || n == core.SettingField || n == "" { + return "the value " + strconv.Quote(n) + " is named twice, empty, or by the name the setting slot keeps" + } + seen[n] = true + } + if c.plural && !seen[core.CountArg] { + return "a sentence with two forms has no value named Count to choose between them" + } + for _, layout := range []string{c.one, c.other} { + if strings.Contains(layout, "{{") { + return "the layout holds {{, which a translation would read as a field" + } + directives := core.DirectivesOf(layout) + // A count that only chooses the form - "holds" against "hold" - is the + // last value and has no place in the layout. + unsaid := c.plural && len(c.names) > 0 && c.names[len(c.names)-1] == core.CountArg && len(directives) == len(c.names)-1 + if len(directives) != len(c.names) && !unsaid { + return "the layout takes " + strconv.Itoa(len(directives)) + " value(s) and is given " + strconv.Itoa(len(c.names)) + } + wraps := 0 + for _, d := range directives { + if strings.ContainsAny(d, "*[") { + return "the layout takes a width or a position from a value: " + d + } + if strings.HasSuffix(d, "w") { + wraps++ + } + } + // A refusal unwraps to the one error it wraps, the way fmt.Errorf + // with one %w does. Two would be two refusals to a window that + // spreads an error carrying several, and the sentence round them + // would be lost (core.Refusal.Unwrap). + if wraps > 1 { + return "the layout wraps " + strconv.Itoa(wraps) + " errors with %w, and a sentence wraps one at most" + } + } + return "" +} + +// TestEverySentenceTheEngineSaysCanBeTranslated holds every core.Says to the +// shape a translation needs: a literal id and layout a guard can read, named +// values, as many values as the layout takes, and no width or position taken +// from a value - a translation fills named fields, and a value without one +// name and one place cannot be put into it. +func TestEverySentenceTheEngineSaysCanBeTranslated(t *testing.T) { + engineSentences(t) +} + +// saidReflection is said/en.json as the code writes it. +func saidReflection(t *testing.T) []byte { + t.Helper() + entries := map[string]map[string]string{} + for id, c := range engineSentences(t) { + other := core.NamedLayout(c.other, c.names) + entry := map[string]string{"description": saidWhere(c), "hash": saidHash(c), "other": other} + if c.plural { + entry["one"] = core.NamedLayout(c.one, c.names) + } + entries[id] = entry + } + var b bytes.Buffer + enc := json.NewEncoder(&b) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + if err := enc.Encode(entries); err != nil { + t.Fatal(err) + } + return b.Bytes() +} + +// saidHash is what another language records of the English it translated. +func saidHash(c saidCall) string { + if c.plural { + return englishHash(core.NamedLayout(c.one, c.names) + "\n" + core.NamedLayout(c.other, c.names)) + } + return englishHash(core.NamedLayout(c.other, c.names)) +} + +// saidWhere is the sentence a translator reads beside the English. +func saidWhere(c saidCall) string { + where := "Said by " + c.pkg + " - a refusal under the box it is about or at the foot of the form, or a note after a run." + if len(c.names) == 0 { + return where + } + fields := make([]string, len(c.names)) + for i, n := range c.names { + fields[i] = "{{." + n + "}}" + } + return where + " Carries " + strings.Join(fields, ", ") + + ", written the way the program writes them - keep each spelled exactly that way." +} + +// TestTheEnglishCopyOfTheEngineSentencesSaysWhatTheCodeSays keeps the copy a +// translator works from equal to the code, and is where the hashes every other +// language is held to come from. +func TestTheEnglishCopyOfTheEngineSentencesSaysWhatTheCodeSays(t *testing.T) { + want := saidReflection(t) + path := filepath.Join(saidDir, "en.json") + if os.Getenv(writeSaidCatalogue) == "1" { + if err := os.MkdirAll(saidDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, want, 0o644); err != nil { + t.Fatal(err) + } + t.Logf("wrote %s, %d B", path, len(want)) + return + } + have, err := os.ReadFile(path) + if err != nil { + t.Fatalf("there is no %s: %v\nRun: %s=1 go test ./internal/guard/ -run ^TestTheEnglishCopyOfTheEngineSentencesSaysWhatTheCodeSays$", + path, err, writeSaidCatalogue) + } + if !bytes.Equal(bytes.ReplaceAll(have, []byte("\r\n"), []byte("\n")), want) { + t.Errorf("%s is not what the code says - a sentence was added, changed or removed.\n"+ + "Run: %s=1 go test ./internal/guard/ -run ^TestTheEnglishCopyOfTheEngineSentencesSaysWhatTheCodeSays$\n"+ + "and then bring every other language in %s up to it.", path, writeSaidCatalogue, saidDir) + } +} + +// TestEveryLanguageSaysEverythingTheEngineSays holds every language the window +// carries to every sentence of the engine: there, translated from the English +// the code says now, and carrying exactly the values the English carries - a +// translation without {{.Fix}} is well formed and drops the part of a refusal +// that says what to do (D6). +func TestEveryLanguageSaysEverythingTheEngineSays(t *testing.T) { + sentences := engineSentences(t) + files := catalogueFilesIn(t, saidDir) + for tag := range catalogueFiles(t) { + if _, has := files[tag]; !has { + t.Errorf("the window speaks %s and %s has no %s.json, so the engine's refusals are English there", + tag, saidDir, tag) + } + } + if len(files) < 2 { + t.Fatal("no language but English has the engine's sentences, so this guard asserts about nothing") + } + for tag, entries := range files { + if tag == text.English { + continue + } + languageSaysTheEngine(t, tag, entries, sentences) + } +} + +// languageSaysTheEngine holds one language to every sentence. +func languageSaysTheEngine(t *testing.T, tag string, entries map[string]map[string]string, sentences map[string]saidCall) { + t.Helper() + ids := make([]string, 0, len(sentences)) + for id := range sentences { + ids = append(ids, id) + } + sort.Strings(ids) + for _, id := range ids { + c := sentences[id] + entry, has := entries[id] + switch { + case !has || entry["other"] == "": + t.Errorf("%s has no %s, so the window says it in English: %q", tag, id, core.NamedLayout(c.other, c.names)) + case entry["hash"] != saidHash(c): + t.Errorf("%s translates %s from an English the code no longer says. It says now:\n %q\n"+ + "Translate it again and record hash %s.", tag, id, core.NamedLayout(c.other, c.names), saidHash(c)) + default: + for form, said := range entry { + if form == "hash" || form == "description" { + continue + } + if problem := fieldsOf(said, c, form); problem != "" { + t.Errorf("%s %s (%s): %s\n %q", tag, id, form, problem, said) + } + } + } + } + for id := range entries { + if _, known := sentences[id]; !known { + t.Errorf("%s carries %s, which the code no longer says", tag, id) + } + } +} + +// fieldFound is one {{.Name}} in a translation. +var fieldFound = regexp.MustCompile(`{{\s*\.([A-Za-z0-9]+)\s*}}`) + +// fieldsOf is what is wrong with the fields one form of a translation uses. +func fieldsOf(said string, c saidCall, form string) string { + want := map[string]bool{} + for _, n := range c.names { + want[n] = true + } + if strings.Contains(c.other, core.SettingSlot) { + want[core.SettingField] = true + } + have := map[string]bool{} + for _, m := range fieldFound.FindAllStringSubmatch(said, -1) { + have[m[1]] = true + } + if strings.Contains(said, core.ArticleSlot) || strings.Contains(said, core.SettingSlot) { + return "carries a slot of the English layout, which a translation writes as {{.Setting}}" + } + for n := range have { + if !want[n] { + return "uses {{." + n + "}}, which the sentence does not carry" + } + } + for n := range want { + // The number may be left out of every form when the English does not + // print it, and of the form for one when it does - "one file". + countOptional := n == core.CountArg && (form != "other" || len(core.DirectivesOf(c.other)) < len(c.names)) + if !have[n] && !countOptional { + return "leaves out {{." + n + "}}, so part of what the English says is missing" + } + } + values := map[string]any{} + for n := range want { + values[n] = "x" + } + tmpl, err := template.New("").Parse(said) + if err != nil { + return "is not a template: " + err.Error() + } + var out strings.Builder + if err := tmpl.Execute(&out, values); err != nil || strings.Contains(out.String(), "") { + return "does not render with the values it is given" + } + return "" +} + +// said is a sentence a guard makes up, for a refusal built by hand. Its +// English is the text itself. +func said(text string) core.Said { return core.Says("guard.Text", "%s", core.A("Text", text)) } + +// enginePackages are the packages whose refusals and notes reach a person +// through both surfaces, and so are said as sentences. +var enginePackages = []string{"internal/core", "internal/engine", "internal/recipe", "internal/preset", + "internal/damage", "internal/format", "internal/tool", "internal/manifest"} + +// TestNoRefusalOfTheEngineIsWrittenAsBareText holds the engine to saying every +// error as a sentence: core.Says, or core.Defect around an error only a fault +// in the program can produce. An fmt.Errorf or an errors.New anywhere else is +// a refusal a window can only show in English. +// +// One exception, by its shape: the publishing calls wrap errors.ErrUnsupported +// so the caller can ask errors.Is and fall back. That error is a signal between +// two functions, and no person reads it. +func TestNoRefusalOfTheEngineIsWrittenAsBareText(t *testing.T) { + root := repoRoot(t) + found := 0 + for _, p := range packages(t) { + if !insideAny(p.rel, enginePackages) { + continue + } + for _, path := range p.files { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, path, nil, 0) + if err != nil { + t.Fatalf("parsing %s: %v", path, err) + } + found++ + for _, bare := range bareErrors(file) { + rel, _ := filepath.Rel(root, path) + if strings.HasPrefix(filepath.Base(path), "publish_") && wrapsUnsupported(bare) { + continue + } + t.Errorf("%s:%d builds an error from bare text. Say it with core.Says (core.Refuse, core.RefuseAbout), "+ + "or wrap it in core.Defect when only a fault in the program can produce it", + filepath.ToSlash(rel), fset.Position(bare.Pos()).Line) + } + } + } + if found < 50 { + t.Fatalf("read %d file(s) of the engine, so the walk did not find it", found) + } +} + +func insideAny(rel string, roots []string) bool { + rel = filepath.ToSlash(rel) + for _, r := range roots { + if rel == r || strings.HasPrefix(rel, r+"/") { + return true + } + } + return false +} + +// bareErrors is every fmt.Errorf and errors.New in a file that is not the +// argument of a Defect. +func bareErrors(file *ast.File) []*ast.CallExpr { + inDefect := map[*ast.CallExpr]bool{} + var all []*ast.CallExpr + ast.Inspect(file, func(n ast.Node) bool { + call, ok := n.(*ast.CallExpr) + if !ok { + return true + } + if calledAs(call, "core", "Defect") || (file.Name.Name == "core" && isIdentCall(call, "Defect")) { + for _, a := range call.Args { + if inner, ok := a.(*ast.CallExpr); ok { + inDefect[inner] = true + } + } + } + if calledAs(call, "fmt", "Errorf") || calledAs(call, "errors", "New") { + all = append(all, call) + } + return true + }) + var out []*ast.CallExpr + for _, c := range all { + if !inDefect[c] { + out = append(out, c) + } + } + return out +} + +func calledAs(call *ast.CallExpr, pkg, fn string) bool { + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok || sel.Sel.Name != fn { + return false + } + x, ok := sel.X.(*ast.Ident) + return ok && x.Name == pkg +} + +func isIdentCall(call *ast.CallExpr, fn string) bool { + id, ok := call.Fun.(*ast.Ident) + return ok && id.Name == fn +} + +// wrapsUnsupported is fmt.Errorf("%w: %w", errors.ErrUnsupported, ...). +func wrapsUnsupported(call *ast.CallExpr) bool { + if len(call.Args) < 2 { + return false + } + layout, ok := literalText(call.Args[0]) + sel, isSel := call.Args[1].(*ast.SelectorExpr) + return ok && layout == "%w: %w" && isSel && sel.Sel.Name == "ErrUnsupported" +} diff --git a/internal/guard/settingslot_test.go b/internal/guard/settingslot_test.go index 8861f06c..98f5dada 100644 --- a/internal/guard/settingslot_test.go +++ b/internal/guard/settingslot_test.go @@ -155,7 +155,7 @@ func TestNoRefusalEverShowsASlotToAPerson(t *testing.T) { // sentence rather than writing it out. value := &format.PropertyValueError{ Format: "png", Key: "width", Value: "99999", - Reason: "it takes a whole number of pixels from 1 to 20000", + Reason: said("it takes a whole number of pixels from 1 to 20000"), } leaks(t, "the command line", value.Error()) leaks(t, "a window", value.InTheWordsOf("Width")) diff --git a/internal/guard/tableheader_test.go b/internal/guard/tableheader_test.go index 9a6f1474..ceefc4c1 100644 --- a/internal/guard/tableheader_test.go +++ b/internal/guard/tableheader_test.go @@ -120,31 +120,32 @@ func TestEveryRowOfATableStandsUnderTheSameNames(t *testing.T) { // puts its own placeholder into an empty menu while the renderer is made, and // parts.menuWidth used to measure whatever placeholder was there. // -// The state is asserted rather than assumed - the placeholder has to have -// ARRIVED for the second measurement to be the warm one - because a guard -// that measures a menu twice before the toolkit touched it would agree with -// the old code. +// Since 2026-10-06 a menu is made with the window's own placeholder, in the +// window's language, so the toolkit has nothing to put in. That is asserted +// rather than assumed: a placeholder of the toolkit's arriving with the +// renderer would be the second width again, and an English word in a window +// speaking another language. The renderer is asked for outright, so the warm +// measurement is taken after the moment the toolkit would have stepped in. func TestAMenuIsOneWidthBeforeAndAfterItIsDrawn(t *testing.T) { ourTheme(t) menu := parts.NewChooser(format.IDs(), nil) - // Menu measures the menu as it builds the box, so the box built here is - // the cold measurement - and asking the box for its size is what makes the - // toolkit build the renderer and put the placeholder in. - sizedCold := parts.Menu(menu) - if menu.PlaceHolder != "" { - t.Fatalf("a menu built and not yet drawn already has the placeholder %q, so there is no cold"+ - " measurement to take", menu.PlaceHolder) + ours := text.PlaceholderChooseOne() + if menu.PlaceHolder != ours { + t.Fatalf("a menu is made with the placeholder %q rather than the window's own %q, so the toolkit"+ + " puts its English one in when the menu is first drawn", menu.PlaceHolder, ours) } - cold := sizedCold.MinSize().Width - if menu.PlaceHolder == "" { - t.Fatal("asking the box for its size did not put the toolkit's placeholder into the menu, so the" + - " warm measurement below is the cold one again and this guard would prove nothing") + cold := parts.Menu(menu).MinSize().Width + if test.WidgetRenderer(menu) == nil { + t.Fatal("the menu has no renderer, so the warm measurement below would be the cold one again" + + " and this guard would prove nothing") + } + if menu.PlaceHolder != ours { + t.Errorf("drawing the menu replaced the window's placeholder %q with %q", ours, menu.PlaceHolder) } warm := parts.Menu(menu).MinSize().Width if cold != warm { - t.Errorf("a menu of the format ids is %.2f px measured before it is drawn and %.2f px after"+ - " the toolkit put %q into it, so the same menu is two widths depending on when it was"+ - " measured", cold, warm, menu.PlaceHolder) + t.Errorf("a menu of the format ids is %.2f px measured before it is drawn and %.2f px after,"+ + " so the same menu is two widths depending on when it was measured", cold, warm) } } diff --git a/internal/guard/testdata/screens/catalogue.png b/internal/guard/testdata/screens/catalogue.png index 8f47ffe1..1652e175 100644 Binary files a/internal/guard/testdata/screens/catalogue.png and b/internal/guard/testdata/screens/catalogue.png differ diff --git a/internal/guard/testdata/screens/catalogue.xml b/internal/guard/testdata/screens/catalogue.xml index d92263e2..5b6f0b16 100644 --- a/internal/guard/testdata/screens/catalogue.xml +++ b/internal/guard/testdata/screens/catalogue.xml @@ -1,7 +1,7 @@ - + - - + + @@ -417,12 +417,12 @@ - - - - + + + + Chooser - + @@ -580,6 +580,32 @@ + + + + a value drawn under its name, longer than the value + + + + + + + + + + + every page lying on its side + + + + + + + + + + + @@ -610,7 +636,7 @@ - + @@ -724,7 +750,7 @@ - + @@ -833,7 +859,7 @@ - + @@ -973,12 +999,12 @@ - - - - + + + + OpenList - + @@ -1222,7 +1248,45 @@ - + + + + + every value drawn under its name, longer than the value + + + + + + + + + + + + + + + + every page standing upright + + + + + every page lying on its side + + + + turn by turn, starting upright + + + + + + + + + @@ -1256,7 +1320,7 @@ - + @@ -1484,7 +1548,7 @@ - + @@ -1655,7 +1719,7 @@ - + @@ -1703,7 +1767,7 @@ - + @@ -1897,7 +1961,7 @@ - + @@ -2339,7 +2403,7 @@ - + @@ -2619,7 +2683,7 @@ - + @@ -2749,7 +2813,7 @@ - + @@ -2863,7 +2927,7 @@ - + @@ -2944,7 +3008,7 @@ - + @@ -3004,7 +3068,7 @@ - + @@ -3053,7 +3117,7 @@ - + @@ -3474,7 +3538,7 @@ - + @@ -3814,7 +3878,7 @@ - + @@ -4325,7 +4389,7 @@ - + @@ -4395,7 +4459,7 @@ - + @@ -4979,7 +5043,7 @@ - + @@ -5563,7 +5627,7 @@ - + @@ -5676,7 +5740,7 @@ - + diff --git a/internal/guard/testdata/screens/preset-menu.png b/internal/guard/testdata/screens/preset-menu.png index e2be72e9..4b8af2bb 100644 Binary files a/internal/guard/testdata/screens/preset-menu.png and b/internal/guard/testdata/screens/preset-menu.png differ diff --git a/internal/guard/testdata/screens/preset-menu.xml b/internal/guard/testdata/screens/preset-menu.xml index d5a13d95..5dab3317 100644 --- a/internal/guard/testdata/screens/preset-menu.xml +++ b/internal/guard/testdata/screens/preset-menu.xml @@ -403,40 +403,46 @@ - - - - - - + + + + + + - - - - - - - empty-and-minimal + + + + + + + empty-and-minimal + Empty and minimal - - - filename-handling + + + filename-handling + File name handling - - - size-boundaries + + + size-boundaries + Size boundaries - - - tabular-import + + + tabular-import + Tabular import - - - text-encoding + + + text-encoding + Text encoding - - - upload-validation + + + upload-validation + Upload validation diff --git a/internal/guard/textencoding_test.go b/internal/guard/textencoding_test.go index ac3df9bf..4bad04ae 100644 --- a/internal/guard/textencoding_test.go +++ b/internal/guard/textencoding_test.go @@ -249,10 +249,10 @@ func TestAWideEncodingRefusesAnOddSizeAndNamesOneItCanWrite(t *testing.T) { id, c, size, below.Minimum, err) } // The four parts every refusal in this tool carries. - if !strings.Contains(below.Reason, "two bytes") { + if !strings.Contains(below.Reason.String(), "two bytes") { t.Errorf("%s %v: the reason does not say why an odd size cannot exist: %q", id, c, below.Reason) } - if !strings.Contains(below.Hint, fmt.Sprintf("%d B", size-1)) { + if !strings.Contains(below.Hint.String(), fmt.Sprintf("%d B", size-1)) { t.Errorf("%s %v: the hint does not offer the size below: %q", id, c, below.Hint) } } @@ -356,10 +356,10 @@ func TestALabelThatWillNotFitSaysWhatItWouldCost(t *testing.T) { if note == nil { t.Fatalf("%s: the label does not fit and nothing said so - silence is banned", id) } - if !strings.Contains(note.Detail, fmt.Sprintf("needs %d B", wide)) { + if !strings.Contains(note.Detail.String(), fmt.Sprintf("needs %d B", wide)) { t.Errorf("%s: the note should say the label needs %d B in this encoding: %q", id, wide, note.Detail) } - if strings.Contains(note.Detail, fmt.Sprintf("needs %d B", narrow)) { + if strings.Contains(note.Detail.String(), fmt.Sprintf("needs %d B", narrow)) { t.Errorf("%s: the note reports what the label costs in UTF-8, not in the encoding asked for: %q", id, note.Detail) } diff --git a/internal/guard/toolkitwords_test.go b/internal/guard/toolkitwords_test.go new file mode 100644 index 00000000..f1208d61 --- /dev/null +++ b/internal/guard/toolkitwords_test.go @@ -0,0 +1,144 @@ +package guard + +import ( + "encoding/json" + "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strings" + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/gui/text" +) + +// The graphics toolkit's own words - the menu of a box to type in, the dialogs +// choosing a folder or a file - in the language the window speaks rather than +// the system's (docs/OKNO-PO-POLSKU-2026-10-05.md section 3.7). The window +// hands the toolkit a file of words for every language it carries, so these +// guards hold the files to the toolkit this build links: a word the toolkit +// asks for that no file has is a word it says in its own language, which may +// be neither the window's nor English. + +// toolkitDir is where every language keeps the toolkit's words. +var toolkitDir = filepath.Join(localeDir, text.ToolkitFolder) + +// toolkitAsks is every word the pinned toolkit asks its catalogue for, read +// from its source - lang.L("...") and lang.X("...") with a literal - and from +// its own English file, with the English it says. +func toolkitAsks(t *testing.T) map[string]string { + t.Helper() + out, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", "fyne.io/fyne/v2").Output() + if err != nil { + t.Fatalf("finding the toolkit's source: %v", err) + } + dir := strings.TrimSpace(string(out)) + raw, err := os.ReadFile(filepath.Join(dir, "lang", "translations", "base.en.json")) + if err != nil { + t.Fatalf("reading the toolkit's English: %v", err) + } + var base map[string]json.RawMessage + if err := json.Unmarshal(raw, &base); err != nil { + t.Fatal(err) + } + asks := map[string]string{} + for key, value := range base { + asks[key] = englishOf(value) + } + literal := regexp.MustCompile(`lang\.[LX]\("((?:[^"\\]|\\.)*)"`) + for _, sub := range []string{"widget", "dialog", "internal", "app", "container"} { + _ = filepath.WalkDir(filepath.Join(dir, sub), func(path string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { + return nil + } + src, err := os.ReadFile(path) + if err != nil { + return nil + } + for _, m := range literal.FindAllStringSubmatch(string(src), -1) { + if _, known := asks[m[1]]; !known { + asks[m[1]] = m[1] + } + } + return nil + }) + } + if len(asks) < 20 { + t.Fatalf("found %d word(s) the toolkit asks for, so the walk of its source found nothing", len(asks)) + } + return asks +} + +// englishOf is a word of the toolkit's English file, a string or {"other": ...}. +func englishOf(raw json.RawMessage) string { + var plain string + if json.Unmarshal(raw, &plain) == nil { + return plain + } + var forms map[string]string + _ = json.Unmarshal(raw, &forms) + return forms["other"] +} + +// TestTheToolkitIsGivenEveryWordItAsksFor holds the window's English file of +// the toolkit's words to the toolkit: every word it asks for is there, in the +// toolkit's own English. A new word arriving with a new version of the +// toolkit turns this red rather than reaching a window in the system's +// language. +func TestTheToolkitIsGivenEveryWordItAsksFor(t *testing.T) { + asks := toolkitAsks(t) + files := catalogueFilesIn(t, toolkitDir) + english, ok := files[text.English] + if !ok { + t.Fatalf("%s has no en.json", toolkitDir) + } + keys := make([]string, 0, len(asks)) + for key := range asks { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + entry, has := english[key] + switch { + case !has: + t.Errorf("the toolkit asks for %q and the window does not give it, so it says it in the language of the system", key) + case entry["other"] != asks[key]: + t.Errorf("the toolkit says %q as %q and the window's English copy says %q", key, asks[key], entry["other"]) + case entry["hash"] != englishHash(entry["other"]): + t.Errorf("the English copy of %q carries hash %q, and its English hashes to %q", key, entry["hash"], englishHash(entry["other"])) + } + } +} + +// TestEveryLanguageGivesTheToolkitEveryWord holds every language the window +// carries to every word of the toolkit, translated from the English it says +// now. +func TestEveryLanguageGivesTheToolkitEveryWord(t *testing.T) { + files := catalogueFilesIn(t, toolkitDir) + english := files[text.English] + for tag := range catalogueFiles(t) { + entries, has := files[tag] + if !has { + t.Errorf("the window speaks %s and %s has no %s.json, so the toolkit's menus and dialogs speak the system's language there", tag, toolkitDir, tag) + continue + } + if tag == text.English { + continue + } + for key, want := range english { + entry, has := entries[key] + switch { + case !has || entry["other"] == "": + t.Errorf("%s has no word for the toolkit's %q", tag, key) + case entry["hash"] != want["hash"]: + t.Errorf("%s translates the toolkit's %q from an English it no longer says (%q). Record hash %s.", tag, key, want["other"], want["hash"]) + } + } + for key := range entries { + if _, known := english[key]; !known { + t.Errorf("%s carries %q, which the toolkit is not given", tag, key) + } + } + } +} diff --git a/internal/guard/windowlanguage_test.go b/internal/guard/windowlanguage_test.go index 35af6e6f..6ef5e49a 100644 --- a/internal/guard/windowlanguage_test.go +++ b/internal/guard/windowlanguage_test.go @@ -123,6 +123,14 @@ func TestEveryLanguageSaysEverythingTheWindowSays(t *testing.T) { t.Errorf("%s.json has no %s, so the window says it in English", tag, id) continue } + // The hash of the English it translated, since 2026-10-05: a key + // cannot tell that the English under it changed, and a sentence + // that looks translated and says last month's English is worse + // than an English one (docs/OKNO-PO-POLSKU-2026-10-05.md). + if entry["hash"] != want["hash"] { + t.Errorf("%s.json translates %s from an English the window no longer says. It says now:\n %q\n"+ + "Translate it again and record hash %s.", tag, id, want["other"], want["hash"]) + } given := map[string]bool{} for _, f := range catalogueField.FindAllStringSubmatch(want["other"]+want["one"], -1) { given[f[1]] = true @@ -192,6 +200,9 @@ func TestEveryLetterEveryLanguageSaysIsInTheTypeface(t *testing.T) { for tag, entries := range registryFiles(t) { languages[tag+" "+text.RegistryFolder] = entries } + for tag, entries := range catalogueFilesIn(t, saidDir) { + languages[tag+" "+text.SaidFolder] = entries + } for tag, entries := range languages { missing := map[string][]string{} for id, entry := range entries { diff --git a/internal/guard/windowwords_test.go b/internal/guard/windowwords_test.go new file mode 100644 index 00000000..86fde0ed --- /dev/null +++ b/internal/guard/windowwords_test.go @@ -0,0 +1,325 @@ +package guard + +import ( + "encoding/json" + "errors" + "os" + "regexp" + "sort" + "strings" + "testing" + "time" + + "fyne.io/fyne/v2/test" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" + "github.com/donislawdev/TestingFilesGenerator/internal/damage" + "github.com/donislawdev/TestingFilesGenerator/internal/engine" + "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/gui/parts" + "github.com/donislawdev/TestingFilesGenerator/internal/gui/text" + "github.com/donislawdev/TestingFilesGenerator/internal/preset" + "github.com/donislawdev/TestingFilesGenerator/internal/recipe" + "github.com/donislawdev/TestingFilesGenerator/internal/tool" + "github.com/donislawdev/TestingFilesGenerator/internal/version" +) + +// The window in the language it speaks, end to end - docs/OKNO-PO-POLSKU- +// 2026-10-05.md. A list shows its values under names in that language and +// hands back the values, the engine's refusals arrive in it, and the words +// around them - time left, the licence - follow. What these guards hold is +// what a translation can get wrong without a word: two values under one name, +// a sentence naming a value the list calls something else, a refusal that +// never went through the catalogue. + +// declaredLists is every closed list a window offers, by whose it is, with +// the setting the values belong to. +type declaredList struct { + owner, setting string + values []string +} + +func declaredLists() []declaredList { + var out []declaredList + add := func(owner string, props []format.Property) { + for _, p := range props { + if len(p.Choices) > 0 { + out = append(out, declaredList{owner, p.Name, p.Choices}) + } + } + } + for _, d := range format.All() { + add(string(text.FormatOwner(d.ID)), d.Properties) + } + for _, p := range preset.All() { + add(string(text.PresetOwner(p.ID)), append(append([]format.Property{}, p.Parameters...), p.Globals()...)) + } + for _, d := range tool.All() { + add(string(text.ToolOwner(d.ID)), d.Settings) + } + for _, d := range damage.All() { + add(string(text.DamageOwner(d.ID)), d.Parameters) + } + out = append(out, declaredList{"recipe", recipe.KeyExpected, recipe.Outcomes()}, + declaredList{"recipe", recipe.KeyExpectedReason, recipe.Reasons()}) + return out +} + +// TestEveryListSaysEachValueOnceInEveryLanguage holds a list to naming its +// values apart. Two values under one name are two rows nobody can choose +// between, and the one somebody picks writes a value they did not mean into +// the recipe. +func TestEveryListSaysEachValueOnceInEveryLanguage(t *testing.T) { + lists := declaredLists() + if len(lists) < 20 { + t.Fatalf("found %d list(s), so the registries were not filled in this process", len(lists)) + } + for tag, entries := range registryFiles(t) { + for _, l := range lists { + seen := map[string]string{} + for _, v := range l.values { + name := v + if e, has := entries[text.ChoiceKey(l.setting, v)]; has && e["other"] != "" { + name = e["other"] + } + if other, twice := seen[strings.ToLower(name)]; twice { + t.Errorf("%s calls %s and %s of %s %s both %q", tag, other, v, l.owner, l.setting, name) + } + seen[strings.ToLower(name)] = v + } + } + } +} + +// valueWord is a value written on its own in a sentence, not part of a longer +// word, a key, a flag or a path - in any case, because a value opening a +// sentence is written with a capital (the first version matched lower case +// only and passed four Polish sentences opening with Mixed, Fixed and +// Realistic, 2026-10-06). +func valueWord(value string) *regexp.Regexp { + return regexp.MustCompile(`(?i)(?:^|[^\w.\-=:/])` + regexp.QuoteMeta(value) + `(?:$|[^\w\-=:/])`) +} + +// TestNoTranslationNamesAListValueTheListCallsOtherwise holds the sentence +// under a setting to the words of the list above it. A Polish window whose +// list says średnik and whose sentence says "choose semicolon" is the mixed +// language this work removed, one line down from where it was (the owner's +// decision of 2026-10-05: a value of a list is said in a sentence by its name +// in the list). Values somebody types - all in a list of format ids, none in +// a date - are not on a list and are not asked about. +func TestNoTranslationNamesAListValueTheListCallsOtherwise(t *testing.T) { + byOwner := map[string][]declaredList{} + for _, l := range declaredLists() { + byOwner[l.owner] = append(byOwner[l.owner], l) + } + checked := 0 + for tag, entries := range registryFiles(t) { + if tag == text.English { + continue + } + for key, entry := range entries { + if !strings.HasPrefix(key, "Detail.") { + continue + } + owner := ownerOfDetail(key) + for _, l := range byOwner[owner] { + for _, v := range l.values { + named, has := entries[text.ChoiceKey(l.setting, v)] + if !has || named["other"] == v || len(v) < 3 { + continue + } + checked++ + if valueWord(v).MatchString(entry["other"]) { + t.Errorf("%s %s names %q, which the %s list above it calls %q:\n %s", + tag, key, v, l.setting, named["other"], entry["other"]) + } + } + } + } + } + if checked < 50 { + t.Fatalf("asked about %d value(s), so no sentence was held to a list", checked) + } +} + +// ownerOfDetail is the owner part of Detail.., where an owner +// holds one slash: format/pdf. +func ownerOfDetail(key string) string { + rest := strings.TrimPrefix(key, "Detail.") + if at := strings.LastIndex(rest, "."); at > 0 { + return rest[:at] + } + return rest +} + +// TestThePresetTitlesInTheWindowAreTheSites holds the window's titles of the +// presets to the ones the site already says, the way the questions are held - +// one Polish title in two files that nothing compared would drift. +func TestThePresetTitlesInTheWindowAreTheSites(t *testing.T) { + raw, err := os.ReadFile(repoRoot(t) + "/web/content/pl/site.json") + if err != nil { + t.Fatalf("reading the Polish site: %v", err) + } + var site struct { + Presets map[string]struct { + Title string `json:"title"` + } `json:"presets"` + } + if err := json.Unmarshal(raw, &site); err != nil { + t.Fatal(err) + } + window := registryFiles(t)["pl"] + ids := preset.IDs() + sort.Strings(ids) + for _, id := range ids { + said := window[text.TitleKey(id)]["other"] + if said == "" || said != site.Presets[id].Title { + t.Errorf("the window calls the %s preset %q and the site calls it %q", id, said, site.Presets[id].Title) + } + } +} + +// listWordsChild runs the guards below in a process of their own - each loads +// a catalogue, which changes the words of everything that runs after it. +const ( + listWordsPolishChild = "TFG_LIST_WORDS_POLISH_CHILD" + listWordsPseudoChild = "TFG_LIST_WORDS_PSEUDO_CHILD" + listWordsEnglishChild = "TFG_LIST_WORDS_ENGLISH_CHILD" +) + +// engineRefusals is a refusal from each package of the engine, made by asking +// it for something it refuses - not built by hand, so each is the error a +// window is really shown. +func engineRefusals(t *testing.T) map[string]error { + t.Helper() + out := map[string]error{} + avif, _ := format.Get("avif") + _, out["format minimum"] = avif.Generator.Plan(format.Request{Bytes: 1, Label: true}) + png, _ := format.Get("png") + out["format value"] = png.CheckProperties(map[string]string{"width": "99999"}) + _, out["engine"] = engine.Plan([]engine.Target{{ID: "a", Format: "txt", Sizes: engine.Uniform(1, 10)}}, engine.Options{}) + _, out["recipe"] = recipe.Parse([]byte("version: 1\n"), "r.yaml") + _, out["preset"] = preset.Expand("nosuch", nil) + _, out["damage"] = damage.Get("nosuch") + _, out["tool"] = tool.Get("nosuch") + _, out["size"] = core.ParseSize("abc") + for what, err := range out { + var said interface{ Said() core.Said } + if err == nil || !errors.As(err, &said) { + t.Fatalf("asking the %s for something it refuses gave %v, which is not a sentence kept as data", what, err) + } + } + return out +} + +// TestTheEnginesRefusalsReachThePolishWindowInPolish is the whole path for the +// engine's sentences: a refusal from each package, the catalogue compiled +// into this build, and the window's way of saying an error. A refusal that +// reached the window as its English would be the mixed window this work is +// about, and the file guards above would not notice a window that never asks. +func TestTheEnginesRefusalsReachThePolishWindowInPolish(t *testing.T) { + if os.Getenv(listWordsPolishChild) != "1" { + runChild(t, "TestTheEnginesRefusalsReachThePolishWindowInPolish", listWordsPolishChild) + return + } + refusals := engineRefusals(t) + if err := text.LoadBuiltIn("pl"); err != nil { + t.Fatalf("Polish would not load: %v", err) + } + for what, err := range refusals { + for _, one := range spreadAll(t, err) { + said := text.Refusal(one, "") + if said == one.Error() { + t.Errorf("the %s refusal reached the Polish window in English: %q", what, said) + } + } + } + // A list of a declared setting: the value under its Polish name in the + // box, and the value itself handed back. + pdf, _ := format.Get("pdf") + fields, _ := parts.PropertyFields(pdf, parts.NewFields(), nil) + for _, f := range fields { + menu, isMenu := f.Control.(*parts.Chooser) + if !isMenu || f.Name != "orientation" { + continue + } + test.NewTempWindow(t, menu) + menu.SetSelected("portrait") + if f.Value() != "portrait" { + t.Errorf("the orientation menu hands back %q, and a recipe needs portrait", f.Value()) + } + // Read off the words the box draws, not off the menu: the menu's + // Selected is the value, and it is meant to stay the value. + if shown := wordsInTheBox(t, menu).Text; shown != text.ChoiceName("orientation", "portrait") { + t.Errorf("the orientation menu shows %q, and the Polish window calls portrait %q", shown, text.ChoiceName("orientation", "portrait")) + } + return + } + t.Fatal("the PDF declares no orientation menu, so nothing was asked of a list") +} + +// spreadAll opens a refusal carrying several into the ones it carries, the +// way the window does before it says them. Every refusal opens into one at +// least, and a guard is failed by one that opens into none: until 2026-10-06 +// a refusal unwrapped to the list of what it wrapped, empty for most, so the +// window marked no box for them and the guards above asked them nothing. +func spreadAll(t *testing.T, err error) []error { + t.Helper() + if joined, several := err.(interface{ Unwrap() []error }); several { + var out []error + for _, one := range joined.Unwrap() { + out = append(out, spreadAll(t, one)...) + } + if len(out) == 0 { + t.Errorf("%q opens into no refusal at all, so a window would say nothing of it", err) + } + return out + } + return []error{err} +} + +// TestNoSentenceOfTheEngineReachesTheWindowUndisguised says every refusal of +// the engine in the pseudo language: every one of them comes out disguised, +// which is the proof it went through the catalogue - a refusal said around it +// would arrive as its English. +func TestNoSentenceOfTheEngineReachesTheWindowUndisguised(t *testing.T) { + if os.Getenv(listWordsPseudoChild) != "1" { + runChild(t, "TestNoSentenceOfTheEngineReachesTheWindowUndisguised", listWordsPseudoChild) + return + } + refusals := engineRefusals(t) + if err := text.LoadBuiltIn(text.Pseudo); err != nil { + t.Fatalf("the pseudo language would not load: %v", err) + } + for what, err := range refusals { + for _, one := range spreadAll(t, err) { + said := text.Refusal(one, "") + if !strings.HasPrefix(said, "[") || strings.Contains(said, one.Error()) { + t.Errorf("the %s refusal reached the window undisguised: %q", what, said) + } + } + } +} + +// TestTheEnglishWindowSaysTimeAndTheLicenceAsTheCommandLineDoes holds the +// window's own composition of two things the command line also says to the +// command line's words, in English: time left, rounded the same way, and the +// licence notice, unchanged and with nothing added. +func TestTheEnglishWindowSaysTimeAndTheLicenceAsTheCommandLineDoes(t *testing.T) { + if os.Getenv(listWordsEnglishChild) != "1" { + runChild(t, "TestTheEnglishWindowSaysTimeAndTheLicenceAsTheCommandLineDoes", listWordsEnglishChild) + return + } + if err := text.LoadBuiltIn(text.English); err != nil { + t.Fatal(err) + } + for _, d := range []time.Duration{0, 999 * time.Millisecond, 59 * time.Second, time.Minute, 59*time.Minute + 30*time.Second, + time.Hour, 3*time.Hour + 7*time.Minute, 49 * time.Hour} { + if got, want := text.Roughly(d), core.Roughly(d); got != want { + t.Errorf("%v is %q in the window and %q on the command line", d, got, want) + } + } + if got := text.Licence(version.LicenceNotice); got != strings.TrimSpace(version.LicenceNotice) { + t.Errorf("the English window's licence notice is not the notice the command line prints:\n%s", got) + } +} diff --git a/internal/gui/catalogue/controls.go b/internal/gui/catalogue/controls.go index 1807d8d4..29b636e3 100644 --- a/internal/gui/catalogue/controls.go +++ b/internal/gui/catalogue/controls.go @@ -158,6 +158,14 @@ func chooser() Entry { c.SetSelected(longText) return onAForm(c) }}, + {"a value drawn under its name, longer than the value", func() fyne.CanvasObject { + // A declared setting in a window that does not speak English: + // the box draws the name and still holds the value (ShownAs). + c := parts.NewChooser(namedValues, func(string) {}) + c.ShownAs = nameOfValue + c.SetSelected("landscape") + return onAForm(c) + }}, {"every format, showing the kind of its value", func() fyne.CanvasObject { // The one menu that draws a picture in the shut box - see // menuLook.placeKind. Its open list is wider than it, for the @@ -169,6 +177,21 @@ func chooser() Entry { }} } +// namedValues and nameOfValue are a list whose values are drawn under names +// longer than themselves, the way a declared setting is drawn in a window +// whose language is not English (Chooser.ShownAs). Words given to the state +// rather than the registry's, so the names differ from the values whatever +// language the catalogue is drawn in. +var namedValues = []string{"portrait", "landscape", "mixed"} + +func nameOfValue(v string) string { + return map[string]string{ + "portrait": "every page standing upright", + "landscape": "every page lying on its side", + "mixed": "turn by turn, starting upright", + }[v] +} + func entry() Entry { return Entry{Name: "Entry", States: []State{ {"empty, with its hint", func() fyne.CanvasObject { diff --git a/internal/gui/catalogue/lists.go b/internal/gui/catalogue/lists.go index 199e915f..58eb9920 100644 --- a/internal/gui/catalogue/lists.go +++ b/internal/gui/catalogue/lists.go @@ -54,6 +54,15 @@ func openList() Entry { l.KindOf = parts.KindOfFile return asWideAsItsBox(many, l) }}, + {"every value drawn under its name, longer than the value", func() fyne.CanvasObject { + // As wide as the box that draws the names, which is wider than + // one drawing the values. + box := parts.NewChooser(namedValues, func(string) {}) + box.ShownAs = nameOfValue + l := parts.NewOpenList(namedValues, "landscape", func(string, bool) {}, func(bool) {}) + l.ShowEach(nameOfValue) + return asWideAsTheBoxOf(box, l) + }}, {"a long value", func() fyne.CanvasObject { values := []string{longText, "png"} return asWideAsItsBox(values, parts.NewOpenList(values, "png", func(string, bool) {}, func(bool) {})) @@ -97,7 +106,13 @@ const everyFormatWindow = 640 // parts.Menu, or wider where the list needs it - the list of formats, which // names every value (parts.ListWidth). func asWideAsItsBox(values []string, list *parts.OpenList) fyne.CanvasObject { - return parts.Sized(parts.ListWidth(parts.NewChooser(values, func(string) {})), list) + return asWideAsTheBoxOf(parts.NewChooser(values, func(string) {}), list) +} + +// asWideAsTheBoxOf is asWideAsItsBox for a box that is given more than its +// values - the names it draws them under. +func asWideAsTheBoxOf(box *parts.Chooser, list *parts.OpenList) fyne.CanvasObject { + return parts.Sized(parts.ListWidth(box), list) } func tabs() Entry { diff --git a/internal/gui/parts/fields.go b/internal/gui/parts/fields.go index 9ae19b26..6bb3911f 100644 --- a/internal/gui/parts/fields.go +++ b/internal/gui/parts/fields.go @@ -1,10 +1,11 @@ package parts import ( - "errors" "strings" "fyne.io/fyne/v2" + + "github.com/donislawdev/TestingFilesGenerator/internal/gui/text" ) // Field is one labelled control that can say it was the one refused. @@ -554,14 +555,12 @@ func (s *Fields) Mark(setting string, err error) bool { // says where its own name goes instead, and anything without a slot is shown // exactly as the engine wrote it, which is the state this window was in before // the labels and is merely plain rather than wrong. See core.SettingSlot. +// +// Since 2026-10-05 it is asked in the window's language as well - see +// text.Refusal, which answers in English exactly as this did before whenever +// the window speaks English or has no words for the refusal. func inTheWordsOnScreen(f *Field, err error) string { - var reworded interface { - InTheWordsOf(string) string - } - if f.Label != "" && errors.As(err, &reworded) { - return reworded.InTheWordsOf(f.Label) - } - return err.Error() + return text.Refusal(err, f.Label) } // Clear takes back whatever one field was complaining about. diff --git a/internal/gui/parts/filekind.go b/internal/gui/parts/filekind.go index a3eeb1e7..2ed0df17 100644 --- a/internal/gui/parts/filekind.go +++ b/internal/gui/parts/filekind.go @@ -106,7 +106,7 @@ func NameOfFormat(id string) string { if err != nil { return "" } - return d.Name + return text.FormatName(id, d.Name) } type fileKind int diff --git a/internal/gui/parts/listcontents.go b/internal/gui/parts/listcontents.go index 9c13c200..f4925250 100644 --- a/internal/gui/parts/listcontents.go +++ b/internal/gui/parts/listcontents.go @@ -73,7 +73,7 @@ func (c *listContents) Rows() []Choice { out := make([]Choice, 0, len(c.entries)) for _, e := range c.entries { value := e.kind == entryValue - out = append(out, Choice{Label: e.text, Name: e.name, Marked: value && c.isChosen(e.text), Choosable: value}) + out = append(out, Choice{Label: e.text, Name: e.name, Marked: value && c.isChosen(e.value), Choosable: value}) } return out } diff --git a/internal/gui/parts/menulook.go b/internal/gui/parts/menulook.go index 6e90849f..67e9807e 100644 --- a/internal/gui/parts/menulook.go +++ b/internal/gui/parts/menulook.go @@ -41,6 +41,7 @@ func (c *Chooser) CreateRenderer() fyne.WidgetRenderer { } look.accent() look.showKind() + look.showName() return look } @@ -67,6 +68,7 @@ func (m *menuLook) Refresh() { // The toolkit's Refresh lays itself out again, which moves the value back // under the picture - so the picture's room is made again after it. m.WidgetRenderer.Refresh() + m.showName() m.accent() m.showKind() m.placeKind(m.menu.Size()) @@ -74,6 +76,26 @@ func (m *menuLook) Refresh() { // showKind puts the picture of the value now in the box into the box, or // hides it when the value has none. +// showName puts the words a value is drawn as in the box, over the value the +// toolkit wrote there - see Chooser.ShownAs. A menu whose values are their own +// words is left as the toolkit drew it. +func (m *menuLook) showName() { + if m.menu.ShownAs == nil || m.menu.Selected == "" { + return + } + for _, o := range m.WidgetRenderer.Objects() { + words, ok := o.(*widget.RichText) + if !ok || len(words.Segments) == 0 { + continue + } + if segment, ok := words.Segments[0].(*widget.TextSegment); ok { + segment.Text = m.menu.shown(m.menu.Selected) + words.Refresh() + } + return + } +} + func (m *menuLook) showKind() { if m.kind == nil { return diff --git a/internal/gui/parts/narrow.go b/internal/gui/parts/narrow.go index f2cba4b6..d33dc208 100644 --- a/internal/gui/parts/narrow.go +++ b/internal/gui/parts/narrow.go @@ -36,8 +36,12 @@ const ( // name is what the value is called, drawn beside it, with its own bold span in // nameFrom and nameTo - empty on a list whose values have no names. type listEntry struct { - kind entryKind + kind entryKind + // text is what the row shows, and value is what choosing the row hands + // back - the same word on most lists, and a value under its name in the + // window's language on a list of a declared setting (Chooser.ShownAs). text string + value string from, to int name string nameFrom, nameTo int @@ -49,6 +53,15 @@ type listEntry struct { type labels struct { headingOf func(string) string nameOf func(string) string + shownAs func(string) string +} + +// shown is the words a value is drawn as. +func (by labels) shown(v string) string { + if by.shownAs == nil { + return v + } + return by.shownAs(v) } // arrange is what an open list draws: the values the typed text keeps, each @@ -74,7 +87,7 @@ func arrange(values []string, by labels, typed string) []listEntry { if by.headingOf == nil { out := make([]listEntry, 0, len(kept)) for _, v := range kept { - out = append(out, valueEntry(v, by.nameOf, typed)) + out = append(out, valueEntry(v, by, typed)) } return out } @@ -96,7 +109,7 @@ func arrange(values []string, by labels, typed string) []listEntry { out = append(out, listEntry{kind: entryHeading, text: text.ListHeadingCount(h, len(groups[h]))}) } for _, v := range groups[h] { - out = append(out, valueEntry(v, by.nameOf, typed)) + out = append(out, valueEntry(v, by, typed)) } } return out @@ -113,16 +126,16 @@ func arrange(values []string, by labels, typed string) []listEntry { // letters as are drawn - true of every format and every format name, which // are ASCII (TestEveryFormatDeclaresTheFullSet), and a value for which it is // not simply gets no bold. -func valueEntry(v string, nameOf func(string) string, typed string) listEntry { - e := listEntry{kind: entryValue, text: v} - if nameOf != nil { - e.name = nameOf(v) +func valueEntry(v string, by labels, typed string) listEntry { + e := listEntry{kind: entryValue, text: by.shown(v), value: v} + if by.nameOf != nil { + e.name = by.nameOf(v) } want := strings.ToLower(strings.TrimSpace(typed)) if want == "" { return e } - if lower := strings.ToLower(v); len(lower) == len(v) { + if lower := strings.ToLower(e.text); len(lower) == len(e.text) { if at := strings.Index(lower, want); at >= 0 { e.from, e.to = at, at+len(want) } @@ -158,7 +171,7 @@ func narrow(values []string, by labels, typed string) []string { } out := make([]string, 0, len(values)) for _, v := range values { - if strings.Contains(strings.ToLower(v), want) || + if strings.Contains(strings.ToLower(by.shown(v)), want) || strings.Contains(strings.ToLower(v), want) || (by.headingOf != nil && aWordStartsWith(by.headingOf(v), want)) || (by.nameOf != nil && aWordStartsWith(by.nameOf(v), want)) { out = append(out, v) diff --git a/internal/gui/parts/openlist.go b/internal/gui/parts/openlist.go index a0235f35..4b8ae0fb 100644 --- a/internal/gui/parts/openlist.go +++ b/internal/gui/parts/openlist.go @@ -85,6 +85,14 @@ func NewOpenList(options []string, chosen string, take func(string, bool), close return l } +// ShowEach draws every value as the words shownAs gives it, and hands the +// value back when its row is chosen. Called before the list is shown, the way +// GroupUnder is. +func (l *OpenList) ShowEach(shownAs func(string) string) { + l.shownAs = shownAs + l.rearrange() +} + // GroupUnder puts every value under a heading, the one headingOf gives it. // Called before the list is shown, the way KindOf is set. func (l *OpenList) GroupUnder(headingOf func(string) string) { @@ -163,7 +171,7 @@ func (l *OpenList) fill(id int, r *ListRow) { r.onTap = nil r.hovered = r.hovered && !r.heading if entry.kind == entryValue { - value := entry.text + value := entry.value if l.KindOf != nil { r.kind = l.KindOf(value) } @@ -326,7 +334,7 @@ func (l *OpenList) TypedKey(event *fyne.KeyEvent) { l.close(true) case fyne.KeyReturn, fyne.KeyEnter, fyne.KeySpace: if l.active >= 0 && l.active < len(l.entries) && l.entries[l.active].kind == entryValue { - l.take(l.entries[l.active].text, true) + l.take(l.entries[l.active].value, true) } } } @@ -404,7 +412,7 @@ func (l *OpenList) Active() int { return l.active } // the first value while the box shows the ninth. func (l *OpenList) StartOn(value string) { for i, e := range l.entries { - if e.kind == entryValue && e.text == value { + if e.kind == entryValue && e.value == value { l.moveTo(i) l.shown = false break diff --git a/internal/gui/parts/property.go b/internal/gui/parts/property.go index ee91ef7f..6d91bb25 100644 --- a/internal/gui/parts/property.go +++ b/internal/gui/parts/property.go @@ -116,6 +116,7 @@ func FromProperty(p format.Property) PropertyField { // manifest as defaulted. Measured in the same run. func choiceField(p format.Property) PropertyField { sel := NewChooser(p.Choices, nil) + sel.ShownAs = func(v string) string { return text.ChoiceName(p.Name, v) } sel.PlaceHolder = leftAlone(p) if p.Default != "" { sel.SetSelected(p.Default) @@ -173,6 +174,9 @@ func textField(p format.Property) PropertyField { // format's to say, in the sentence beside the box. func leftAlone(p format.Property) string { switch { + case p.Default != "" && len(p.Choices) > 0: + // A value of a list is said the way the list says it. + return text.PlaceholderLeftEmpty(text.ChoiceName(p.Name, p.Default)) case p.Default != "": return text.PlaceholderLeftEmpty(p.Default) case p.Kind == format.PropertyText: diff --git a/internal/gui/parts/ring.go b/internal/gui/parts/ring.go index 9f90440a..8db16547 100644 --- a/internal/gui/parts/ring.go +++ b/internal/gui/parts/ring.go @@ -197,6 +197,13 @@ type Chooser struct { // searched by its filter - set on the same one menu, and making its list // wider than the box it drops from. See listWidth. NameOf func(string) string + // ShownAs is the words a value is drawn as, in the box and in the list, + // where they are not the value itself: the values of a declared setting + // are what a recipe writes, and the window shows them in its own language + // (the owner's decision of 2026-10-05). The menu still holds and hands + // back the value - Selected, OnChanged and SetSelected all speak values - + // so nothing that reads a menu reads a translation. Nil draws the values. + ShownAs func(string) string // opened is the list this menu last dropped down, and it is here for a // guard: the canvas says whether a list appeared, and this says what was // in it. Neither alone is worth anything - a list built correctly and never @@ -207,6 +214,23 @@ type Chooser struct { reports wiredOnce[string] } +// shown is the words one value is drawn as - see ShownAs. +func (c *Chooser) shown(v string) string { + if c.ShownAs == nil || v == "" { + return v + } + return c.ShownAs(v) +} + +// shownAll is every value as it is drawn, for measuring. +func shownAll(c *Chooser) []string { + out := make([]string, len(c.Options)) + for i, v := range c.Options { + out[i] = c.shown(v) + } + return out +} + // Opened is the list this menu last dropped down, or nil if it never has. func (c *Chooser) Opened() *OpenList { return c.opened } @@ -236,6 +260,9 @@ func NewChooser(options []string, changed func(string)) *Chooser { c := &Chooser{} c.Options = options c.OnChanged = changed + // The words of an empty menu, from the window's catalogue. The toolkit + // puts "(Select one)" in otherwise, in English whatever the window speaks. + c.PlaceHolder = text.PlaceholderChooseOne() if IsEveryFormat(options) { c.KindOf = KindOfFile // Grouped and filtered for the same reason and on the same test as @@ -305,16 +332,18 @@ func menuWidth(c *Chooser) float32 { // is the one measurement that knows the placeholder. The floor at the end // of this function is what covers it today, with room to spare. // - // Nothing of ours ever sets a placeholder on a menu - it would be a word a - // person reads coming from outside the text package - so there is no - // placeholder of ours to measure either. + // Since 2026-10-06 every menu is made with the window's own placeholder + // (NewChooser), so the toolkit no longer puts one in and the two widths + // cannot come back from that side. It is still not measured here: the + // floor at the end of this function covers it, and the guard above says so + // of every menu laid out. // In bold on a menu with a filter, because the filter draws the part of a // value that matched in bold, and bold letters are wider - so a value // typed in full is the widest that value is ever drawn. style := fyne.TextStyle{Bold: c.Filtered} var widest float32 for _, option := range c.Options { - if w := fyne.MeasureText(option, size, style).Width; w > widest { + if w := fyne.MeasureText(c.shown(option), size, style).Width; w > widest { widest = w } } @@ -425,7 +454,7 @@ func listWidth(c *Chooser, box float32) float32 { // Every name after the column the values stand in, each measured in // bold: the filter draws what matched in bold, and a name typed in // full is the widest it is ever drawn. - column := widestValue(c.Options) + column := widestValue(shownAll(c)) for _, option := range c.Options { name := fyne.MeasureText(c.NameOf(option), theme.TextSize(), fyne.TextStyle{Bold: true}).Width widest = fyne.Max(widest, RowWidthFor(column+listNameGap+name, c.KindOf != nil)) @@ -577,6 +606,9 @@ func (c *Chooser) drop(surface fyne.Canvas) { if c.Filtered { list.WithFilter() } + if c.ShownAs != nil { + list.ShowEach(c.ShownAs) + } if c.NameOf != nil { list.NameEach(c.NameOf) } @@ -776,7 +808,7 @@ func (c *Chooser) TypedRune(r rune) { if at < 0 { at += len(c.Options) } - if strings.HasPrefix(strings.ToLower(c.Options[at]), want) { + if strings.HasPrefix(strings.ToLower(c.shown(c.Options[at])), want) { c.SetSelectedIndex(at) return } diff --git a/internal/gui/run_cgo.go b/internal/gui/run_cgo.go index 138d5175..37bc91ef 100644 --- a/internal/gui/run_cgo.go +++ b/internal/gui/run_cgo.go @@ -494,6 +494,22 @@ func speakTheChosenLanguage(kept window.Remembered, pseudo bool, errOut io.Write if err := text.LoadBuiltIn(speak); err != nil { fmt.Fprintln(errOut, text.CatalogueNotLoaded(err)) } + // The toolkit's own words - the menu of a box to type in, the dialogs that + // choose a folder or a file - in the language the window speaks rather + // than the system's. The toolkit picks its language from the system and + // offers no call to choose one, so ours are given under the system's own + // tag, which then is the closest match it can find. Measured 2026-10-05 + // on a pl-PL system (tools/probes/fynelang): words given under pl-PL + // replace the toolkit's, and words given under any other tag change + // nothing. Before the first window, so no word of the toolkit has been + // asked for yet. + words, err := text.ToolkitWords() + if err == nil { + err = lang.AddTranslationsForLocale(words, lang.SystemLocale()) + } + if err != nil { + fmt.Fprintln(errOut, text.CatalogueNotLoaded(err)) + } } // restartInstead starts the program again once this window has gone. The @@ -506,7 +522,7 @@ func restartInstead(args []string, errOut io.Writer) { if err == nil { return } - sentence := text.PreferencesRestartFailed(err.Error()) + sentence := text.PreferencesRestartFailed(text.Refusal(err, "")) fmt.Fprintln(errOut, sentence) sayInADialog(text.WindowTitle(version.Version), sentence) } diff --git a/internal/gui/text/catalogue.go b/internal/gui/text/catalogue.go index 23970942..782434d7 100644 --- a/internal/gui/text/catalogue.go +++ b/internal/gui/text/catalogue.go @@ -222,9 +222,15 @@ func Load(fsys fs.FS, dir string, prefer ...string) error { // translator, and the English on screen comes from the registries // themselves - a copy one commit behind them must not be able to answer // for them. - err := loadFiles(bundle, fsys, path.Join(dir, RegistryFolder), English+".json") - if err != nil && !errors.Is(err, fs.ErrNotExist) { - return err + // + // The engine's sentences the same way, and for the same reason: their + // English is in the code that refuses, and en.json beside them is a copy for + // a translator - see said.go. + for _, folder := range []string{RegistryFolder, SaidFolder} { + err := loadFiles(bundle, fsys, path.Join(dir, folder), English+".json") + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return err + } } localiser = i18n.NewLocalizer(bundle, append(prefer, English)...) @@ -270,6 +276,33 @@ func carries(bundle *i18n.Bundle, asked string) bool { return false } +// ToolkitFolder is where the words of the graphics toolkit are kept, inside +// the folder of the window's catalogue. Never loaded into the window's own +// catalogue - they are the toolkit's, and are handed to it. +const ToolkitFolder = "toolkit" + +// ToolkitWords is the toolkit's words in the language this window speaks, as a +// catalogue file the toolkit reads: the language's own file, the English one +// for a language that has none, and the English disguised in Pseudo. +func ToolkitWords() ([]byte, error) { + raw, err := fs.ReadFile(builtIn, path.Join("locale", ToolkitFolder, Speaking()+".json")) + if err != nil { + raw, err = fs.ReadFile(builtIn, path.Join("locale", ToolkitFolder, English+".json")) + } + if err != nil || !pseudo { + return raw, err + } + var entries map[string]map[string]string + if err := json.Unmarshal(raw, &entries); err != nil { + return nil, err + } + disguised := map[string]map[string]string{} + for key, entry := range entries { + disguised[key] = map[string]string{"other": pseudoOf(entry["other"])} + } + return json.Marshal(disguised) +} + // LoadBuiltIn is Load over the catalogue compiled into this program. // // It is called once, where the window is built. Changing language after that diff --git a/internal/gui/text/locale/en.json b/internal/gui/text/locale/en.json index 913ec299..85d3b4cc 100644 --- a/internal/gui/text/locale/en.json +++ b/internal/gui/text/locale/en.json @@ -1,832 +1,1104 @@ { + "AboutLicenceBinds": { + "description": "Shown on the About screen.", + "hash": "sha256-e3b0c44298fc", + "other": "" + }, "AboutTagline": { "description": "Shown on the About screen.", + "hash": "sha256-3677496ffba0", "other": "Generate test files, and know how the system under test should react to them." }, "AllowedDefault": { "description": "The start of the sentence under a setting a format, a preset or a damage declares, saying what it takes. The values in it - true, 2mb, the numbers, the default, the names a recipe writes - stay as they are. Carries one value, {{.Default}}, which has to stay spelled exactly that way.", + "hash": "sha256-22ad56774302", "other": "default {{.Default}}" }, "AllowedJointly": { "description": "A note under two settings of a format whose product has a ceiling. Of and By are the names a recipe writes and stay as they are. Carries these values, each of which has to stay spelled exactly that way: {{.Of}}, {{.By}}, {{.Max}}, {{.Unit}}, {{.Why}}.", + "hash": "sha256-890007fb5262", "other": "{{.Of}} times {{.By}} cannot pass {{.Max}} {{.Unit}}, because {{.Why}}" }, "AllowedOneOf": { "description": "The start of the sentence under a setting a format, a preset or a damage declares, saying what it takes. The values in it - true, 2mb, the numbers, the default, the names a recipe writes - stay as they are. Carries one value, {{.Values}}, which has to stay spelled exactly that way.", + "hash": "sha256-96a180377b5d", "other": "one of: {{.Values}}" }, "AllowedRange": { "description": "The start of the sentence under a setting a format, a preset or a damage declares, saying what it takes. The values in it - true, 2mb, the numbers, the default, the names a recipe writes - stay as they are. Carries these values, each of which has to stay spelled exactly that way: {{.Min}}, {{.Max}}.", + "hash": "sha256-430416192be6", "other": "whole number from {{.Min}} to {{.Max}}" }, "AllowedRangeOf": { "description": "The start of the sentence under a setting a format, a preset or a damage declares, saying what it takes. The values in it - true, 2mb, the numbers, the default, the names a recipe writes - stay as they are. Carries these values, each of which has to stay spelled exactly that way: {{.Unit}}, {{.Min}}, {{.Max}}.", + "hash": "sha256-f52248fe6440", "other": "whole number of {{.Unit}} from {{.Min}} to {{.Max}}" }, "AllowedSize": { "description": "The start of the sentence under a setting a format, a preset or a damage declares, saying what it takes. The values in it - true, 2mb, the numbers, the default, the names a recipe writes - stay as they are.", + "hash": "sha256-ab006a92eb28", "other": "a size such as 2mb, or a plain byte count" }, "AllowedTrueOrFalse": { "description": "The start of the sentence under a setting a format, a preset or a damage declares, saying what it takes. The values in it - true, 2mb, the numbers, the default, the names a recipe writes - stay as they are.", + "hash": "sha256-ed50d4017aaf", "other": "true or false" }, "AllowedWholeNumber": { "description": "The start of the sentence under a setting a format, a preset or a damage declares, saying what it takes. The values in it - true, 2mb, the numbers, the default, the names a recipe writes - stay as they are.", + "hash": "sha256-24c327302769", "other": "whole number" }, "AllowedWholeNumberOf": { "description": "The start of the sentence under a setting a format, a preset or a damage declares, saying what it takes. The values in it - true, 2mb, the numbers, the default, the names a recipe writes - stay as they are. Carries one value, {{.Unit}}, which has to stay spelled exactly that way.", + "hash": "sha256-3d353f1038c7", "other": "whole number of {{.Unit}}" }, "AllowedWithDefault": { "description": "The start of the sentence under a setting a format, a preset or a damage declares, saying what it takes. The values in it - true, 2mb, the numbers, the default, the names a recipe writes - stay as they are. Carries these values, each of which has to stay spelled exactly that way: {{.What}}, {{.Default}}.", + "hash": "sha256-a8076abaa30c", "other": "{{.What}}, default {{.Default}}" }, "BatchHeading": { "description": "Shown in the window. Carries one value, {{.Number}}, which has to stay spelled exactly that way.", + "hash": "sha256-f26edb187ab1", "other": "Batch {{.Number}}" }, "ButtonAddBatch": { "description": "The words on a button.", + "hash": "sha256-cf795e51114f", "other": "Add a batch" }, "ButtonAddContents": { "description": "The words on a button.", + "hash": "sha256-4b12027a5240", "other": "Add files inside" }, "ButtonCancel": { "description": "The words on a button.", + "hash": "sha256-19766ed6ccb2", "other": "Cancel" }, "ButtonChoose": { "description": "The words on a button.", + "hash": "sha256-895ba517a993", "other": "Choose..." }, "ButtonCopy": { "description": "The words on a button.", + "hash": "sha256-e21f935f11d7", "other": "Copy" }, "ButtonCopyAll": { "description": "The words on a button.", + "hash": "sha256-96e3f09a5f16", "other": "Copy all" }, "ButtonDonate": { "description": "The words on a button.", + "hash": "sha256-c91ee0f2799d", "other": "Donate" }, "ButtonDuplicateBatch": { "description": "The words on a button.", + "hash": "sha256-02cdaabfca80", "other": "Duplicate" }, "ButtonForget": { "description": "The words on a button.", + "hash": "sha256-a6bd489d9550", "other": "Forget" }, "ButtonGenerate": { "description": "The words on a button.", + "hash": "sha256-49e49bb4401e", "other": "Generate" }, "ButtonOpenFolder": { "description": "The words on a button.", + "hash": "sha256-6a9084025c4b", "other": "Open folder" }, "ButtonOpenInstructions": { "description": "The words on a button.", + "hash": "sha256-d5ca5d9e594b", "other": "Open instructions" }, "ButtonOpenManifest": { "description": "The words on a button.", + "hash": "sha256-520b7632c1e9", "other": "Open manifest" }, "ButtonPreview": { "description": "The words on a button.", + "hash": "sha256-324b134f57c7", "other": "Preview" }, "ButtonRemoveBatch": { "description": "The words on a button.", + "hash": "sha256-c3812fc4acb8", "other": "Remove" }, "ButtonRemoveContents": { "description": "The words on a button.", + "hash": "sha256-c3812fc4acb8", "other": "Remove" }, "ButtonRestart": { "description": "The words on a button.", + "hash": "sha256-09af1002abaf", "other": "Restart now" }, "ButtonRunTool": { "description": "The words on a button.", + "hash": "sha256-00d60e31a4e6", "other": "Run" }, "ChoiceSameAsSystem": { "description": "An entry in a list somebody chooses from. Carries one value, {{.Language}}, which has to stay spelled exactly that way.", + "hash": "sha256-bb58ca050419", "other": "Same as the system ({{.Language}})" }, "ContentsHeading": { "description": "Shown where an archive says what it holds.", + "hash": "sha256-5aa71ee1325d", "other": "Files inside each archive" }, "DamageNone": { "description": "Shown in the window.", + "hash": "sha256-140bedbf9c3f", "other": "none" }, "DamageSettingsFor": { "description": "Shown in the window. Carries one value, {{.Damage}}, which has to stay spelled exactly that way.", + "hash": "sha256-ef202c268786", "other": "Settings for {{.Damage}}" }, "DecimalMark": { "description": "What this language writes between the whole part of a number and its tenths, as in 10.0 MB. One character.", + "hash": "sha256-cdb4ee2aea69", "other": "." }, + "Defect": { + "description": "Shown in the window. Carries one value, {{.Detail}}, which has to stay spelled exactly that way.", + "hash": "sha256-2241be2ea8a6", + "other": "Something inside the program went wrong, and it should not have. Report it with this description: {{.Detail}}" + }, "DetailBasePreset": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-e04b69a605dc", "other": "The settings under it are the preset's own. One left empty takes its default, and the manifest records which ones did." }, "DetailBoundary": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-eaf5b74b1f70", "other": "Give the limit your system declares, as 10mb. Units count in 1024s, and the run prints the number it used." }, "DetailBuildOnPreset": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-5f88fed7a993", "other": "Unticked, the batches below are the whole recipe. Ticked, the chosen preset's files come first and the batches are added after them." }, "DetailDamage": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-96921213f617", "other": "The files come out the size you asked for and no reader will accept them, which is what a validator has to reject. The manifest records what was broken and says the file is expected to be rejected." }, "DetailDonate": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-8531eb297aaa", "other": "Opens the support page in your browser. The tool is free and stays free - this pays for the time that goes into it." }, "DetailExpected": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-7923af561fdd", "other": "It reaches the manifest and nothing else reads it. Leave it alone where the right answer depends on the application's own policy." }, "DetailGroup": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-37e7791bbda7", "other": "It reaches the manifest, so a test can assert about a whole class of case at once." }, "DetailLabel": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-aeb24fe1804e", "other": "Writes into the file what it is and how big it was meant to be. Turn it off for a file that has to hold nothing but its content." }, "DetailManifest": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-98009d7dd5ba", "other": "It goes in the output directory beside the files." }, "DetailNameTemplate": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-3e64e80529fb", "other": "Left empty, the files are named after the batch and numbered, like invoices_0001.pdf. Fill it in and the name is used exactly as written, extension and all. The one placeholder is {index:04}, which becomes 0001, 0002 and so on, and a batch of more than one file needs it." }, "DetailOutputDir": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-55317cd71eaa", "other": "It is created if it is not there." }, "DetailPreset": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-9c28266cd16b", "other": "The set is worked out from the answer." }, "DetailPurpose": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-d3b3309352dc", "other": "It is written into the manifest and into the instructions beside it, which say what every file of the run is for. It changes no byte of any file." }, "DetailReason": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-d7f3c443d829", "other": "From a closed list, so a report can group by reason. It names the rule in play whatever the outcome is - a file a byte under a size limit is expected to be accepted, and the rule in play is still the size limit. A reason needs an outcome beside it." }, "DetailSeed": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-823564b2ed53", "other": "On any machine and in any build of this tool. 0 is the seed a run uses when nobody asks for another one. It is a seed like any other rather than a request for random files, which this tool never produces." }, "DetailSize": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-794528f6abe6", "other": "Units count in 1024s, so 10mb is 10485760 bytes. A plain number is a count of bytes." }, "DetailSizeRange": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-421d366dbd3e", "other": "Two sizes with a hyphen, as 1kb-8kb. Each file gets its own size, drawn from the seed, so the run repeats." }, "DetailSizeWay": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-b65673254620", "other": "One size gives every file the same size. A range draws a different size for each file. Around a limit makes three files: one byte under the limit, one on it, one over." }, "DetailTargetID": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-1a2680483a4d", "other": "It reaches the manifest and the file names, so a test can tell these files from the rest of the run. The seeds are derived from it, so changing it changes the bytes." }, "DetailTool": { "description": "The longer explanation behind the button beside a field name.", + "hash": "sha256-1fdd99516e02", "other": "What the tool works on and its settings change with the tool chosen." }, "DirectoryWithFreeSpace": { "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.Directory}}, {{.Free}}.", + "hash": "sha256-3e96ff18edeb", "other": "{{.Directory}} ({{.Free}} free)" }, "FieldBasePreset": { "description": "The name above a box somebody fills in.", + "hash": "sha256-6daf826260dd", "other": "Preset to build on" }, "FieldBoundary": { "description": "The name above a box somebody fills in.", + "hash": "sha256-0db0580b20bd", "other": "Limit to test" }, "FieldBuildOnPreset": { "description": "The name above a box somebody fills in.", + "hash": "sha256-765fe22ce5d4", "other": "Start from a preset" }, "FieldCount": { "description": "The name above a box somebody fills in.", + "hash": "sha256-5369b7db3d69", "other": "How many files" }, "FieldDamage": { "description": "The name above a box somebody fills in.", + "hash": "sha256-0c874f581a45", "other": "Damage" }, "FieldExpected": { "description": "The name above a box somebody fills in.", + "hash": "sha256-08159997039d", "other": "Expected outcome" }, "FieldFormat": { "description": "The name above a box somebody fills in.", + "hash": "sha256-2f343666aaa8", "other": "Format" }, "FieldGroup": { "description": "The name above a box somebody fills in.", + "hash": "sha256-6f87ee9cb9dc", "other": "Kind of case" }, "FieldLabel": { "description": "The name above a box somebody fills in.", + "hash": "sha256-98e8b12087c4", "other": "Label in each file" }, "FieldLanguage": { "description": "The name above a box somebody fills in.", + "hash": "sha256-703df606ae88", "other": "Window language" }, "FieldManifest": { "description": "The name above a box somebody fills in.", + "hash": "sha256-882e40f5910b", "other": "Manifest file name" }, "FieldNameTemplate": { "description": "The name above a box somebody fills in.", + "hash": "sha256-6d00d6e32977", "other": "File names" }, "FieldOutputDir": { "description": "The name above a box somebody fills in.", + "hash": "sha256-c94c0d681c64", "other": "Output directory" }, "FieldPreset": { "description": "The name above a box somebody fills in.", + "hash": "sha256-7252e7ce0085", "other": "Preset" }, "FieldPurpose": { "description": "The name above a box somebody fills in.", + "hash": "sha256-d4e8830a71c7", "other": "Purpose" }, "FieldReason": { "description": "The name above a box somebody fills in.", + "hash": "sha256-d697fea77188", "other": "Rule being tested" }, "FieldSeed": { "description": "The name above a box somebody fills in.", + "hash": "sha256-367ef06b8fbf", "other": "Seed" }, "FieldSize": { "description": "The name above a box somebody fills in.", + "hash": "sha256-1af851907331", "other": "Size" }, "FieldSizeRange": { "description": "The name above a box somebody fills in.", + "hash": "sha256-131fb459b1a7", "other": "Size range" }, "FieldSizeWay": { "description": "The name above a box somebody fills in.", + "hash": "sha256-8ab998e87d55", "other": "How the size is given" }, "FieldTargetID": { "description": "The name above a box somebody fills in.", + "hash": "sha256-cb719956a85c", "other": "Batch name" }, "FieldTool": { "description": "The name above a box somebody fills in.", + "hash": "sha256-2e53bdcd0740", "other": "Tool" }, "Files": { "description": "Shown in the window. One form per number, and a language may need more than the two English has - add them beside these. Carries one value, {{.Count}}, which has to stay spelled exactly that way.", + "hash": "sha256-70212ca62bcd", "one": "1 file", "other": "{{.Count}} files" }, "FormatCount": { "description": "Shown in the window. One form per number, and a language may need more than the two English has - add them beside these. Carries one value, {{.Count}}, which has to stay spelled exactly that way.", + "hash": "sha256-12de0241f4a5", "one": "1 format", "other": "{{.Count}} formats" }, "HintBasePreset": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-d75c8b442fd9", "other": "Its files come first." }, "HintBoundary": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-3ef02abcdddf", "other": "Three files: one byte under the limit, one on it, one over." }, "HintDamage": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-67e5c4c4940a", "other": "Break the files on purpose." }, "HintExpected": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-a1b4f34d2e0e", "other": "What the system under test should do with these files." }, "HintFormat": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-f5616fe5a139", "other": "What kind of file to produce." }, "HintGroup": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-e95a20274902", "other": "Marks several batches as one kind of case." }, "HintManifest": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-986221f924c6", "other": "The record of what this run produced." }, "HintNameTemplate": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-c92ff0a07510", "other": "What every file in this batch is called." }, "HintOutputDir": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-35dfac65eb8a", "other": "Where the files and the manifest go." }, "HintPreset": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-e86685eae6f6", "other": "What you are testing." }, "HintPurpose": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-ff421b5abcc8", "other": "What these files are and why they are in the set." }, "HintReason": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-6307e1666cea", "other": "Which rule this is about." }, "HintSeed": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-5cd4cde003a4", "other": "The same seed gives the same bytes." }, "HintSize": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-70aad1580c12", "other": "Exact size of every file." }, "HintSizeExact": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-ee94fadda67a", "other": "One size for every file." }, "HintSizeRange": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-a7f91764d39d", "other": "A different size for every file." }, "HintTargetID": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-16b346acd09e", "other": "A short name for this batch." }, "HintTool": { "description": "The line under a field name, saying what the field does.", + "hash": "sha256-6a3014a90ca2", "other": "What you want to find out." }, "HowToUseChoose": { "description": "Shown in the window.", + "hash": "sha256-d3826a08883e", "other": "Choose a preset, or fill in one batch on the first screen." }, "HowToUsePress": { "description": "Shown in the window.", + "hash": "sha256-efe55efa1794", "other": "Press Generate. The files and a manifest land in the output folder." }, "HowToUseRead": { "description": "Shown in the window.", + "hash": "sha256-ef0d5f7632c8", "other": "Point your test at the manifest. For every file it says what the system under test should do with it - accept it, reject it or sanitize it - or records the outcome as unspecified, where the right answer belongs to the application's own policy." }, "InstructionsNotSaved": { "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.Path}}, {{.Cause}}.", + "hash": "sha256-bfb2e6c568e3", "other": "The instructions could not be saved to {{.Path}} ({{.Cause}}). The manifest was saved and holds the same facts." }, "LanguageName": { "description": "The name of the language this file is written in, written in that language: Polski, not Polish. It is how the language is listed on the Preferences screen.", + "hash": "sha256-ba118bf7fc9c", "other": "English" }, "ListHeadingCount": { "description": "Shown inside an open list somebody chooses from. Carries these values, each of which has to stay spelled exactly that way: {{.Kind}}, {{.Count}}.", + "hash": "sha256-058722bef091", "other": "{{.Kind}} · {{.Count}}" }, + "ListJoinedAnd": { + "description": "Shown inside an open list somebody chooses from. Carries these values, each of which has to stay spelled exactly that way: {{.Head}}, {{.Last}}.", + "hash": "sha256-40953da1c535", + "other": "{{.Head}} and {{.Last}}" + }, + "ListJoinedOr": { + "description": "Shown inside an open list somebody chooses from. Carries these values, each of which has to stay spelled exactly that way: {{.Head}}, {{.Last}}.", + "hash": "sha256-c9be11158971", + "other": "{{.Head}} or {{.Last}}" + }, "ListKindArchives": { "description": "A heading inside an open list of formats, over the formats of one kind of file.", + "hash": "sha256-e404aa80d8df", "other": "Archives" }, "ListKindDocuments": { "description": "A heading inside an open list of formats, over the formats of one kind of file.", + "hash": "sha256-b4e929d8bcfe", "other": "Documents" }, "ListKindPictures": { "description": "A heading inside an open list of formats, over the formats of one kind of file.", + "hash": "sha256-1e6245009d67", "other": "Pictures" }, "ListKindSound": { "description": "A heading inside an open list of formats, over the formats of one kind of file.", + "hash": "sha256-46124e023603", "other": "Sound" }, "ListKindText": { "description": "A heading inside an open list of formats, over the formats of one kind of file.", + "hash": "sha256-c04c0f52d26a", "other": "Text and data" }, "ListKindVideo": { "description": "A heading inside an open list of formats, over the formats of one kind of file.", + "hash": "sha256-d534be829e32", "other": "Video" }, "ListNothingMatches": { "description": "Shown inside an open list somebody chooses from.", + "hash": "sha256-7b2c3b9853ac", "other": "No format matches - clear the box to see all" }, "ManifestNamed": { "description": "Shown in the window. Carries one value, {{.Name}}, which has to stay spelled exactly that way.", + "hash": "sha256-f9540bb6e465", "other": "Manifest: {{.Name}}" }, "ManifestNotSaved": { "description": "Shown in the window. Carries one value, {{.Path}}, which has to stay spelled exactly that way.", + "hash": "sha256-bb722052cb9e", "other": "the files were written and the manifest could not be saved to {{.Path}}" }, "ManifestTooLargeToRead": { "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.Size}}, {{.Limit}}.", + "hash": "sha256-0b5d14c5fc58", "other": "this run's record is about {{.Size}} and this build reads at most {{.Limit}}, so Verify and Clean up will not be able to read it. Split the run to keep each record readable." }, "NotAWholeNumber": { "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.Field}}, {{.Value}}.", + "hash": "sha256-2537ac57d3c7", "other": "{{.Field}} is {{.Value}}, which is not a whole number. Write the digits out, such as 1 or 500" }, "NoteBase": { "description": "Shown in the window.", + "hash": "sha256-e48510060140", "other": "With the box ticked, the chosen preset's files come first and the batches below are added after them. Without it, the batches are the whole recipe." }, "NoteManifestOnly": { "description": "Shown in the window.", + "hash": "sha256-794d5d9e2043", "other": "These describe the case. They go into the manifest and change nothing in the files." }, "NothingProduced": { "description": "Shown in the window.", + "hash": "sha256-7d90c4a68d2c", "other": "Nothing was produced." }, + "PlaceholderChooseOne": { + "description": "Stands in an empty box, in a quieter colour than a value.", + "hash": "sha256-fdfe6bf17ea5", + "other": "(Select one)" + }, "PlaceholderFilter": { "description": "Stands in an empty box, in a quieter colour than a value.", + "hash": "sha256-7647095cb6a8", "other": "type to filter" }, "PlaceholderLeftEmpty": { "description": "Stands in an empty box, in a quieter colour than a value. Carries one value, {{.Value}}, which has to stay spelled exactly that way.", + "hash": "sha256-aaa21a9c2770", "other": "default: {{.Value}}" }, "PlaceholderNotSet": { "description": "Stands in an empty box, in a quieter colour than a value.", + "hash": "sha256-1aef93991721", "other": "not set" }, "PlaceholderNotStated": { "description": "Stands in an empty box, in a quieter colour than a value.", + "hash": "sha256-109df22c5a75", "other": "not stated" }, "PlaceholderWorkedOut": { "description": "Stands in an empty box, in a quieter colour than a value.", + "hash": "sha256-195191b2dc3a", "other": "worked out from the size" }, "PreferencesForgetWhat": { "description": "Shown on the Preferences screen.", + "hash": "sha256-f7c12f863d64", "other": "Forget clears the three things above. This window then keeps nothing when it closes, and the next one opens the way the first one did." }, "PreferencesForgotten": { "description": "Shown on the Preferences screen.", + "hash": "sha256-8e950dfcda31", "other": "Forgotten. The next window opens the way the first one did." }, "PreferencesKept": { "description": "Shown on the Preferences screen.", + "hash": "sha256-896c43e06cdb", "other": "Between runs this window keeps three things: the output directory, the size of the window, and the language chosen above. The folder picker also keeps the last folder it showed. Nothing leaves this computer." }, "PreferencesKeptIn": { "description": "Shown on the Preferences screen.", + "hash": "sha256-65ceb0c15ff4", "other": "They are kept in this folder:" }, "PreferencesLanguageScope": { "description": "Shown on the Preferences screen.", + "hash": "sha256-bc4174717ca7", "other": "Only the window changes language. The files it makes, recipes, manifests and the command line do not." }, "PreferencesMissing": { "description": "Shown on the Preferences screen. Carries one value, {{.Tag}}, which has to stay spelled exactly that way.", + "hash": "sha256-7679a7684a73", "other": "The language chosen before, {{.Tag}}, is not in this version. The window follows the system until you choose again." }, "PreferencesNextStart": { "description": "Shown on the Preferences screen.", + "hash": "sha256-6fee24596599", "other": "The window speaks the chosen language from the next time it opens." }, "PreferencesRestartBusy": { "description": "Shown on the Preferences screen.", + "hash": "sha256-1d940383b3e9", "other": "Files are being made. Restart once they are done." }, "PreferencesRestartClears": { "description": "Shown on the Preferences screen.", + "hash": "sha256-29776eba6622", "other": "Restarting closes this window and opens it again. Anything typed on the other tabs is cleared." }, "PreferencesRestartFailed": { "description": "Shown on the Preferences screen. Carries one value, {{.Reason}}, which has to stay spelled exactly that way.", + "hash": "sha256-8d92a5a49edb", "other": "The window could not start again: {{.Reason}}. The language chosen is saved. Open the program again to see it." }, "PresetCatchesHeading": { "description": "Shown on the preset screen.", + "hash": "sha256-110703eac5e9", "other": "Typically finds:" }, "PreviewNothingWritten": { "description": "Shown after a preview and before anything is written.", + "hash": "sha256-9aea7a90a504", "other": "nothing written yet" }, "Progress": { "description": "Shown while a run is going. Carries these values, each of which has to stay spelled exactly that way: {{.Done}}, {{.Total}}, {{.BytesDone}}, {{.BytesTotal}}, {{.Percent}}.", + "hash": "sha256-a9de146c2bba", "other": "{{.Done}}/{{.Total}} files {{.BytesDone}} of {{.BytesTotal}} {{.Percent}}%" }, "RefusedBeforeWriting": { "description": "Shown in the window.", + "hash": "sha256-531c1a087f5f", "other": "Nothing was written. Check the settings marked above." }, "RendererDidNotHelp": { "description": "About the software renderer shipped beside the program on Windows: in the system dialog and on standard error when the window could not be opened even so, on standard error when a window starts drawing with it, and on the About screen while it does.", + "hash": "sha256-cb20cd3fbbee", "other": "The software renderer shipped beside the program was loaded, and the graphics toolkit still could not open a window with it." }, "RendererDrawing": { "description": "About the software renderer shipped beside the program on Windows: in the system dialog and on standard error when the window could not be opened even so, on standard error when a window starts drawing with it, and on the About screen while it does.", + "hash": "sha256-b94c25a817e3", "other": "This window is drawn by the software renderer shipped beside the program (Mesa llvmpipe), not by the graphics driver. Everything works. Drawing is slower than with a driver that provides OpenGL 2.1." }, "RendererNotBeside": { "description": "About the software renderer shipped beside the program on Windows: in the system dialog and on standard error when the window could not be opened even so, on standard error when a window starts drawing with it, and on the About screen while it does. Carries one value, {{.Path}}, which has to stay spelled exactly that way.", + "hash": "sha256-b0e260ef6617", "other": "The software renderer that ships with the Windows archive was not found beside the program - {{.Path}} is missing. Put the opengl folder from the archive back next to tfg-gui.exe, or use a graphics driver that provides OpenGL 2.1." }, "RendererNotLoaded": { "description": "About the software renderer shipped beside the program on Windows: in the system dialog and on standard error when the window could not be opened even so, on standard error when a window starts drawing with it, and on the About screen while it does. Carries one value, {{.Error}}, which has to stay spelled exactly that way.", + "hash": "sha256-55832142c3eb", "other": "The software renderer shipped beside the program could not be loaded: {{.Error}}." }, "RendererNotReadable": { "description": "About the software renderer shipped beside the program on Windows: in the system dialog and on standard error when the window could not be opened even so, on standard error when a window starts drawing with it, and on the About screen while it does. Carries one value, {{.Error}}, which has to stay spelled exactly that way.", + "hash": "sha256-453a6da2bea7", "other": "A file of the software renderer beside the program could not be read: {{.Error}}. Check who may read the opengl folder next to tfg-gui.exe, or use a graphics driver that provides OpenGL 2.1." }, "RendererNotShipped": { "description": "About the software renderer shipped beside the program on Windows: in the system dialog and on standard error when the window could not be opened even so, on standard error when a window starts drawing with it, and on the About screen while it does.", + "hash": "sha256-bfadaecbdd36", "other": "No software renderer ships for this system, so this flag changes nothing here." }, "RendererStartFailed": { "description": "About the software renderer shipped beside the program on Windows: in the system dialog and on standard error when the window could not be opened even so, on standard error when a window starts drawing with it, and on the About screen while it does. Carries one value, {{.Error}}, which has to stay spelled exactly that way.", + "hash": "sha256-3540e225c773", "other": "Starting the program again with the software renderer did not work: {{.Error}}." }, "RendererStartingAgain": { "description": "About the software renderer shipped beside the program on Windows: in the system dialog and on standard error when the window could not be opened even so, on standard error when a window starts drawing with it, and on the About screen while it does.", + "hash": "sha256-86c4b674d84e", "other": "The first attempt to open a window did not succeed - usually a graphics driver without OpenGL 2.1. Starting again with the software renderer shipped beside the program." }, "SectionBase": { "description": "The heading over a group of fields.", + "hash": "sha256-cbef193cc958", "other": "Build on a preset" }, "SectionCarriedBeside": { "description": "The heading over a group of fields.", + "hash": "sha256-4cc79104fe33", "other": "Shipped beside the program on Windows" }, "SectionCarriedCode": { "description": "The heading over a group of fields.", + "hash": "sha256-b0f1aba6039b", "other": "Third party code compiled in" }, "SectionCarriedFiles": { "description": "The heading over a group of fields.", + "hash": "sha256-81da952490ec", "other": "Files compiled in that are not code" }, "SectionConfiguration": { "description": "The heading over a group of fields.", + "hash": "sha256-b8c1cc7e0ce4", "other": "File configuration" }, "SectionHowToUse": { "description": "The heading over a group of fields.", + "hash": "sha256-ffa04c25b105", "other": "How to use it" }, "SectionLanguage": { "description": "The heading over a group of fields.", + "hash": "sha256-a4fe65264ef7", "other": "Language" }, "SectionLicence": { "description": "The heading over a group of fields.", + "hash": "sha256-f3ec8e880a46", "other": "Licence" }, "SectionManifestNotes": { "description": "The heading over a group of fields.", + "hash": "sha256-524730d883af", "other": "Notes for the manifest" }, "SectionOutput": { "description": "The heading over a group of fields.", + "hash": "sha256-b2439bcb8dee", "other": "Output" }, "SectionPreset": { "description": "The heading over a group of fields.", + "hash": "sha256-ed65b5dde19f", "other": "The question" }, "SectionRemembered": { "description": "The heading over a group of fields.", + "hash": "sha256-3969bfb60b8d", "other": "What this window remembers" }, "SectionSettings": { "description": "The heading over a group of fields.", + "hash": "sha256-74a883a037bc", "other": "Settings" }, "SectionSupport": { "description": "The heading over a group of fields.", + "hash": "sha256-be91940b79f4", "other": "Support" }, "SectionTool": { "description": "The heading over a group of fields.", + "hash": "sha256-ed65b5dde19f", "other": "The question" }, "SectionToolInput": { "description": "The heading over a group of fields.", + "hash": "sha256-4f8655e48218", "other": "What it works on" }, "SectionToolResult": { "description": "The heading over a group of fields.", + "hash": "sha256-6e7d50e84f47", "other": "Result" }, "SettingKey": { "description": "Shown in the window. Carries one value, {{.Key}}, which has to stay spelled exactly that way.", + "hash": "sha256-3cd70f268cbd", "other": "Written as {{.Key}} in a recipe." }, "SettingsFor": { "description": "Shown in the window. Carries one value, {{.Format}}, which has to stay spelled exactly that way.", + "hash": "sha256-234ec8849d74", "other": "Settings for {{.Format}}" }, "SizeAndBytes": { "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.Human}}, {{.Exact}}.", + "hash": "sha256-7d36ca31e3e6", "other": "{{.Human}} ({{.Exact}})" }, "SizeBetween": { "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.Least}}, {{.Most}}.", + "hash": "sha256-d5d1a79f10e9", "other": "between {{.Least}} and {{.Most}}" }, "SizeFromContents": { "description": "Shown in the window.", + "hash": "sha256-fc3aa1e2b2c3", "other": "decided by the files inside, known after Preview" }, "SizeWayBoundary": { "description": "Shown in the window.", + "hash": "sha256-6cea6d4c9fee", "other": "Around a limit" }, "SizeWayExact": { "description": "Shown in the window.", + "hash": "sha256-225cdeb34c4b", "other": "One size" }, "SizeWayRange": { "description": "Shown in the window.", + "hash": "sha256-8879adb25c7d", "other": "A range" }, "StoppedAfter": { "description": "Shown in the window. Carries one value, {{.Files}}, which has to stay spelled exactly that way.", + "hash": "sha256-f08278015581", "other": "Stopped after {{.Files}}. The manifest describes exactly those." }, "SubtitleGenerate": { "description": "The sentence under the heading at the top of a screen, saying what the screen is for.", + "hash": "sha256-e7dcd4001e28", "other": "Files of one format and one size, with a manifest that says how the system under test should react to them." }, "SubtitlePreferences": { "description": "The sentence under the heading at the top of a screen, saying what the screen is for.", + "hash": "sha256-0e310c7f4078", "other": "How this window speaks, and what it keeps between runs. Nothing here changes the files it makes." }, "SubtitlePreset": { "description": "The sentence under the heading at the top of a screen, saying what the screen is for.", + "hash": "sha256-7962df9b6a39", "other": "Ready-made sets of files, each built to answer one question about the system under test." }, "SubtitleRecipe": { "description": "The sentence under the heading at the top of a screen, saying what the screen is for.", + "hash": "sha256-784e4d30ec24", "other": "Batches of different formats and sizes, generated together in one run." }, "SubtitleTools": { "description": "The sentence under the heading at the top of a screen, saying what the screen is for.", + "hash": "sha256-53bdfa16d586", "other": "Small things to do with files you already have. Every tool here is on the command line too, as tfg tool." }, + "SystemAlreadyThere": { + "description": "Shown in the window. Carries one value, {{.Number}}, which has to stay spelled exactly that way.", + "hash": "sha256-5907673e30cf", + "other": "something is already there (system error {{.Number}})" + }, + "SystemFailure": { + "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.Path}}, {{.Reason}}.", + "hash": "sha256-1f0b47895b42", + "other": "{{.Path}}: {{.Reason}}" + }, + "SystemNoPermission": { + "description": "Shown in the window. Carries one value, {{.Number}}, which has to stay spelled exactly that way.", + "hash": "sha256-cbe9b67f544e", + "other": "the system refused permission (system error {{.Number}})" + }, + "SystemNothingThere": { + "description": "Shown in the window. Carries one value, {{.Number}}, which has to stay spelled exactly that way.", + "hash": "sha256-1d328ea28fdf", + "other": "there is nothing at that path (system error {{.Number}})" + }, + "SystemRefused": { + "description": "Shown in the window. Carries one value, {{.Number}}, which has to stay spelled exactly that way.", + "hash": "sha256-67f5a8f55d87", + "other": "the system refused it (system error {{.Number}})" + }, "TabAbout": { "description": "The name of a tab along the top of the window.", + "hash": "sha256-4efca0d10c5f", "other": "About" }, "TabOneTarget": { "description": "The name of a tab along the top of the window.", + "hash": "sha256-3daaa5676f79", "other": "Single batch" }, "TabPreferences": { "description": "The name of a tab along the top of the window.", + "hash": "sha256-66962f72a088", "other": "Preferences" }, "TabPresets": { "description": "The name of a tab along the top of the window.", + "hash": "sha256-954f93fe1c66", "other": "Presets" }, "TabRecipe": { "description": "The name of a tab along the top of the window.", + "hash": "sha256-354aa55e37ba", "other": "Several batches" }, "TabTools": { "description": "The name of a tab along the top of the window.", + "hash": "sha256-ea93d6a262ec", "other": "Tools" }, + "TimeHours": { + "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.Hours}}, {{.Minutes}}.", + "hash": "sha256-a8b56babab0f", + "other": "{{.Hours}}h{{.Minutes}}m" + }, "TimeLeft": { "description": "Shown in the window. Carries one value, {{.Roughly}}, which has to stay spelled exactly that way.", + "hash": "sha256-635fa180859f", "other": "{{.Roughly}} left" }, + "TimeMinutes": { + "description": "Shown in the window. Carries one value, {{.Minutes}}, which has to stay spelled exactly that way.", + "hash": "sha256-c41f2cfc6d02", + "other": "{{.Minutes}}m" + }, + "TimeSeconds": { + "description": "Shown in the window. Carries one value, {{.Seconds}}, which has to stay spelled exactly that way.", + "hash": "sha256-1a952843df91", + "other": "{{.Seconds}}s" + }, "TooManyFiles": { "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.Count}}, {{.Reason}}.", + "hash": "sha256-8c9c48f84a9d", "other": "this run asks for {{.Count}} files - {{.Reason}}" }, "ToolChoice": { "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.Tool}}, {{.Question}}.", + "hash": "sha256-d1b261482ceb", "other": "{{.Tool}} - {{.Question}}" }, "ToolCopied": { "description": "Shown in the window. Carries one value, {{.What}}, which has to stay spelled exactly that way.", + "hash": "sha256-a2fde6f3d590", "other": "The {{.What}} is on the clipboard." }, "ToolCopiedAll": { "description": "Shown in the window.", + "hash": "sha256-3f2e59dcb582", "other": "The whole result is on the clipboard." }, "ToolDoesNotMatch": { "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.About}}, {{.Got}}, {{.Wanted}}.", + "hash": "sha256-c31d578f1544", "other": "Does not match: the {{.About}} is {{.Got}} and {{.Wanted}} was expected." }, "ToolFlag": { "description": "Shown in the window. Carries one value, {{.Key}}, which has to stay spelled exactly that way.", + "hash": "sha256-841ea229d74b", "other": "Written as --{{.Key}} after tfg tool on the command line." }, "ToolInputWrittenAs": { "description": "Shown in the window. Carries one value, {{.Tool}}, which has to stay spelled exactly that way.", + "hash": "sha256-75e45e99f51f", "other": "Written as the path after tfg tool {{.Tool}} on the command line." }, "ToolListDoesNotMatch": { "description": "Shown in the window. Carries one value, {{.About}}, which has to stay spelled exactly that way.", + "hash": "sha256-bc5aeb5346c9", "other": "Does not match: {{.About}} lists files that are not what it says." }, "ToolListMatches": { "description": "Shown in the window. Carries one value, {{.About}}, which has to stay spelled exactly that way.", + "hash": "sha256-5a4ebb280d05", "other": "Matches: every file {{.About}} lists is what it says." }, "ToolMatches": { "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.About}}, {{.Got}}.", + "hash": "sha256-7964a2497450", "other": "Matches: the {{.About}} is {{.Got}}, as expected." }, "ToolMoreItems": { "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.More}}, {{.Tool}}.", + "hash": "sha256-16093fa01b04", "other": "And {{.More}} more. tfg tool {{.Tool}} lists them all." }, "ToolNothingYet": { "description": "Shown in the window.", + "hash": "sha256-6184988bdded", "other": "Nothing worked out yet. Choose what to work on and press Run." }, "ToolReading": { "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.Done}}, {{.Total}}.", + "hash": "sha256-d0e0b4b2b81f", "other": "Read {{.Done}} of {{.Total}}." }, "ToolResultLine": { "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.Name}}, {{.Value}}.", + "hash": "sha256-c8b9b00643c2", "other": "{{.Name}} {{.Value}}" }, "ToolStopped": { "description": "Shown in the window.", + "hash": "sha256-1da20fb189f8", "other": "Stopped before it finished. Nothing was written or changed." }, "UseSmallestSize": { "description": "Shown in the window. Carries one value, {{.Size}}, which has to stay spelled exactly that way.", + "hash": "sha256-c42521834dd8", "other": "Use the smallest size, {{.Size}}" }, "WillGoTo": { "description": "Shown in the window. Carries one value, {{.Directory}}, which has to stay spelled exactly that way.", + "hash": "sha256-88a5bdf9be0b", "other": "will go to {{.Directory}}" }, "WindowRefused": { "description": "Shown in a system dialog and on standard error when the toolkit could not open the window at all, so there is no window to show it in. Carries one value, {{.Cause}}, which has to stay spelled exactly that way.", + "hash": "sha256-1b746ceec688", "other": "The window could not be opened. It draws through OpenGL 2.1, and the graphics toolkit could not get that from the driver on this computer. The toolkit said: {{.Cause}}. Everything the window does is also on the command line - run \"tfg --help\" - and that needs no graphics driver. To get the window, use a graphics driver that provides OpenGL 2.1." }, "WindowRefusedNoCause": { "description": "Shown in a system dialog and on standard error when the toolkit could not open the window at all, so there is no window to show it in.", + "hash": "sha256-413b5a197004", "other": "The window could not be opened. It draws through OpenGL 2.1, and the graphics toolkit could not get that from the driver on this computer. Everything the window does is also on the command line - run \"tfg --help\" - and that needs no graphics driver. To get the window, use a graphics driver that provides OpenGL 2.1." }, "WindowRefusedTitle": { "description": "Shown in a system dialog and on standard error when the toolkit could not open the window at all, so there is no window to show it in.", + "hash": "sha256-304b31204244", "other": "Testing Files Generator could not open its window" }, "WorkingOutTheCost": { "description": "Shown in the window.", + "hash": "sha256-76988f639628", "other": "Working out what this would cost..." }, "WritingTo": { "description": "Shown in the window. Carries one value, {{.Directory}}, which has to stay spelled exactly that way.", + "hash": "sha256-57f16f2a6428", "other": "Files will go to {{.Directory}}" }, "Written": { "description": "Shown in the window. Carries one value, {{.Files}}, which has to stay spelled exactly that way.", + "hash": "sha256-951270031dfe", "other": "{{.Files}} written." }, "WrittenWithFailures": { "description": "Shown in the window. Carries these values, each of which has to stay spelled exactly that way: {{.Files}}, {{.Failed}}.", + "hash": "sha256-32d2d6f1006a", "other": "{{.Files}} written, {{.Failed}} could not be produced." } } diff --git a/internal/gui/text/locale/pl.json b/internal/gui/text/locale/pl.json index 8c8d85af..77425e1e 100644 --- a/internal/gui/text/locale/pl.json +++ b/internal/gui/text/locale/pl.json @@ -1,209 +1,222 @@ { - "AboutTagline": { "other": "Generuj pliki testowe i wiedz, jak system pod testem powinien na nie zareagować." }, - "AllowedDefault": { "other": "domyślnie {{.Default}}" }, - "AllowedJointly": { "other": "{{.Of}} razy {{.By}} nie może przekroczyć {{.Max}} {{.Unit}}, bo {{.Why}}" }, - "AllowedOneOf": { "other": "jedna z wartości: {{.Values}}" }, - "AllowedRange": { "other": "liczba całkowita od {{.Min}} do {{.Max}}" }, - "AllowedRangeOf": { "other": "liczba {{.Unit}} od {{.Min}} do {{.Max}}" }, - "AllowedSize": { "other": "rozmiar, na przykład 2mb, albo sama liczba bajtów" }, - "AllowedTrueOrFalse": { "other": "true albo false" }, - "AllowedWholeNumber": { "other": "liczba całkowita" }, - "AllowedWholeNumberOf": { "other": "liczba {{.Unit}}" }, - "AllowedWithDefault": { "other": "{{.What}}, domyślnie {{.Default}}" }, - "BatchHeading": { "other": "Partia {{.Number}}" }, - "ButtonAddBatch": { "other": "Dodaj partię" }, - "ButtonAddContents": { "other": "Dodaj pliki do środka" }, - "ButtonCancel": { "other": "Anuluj" }, - "ButtonChoose": { "other": "Wybierz..." }, - "ButtonCopy": { "other": "Kopiuj" }, - "ButtonCopyAll": { "other": "Kopiuj wszystko" }, - "ButtonDonate": { "other": "Wesprzyj" }, - "ButtonDuplicateBatch": { "other": "Powiel" }, - "ButtonForget": { "other": "Zapomnij" }, - "ButtonGenerate": { "other": "Generuj" }, - "ButtonOpenFolder": { "other": "Otwórz folder" }, - "ButtonOpenInstructions": { "other": "Otwórz instrukcje" }, - "ButtonOpenManifest": { "other": "Otwórz manifest" }, - "ButtonPreview": { "other": "Podgląd" }, - "ButtonRemoveBatch": { "other": "Usuń" }, - "ButtonRemoveContents": { "other": "Usuń" }, - "ButtonRestart": { "other": "Uruchom ponownie" }, - "ButtonRunTool": { "other": "Uruchom" }, - "ChoiceSameAsSystem": { "other": "Jak w systemie ({{.Language}})" }, - "ContentsHeading": { "other": "Pliki w każdym archiwum" }, - "DamageNone": { "other": "brak" }, - "DamageSettingsFor": { "other": "Ustawienia dla {{.Damage}}" }, - "DecimalMark": { "other": "," }, - "DetailBasePreset": { "other": "Ustawienia pod nim należą do presetu. Puste bierze swoją wartość domyślną, a manifest zapisuje, które tak zrobiły." }, - "DetailBoundary": { "other": "Podaj limit, który deklaruje twój system, na przykład 10mb. Jednostki liczą się po 1024, a przebieg wypisuje liczbę, której użył." }, - "DetailBuildOnPreset": { "other": "Bez zaznaczenia partie poniżej są całym przepisem. Z zaznaczeniem najpierw idą pliki wybranego presetu, a partie są dodawane po nich." }, - "DetailDamage": { "other": "Pliki mają rozmiar, o który prosisz, i żaden czytnik ich nie przyjmie - a właśnie to walidator ma odrzucić. Manifest zapisuje, co zostało zepsute, i mówi, że plik powinien zostać odrzucony." }, - "DetailDonate": { "other": "Otwiera stronę wsparcia w przeglądarce. Narzędzie jest darmowe i takie zostanie - to płaci za czas, który w nie idzie." }, - "DetailExpected": { "other": "Trafia do manifestu i nic innego go nie czyta. Zostaw puste tam, gdzie właściwa odpowiedź zależy od polityki samej aplikacji." }, - "DetailGroup": { "other": "Trafia do manifestu, więc test może sprawdzić całą klasę przypadków naraz." }, - "DetailLabel": { "other": "Zapisuje w pliku, czym jest i jaki miał mieć rozmiar. Wyłącz dla pliku, który ma zawierać wyłącznie swoją treść." }, - "DetailManifest": { "other": "Trafia do katalogu wyjściowego obok plików." }, - "DetailNameTemplate": { "other": "Puste - pliki dostają nazwę partii i numer, na przykład invoices_0001.pdf. Wypełnione - nazwa jest użyta dokładnie tak, jak ją napisano, razem z rozszerzeniem. Jedyny znacznik to {index:04}, który staje się 0001, 0002 i tak dalej, a partia z więcej niż jednym plikiem go potrzebuje." }, - "DetailOutputDir": { "other": "Zostanie utworzony, jeśli go nie ma." }, - "DetailPreset": { "other": "Zestaw jest wyliczany z odpowiedzi." }, - "DetailPurpose": { "other": "Trafia do manifestu i do instrukcji obok niego, które mówią, do czego służy każdy plik przebiegu. Nie zmienia żadnego bajtu żadnego pliku." }, - "DetailReason": { "other": "Z zamkniętej listy, żeby raport mógł grupować po powodzie. Nazywa regułę, o którą chodzi, niezależnie od wyniku - plik o bajt mniejszy od limitu powinien zostać przyjęty, a regułą w grze wciąż jest limit rozmiaru. Powód wymaga obok siebie oczekiwanego wyniku." }, - "DetailSeed": { "other": "Na każdej maszynie i w każdej wersji tego narzędzia. 0 to ziarno, którego przebieg używa, gdy nikt nie poprosi o inne. To ziarno jak każde inne, a nie prośba o losowe pliki - takich to narzędzie nigdy nie tworzy." }, - "DetailSize": { "other": "Jednostki liczą się po 1024, więc 10mb to 10485760 bajtów. Sama liczba to liczba bajtów." }, - "DetailSizeRange": { "other": "Dwa rozmiary z łącznikiem, na przykład 1kb-8kb. Każdy plik dostaje własny rozmiar wyprowadzony z ziarna, więc przebieg się powtarza." }, - "DetailSizeWay": { "other": "Jeden rozmiar daje każdemu plikowi ten sam rozmiar. Zakres losuje inny rozmiar dla każdego pliku. Wokół limitu tworzy trzy pliki: bajt poniżej limitu, dokładnie na nim i bajt powyżej." }, - "DetailTargetID": { "other": "Trafia do manifestu i do nazw plików, więc test odróżni te pliki od reszty przebiegu. Ziarna są z niej wyprowadzane, więc jej zmiana zmienia bajty." }, - "DetailTool": { "other": "To, na czym narzędzie pracuje, i jego ustawienia zmieniają się razem z wybranym narzędziem." }, - "DirectoryWithFreeSpace": { "other": "{{.Directory}} (wolne {{.Free}})" }, - "FieldBasePreset": { "other": "Preset bazowy" }, - "FieldBoundary": { "other": "Testowany limit" }, - "FieldBuildOnPreset": { "other": "Zacznij od presetu" }, - "FieldCount": { "other": "Liczba plików" }, - "FieldDamage": { "other": "Uszkodzenie" }, - "FieldExpected": { "other": "Oczekiwany wynik" }, - "FieldFormat": { "other": "Format" }, - "FieldGroup": { "other": "Rodzaj przypadku" }, - "FieldLabel": { "other": "Etykieta w każdym pliku" }, - "FieldLanguage": { "other": "Język okna" }, - "FieldManifest": { "other": "Nazwa pliku manifestu" }, - "FieldNameTemplate": { "other": "Nazwy plików" }, - "FieldOutputDir": { "other": "Katalog wyjściowy" }, - "FieldPreset": { "other": "Preset" }, - "FieldPurpose": { "other": "Cel" }, - "FieldReason": { "other": "Testowana reguła" }, - "FieldSeed": { "other": "Ziarno" }, - "FieldSize": { "other": "Rozmiar" }, - "FieldSizeRange": { "other": "Zakres rozmiarów" }, - "FieldSizeWay": { "other": "Jak podany jest rozmiar" }, - "FieldTargetID": { "other": "Nazwa partii" }, - "FieldTool": { "other": "Narzędzie" }, - "Files": { "one": "1 plik", "few": "{{.Count}} pliki", "many": "{{.Count}} plików", "other": "{{.Count}} pliku" }, - "FormatCount": { "one": "1 format", "few": "{{.Count}} formaty", "many": "{{.Count}} formatów", "other": "{{.Count}} formatu" }, - "HintBasePreset": { "other": "Jego pliki idą pierwsze." }, - "HintBoundary": { "other": "Trzy pliki: bajt poniżej limitu, dokładnie na nim i bajt powyżej." }, - "HintDamage": { "other": "Celowo psuje pliki." }, - "HintExpected": { "other": "Co system pod testem powinien zrobić z tymi plikami." }, - "HintFormat": { "other": "Jaki rodzaj pliku wytworzyć." }, - "HintGroup": { "other": "Oznacza kilka partii jako jeden rodzaj przypadku." }, - "HintManifest": { "other": "Zapis tego, co wytworzył przebieg." }, - "HintNameTemplate": { "other": "Jak nazywa się każdy plik tej partii." }, - "HintOutputDir": { "other": "Dokąd trafiają pliki i manifest." }, - "HintPreset": { "other": "Co testujesz." }, - "HintPurpose": { "other": "Czym są te pliki i dlaczego są w zestawie." }, - "HintReason": { "other": "Której reguły to dotyczy." }, - "HintSeed": { "other": "To samo ziarno daje te same bajty." }, - "HintSize": { "other": "Dokładny rozmiar każdego pliku." }, - "HintSizeExact": { "other": "Jeden rozmiar dla każdego pliku." }, - "HintSizeRange": { "other": "Inny rozmiar dla każdego pliku." }, - "HintTargetID": { "other": "Krótka nazwa tej partii." }, - "HintTool": { "other": "Czego chcesz się dowiedzieć." }, - "HowToUseChoose": { "other": "Wybierz preset albo wypełnij jedną partię na pierwszym ekranie." }, - "HowToUsePress": { "other": "Naciśnij Generuj. Pliki i manifest trafią do katalogu wyjściowego." }, - "HowToUseRead": { "other": "Skieruj swój test na manifest. Dla każdego pliku mówi, co system pod testem powinien z nim zrobić - przyjąć, odrzucić albo oczyścić - albo zapisuje wynik jako unspecified, gdy właściwa odpowiedź należy do polityki samej aplikacji." }, - "InstructionsNotSaved": { "other": "Nie udało się zapisać instrukcji do {{.Path}} ({{.Cause}}). Manifest został zapisany i zawiera te same informacje." }, - "LanguageName": { "other": "Polski" }, - "ListHeadingCount": { "other": "{{.Kind}} · {{.Count}}" }, - "ListKindArchives": { "other": "Archiwa" }, - "ListKindDocuments": { "other": "Dokumenty" }, - "ListKindPictures": { "other": "Obrazy" }, - "ListKindSound": { "other": "Dźwięk" }, - "ListKindText": { "other": "Tekst i dane" }, - "ListKindVideo": { "other": "Wideo" }, - "ListNothingMatches": { "other": "Żaden format nie pasuje - wyczyść pole, żeby zobaczyć wszystkie" }, - "ManifestNamed": { "other": "Manifest: {{.Name}}" }, - "ManifestNotSaved": { "other": "pliki zostały zapisane, a manifestu nie udało się zapisać do {{.Path}}" }, - "ManifestTooLargeToRead": { "other": "zapis tego przebiegu ma około {{.Size}}, a ta wersja czyta najwyżej {{.Limit}}, więc polecenia verify i cleanup nie zdołają go odczytać. Podziel przebieg, żeby każdy zapis dał się odczytać." }, - "NotAWholeNumber": { "other": "{{.Field}} ma wartość {{.Value}}, która nie jest liczbą całkowitą. Wpisz same cyfry, na przykład 1 albo 500" }, - "NoteBase": { "other": "Z zaznaczonym polem najpierw idą pliki wybranego presetu, a partie poniżej są dodawane po nich. Bez niego partie są całym przepisem." }, - "NoteManifestOnly": { "other": "Te pola opisują przypadek. Trafiają do manifestu i niczego nie zmieniają w plikach." }, - "NothingProduced": { "other": "Nic nie zostało wytworzone." }, - "PlaceholderFilter": { "other": "wpisz, aby zawęzić" }, - "PlaceholderLeftEmpty": { "other": "domyślnie: {{.Value}}" }, - "PlaceholderNotSet": { "other": "nie ustawiono" }, - "PlaceholderNotStated": { "other": "nie podano" }, - "PlaceholderWorkedOut": { "other": "wyliczane z rozmiaru" }, - "PreferencesForgetWhat": { "other": "Zapomnij czyści trzy rzeczy powyżej. To okno nie zachowa potem niczego przy zamknięciu, a następne otworzy się tak jak pierwsze." }, - "PreferencesForgotten": { "other": "Zapomniane. Następne okno otworzy się tak jak pierwsze." }, - "PreferencesKept": { "other": "Między uruchomieniami to okno zachowuje trzy rzeczy: katalog wyjściowy, rozmiar okna i język wybrany powyżej. Okno wyboru katalogu zachowuje też ostatni pokazany katalog. Nic nie opuszcza tego komputera." }, - "PreferencesKeptIn": { "other": "Są przechowywane w tym folderze:" }, - "PreferencesLanguageScope": { "other": "Język zmienia tylko okno, a nie pliki, które tworzy, przepisy, manifesty ani linię komend." }, - "PreferencesMissing": { "other": "Język wybrany wcześniej, {{.Tag}}, nie jest dostępny w tej wersji. Okno używa języka systemu, dopóki nie wybierzesz ponownie." }, - "PreferencesNextStart": { "other": "Okno mówi wybranym językiem od następnego otwarcia." }, - "PreferencesRestartBusy": { "other": "Trwa tworzenie plików. Uruchom ponownie, gdy się skończy." }, - "PreferencesRestartClears": { "other": "Ponowne uruchomienie zamyka to okno i otwiera je od nowa. Wszystko, co wpisano na innych zakładkach, zostanie wyczyszczone." }, - "PreferencesRestartFailed": { "other": "Nie udało się ponownie uruchomić okna: {{.Reason}}. Wybrany język jest zapisany. Otwórz program ponownie, żeby go zobaczyć." }, - "PresetCatchesHeading": { "other": "Zwykle znajduje:" }, - "PreviewNothingWritten": { "other": "jeszcze nic nie zapisano" }, - "Progress": { "other": "pliki {{.Done}}/{{.Total}} {{.BytesDone}} z {{.BytesTotal}} {{.Percent}}%" }, - "RefusedBeforeWriting": { "other": "Nic nie zostało zapisane. Sprawdź ustawienia zaznaczone powyżej." }, - "RendererDidNotHelp": { "other": "Programowy renderer dostarczony obok programu został załadowany, a biblioteka graficzna i tak nie zdołała otworzyć z nim okna." }, - "RendererDrawing": { "other": "To okno rysuje programowy renderer dostarczony obok programu (Mesa llvmpipe), a nie sterownik grafiki. Wszystko działa. Rysowanie jest wolniejsze niż ze sterownikiem obsługującym OpenGL 2.1." }, - "RendererNotBeside": { "other": "Obok programu nie ma programowego renderera z archiwum dla Windows - brakuje {{.Path}}. Przywróć folder opengl z archiwum obok tfg-gui.exe albo użyj sterownika grafiki obsługującego OpenGL 2.1." }, - "RendererNotLoaded": { "other": "Nie udało się załadować programowego renderera dostarczonego obok programu: {{.Error}}." }, - "RendererNotReadable": { "other": "Nie udało się odczytać pliku programowego renderera obok programu: {{.Error}}. Sprawdź, kto może czytać folder opengl obok tfg-gui.exe, albo użyj sterownika grafiki obsługującego OpenGL 2.1." }, - "RendererNotShipped": { "other": "Dla tego systemu nie jest dostarczany programowy renderer, więc ta flaga niczego tu nie zmienia." }, - "RendererStartFailed": { "other": "Ponowne uruchomienie programu z programowym rendererem nie powiodło się: {{.Error}}." }, - "RendererStartingAgain": { "other": "Pierwsza próba otwarcia okna nie powiodła się - zwykle to sterownik grafiki bez OpenGL 2.1. Uruchamiam ponownie z programowym rendererem dostarczonym obok programu." }, - "SectionBase": { "other": "Budowanie na presecie" }, - "SectionCarriedBeside": { "other": "Dostarczane obok programu w Windows" }, - "SectionCarriedCode": { "other": "Wkompilowany kod innych autorów" }, - "SectionCarriedFiles": { "other": "Wkompilowane pliki, które nie są kodem" }, - "SectionConfiguration": { "other": "Konfiguracja pliku" }, - "SectionHowToUse": { "other": "Jak używać" }, - "SectionLanguage": { "other": "Język" }, - "SectionLicence": { "other": "Licencja" }, - "SectionManifestNotes": { "other": "Uwagi do manifestu" }, - "SectionOutput": { "other": "Wyjście" }, - "SectionPreset": { "other": "Pytanie" }, - "SectionRemembered": { "other": "Co pamięta to okno" }, - "SectionSettings": { "other": "Ustawienia" }, - "SectionSupport": { "other": "Wsparcie" }, - "SectionTool": { "other": "Pytanie" }, - "SectionToolInput": { "other": "Na czym pracuje" }, - "SectionToolResult": { "other": "Wynik" }, - "SettingKey": { "other": "W przepisie zapisywane jako {{.Key}}." }, - "SettingsFor": { "other": "Ustawienia dla {{.Format}}" }, - "SizeAndBytes": { "other": "{{.Human}} ({{.Exact}})" }, - "SizeBetween": { "other": "od {{.Least}} do {{.Most}}" }, - "SizeFromContents": { "other": "zależy od plików w środku, znany po Podglądzie" }, - "SizeWayBoundary": { "other": "Wokół limitu" }, - "SizeWayExact": { "other": "Jeden rozmiar" }, - "SizeWayRange": { "other": "Zakres" }, - "StoppedAfter": { "other": "Zatrzymano. Zapisano: {{.Files}}. Manifest opisuje dokładnie te pliki." }, - "SubtitleGenerate": { "other": "Pliki jednego formatu i jednego rozmiaru, z manifestem, który mówi, jak system pod testem powinien na nie zareagować." }, - "SubtitlePreferences": { "other": "Jak mówi to okno i co zachowuje między uruchomieniami. Nic tutaj nie zmienia plików, które tworzy." }, - "SubtitlePreset": { "other": "Gotowe zestawy plików, każdy zbudowany, by odpowiedzieć na jedno pytanie o system pod testem." }, - "SubtitleRecipe": { "other": "Partie różnych formatów i rozmiarów, generowane razem w jednym przebiegu." }, - "SubtitleTools": { "other": "Drobne czynności na plikach, które już masz. Każde narzędzie z tej zakładki jest też w linii poleceń, jako tfg tool." }, - "TabAbout": { "other": "O programie" }, - "TabOneTarget": { "other": "Jedna partia" }, - "TabPreferences": { "other": "Preferencje" }, - "TabPresets": { "other": "Presety" }, - "TabRecipe": { "other": "Kilka partii" }, - "TabTools": { "other": "Narzędzia" }, - "TimeLeft": { "other": "zostało {{.Roughly}}" }, - "TooManyFiles": { "other": "liczba plików w tym przebiegu to {{.Count}} - {{.Reason}}" }, - "ToolChoice": { "other": "{{.Tool}} - {{.Question}}" }, - "ToolCopied": { "other": "Skopiowano do schowka: {{.What}}." }, - "ToolCopiedAll": { "other": "Skopiowano do schowka cały wynik." }, - "ToolDoesNotMatch": { "other": "Nie zgadza się: {{.About}} to {{.Got}}, a oczekiwano {{.Wanted}}." }, - "ToolFlag": { "other": "W linii poleceń zapisuje się jako --{{.Key}} po tfg tool." }, - "ToolInputWrittenAs": { "other": "W linii poleceń zapisuje się jako ścieżka po tfg tool {{.Tool}}." }, - "ToolListDoesNotMatch": { "other": "Nie zgadza się: {{.About}} wymienia pliki, które nie są takie, jak tam zapisano." }, - "ToolListMatches": { "other": "Zgadza się: każdy plik wymieniony w {{.About}} jest taki, jak tam zapisano." }, - "ToolMatches": { "other": "Zgadza się: {{.About}} to {{.Got}}, tak jak oczekiwano." }, - "ToolMoreItems": { "other": "I jeszcze {{.More}}. tfg tool {{.Tool}} wypisuje wszystkie." }, - "ToolNothingYet": { "other": "Jeszcze nic nie policzono. Wybierz, na czym narzędzie ma pracować, i naciśnij Uruchom." }, - "ToolReading": { "other": "Przeczytano {{.Done}} z {{.Total}}." }, - "ToolResultLine": { "other": "{{.Name}} {{.Value}}" }, - "ToolStopped": { "other": "Zatrzymano przed końcem. Nic nie zostało zapisane ani zmienione." }, - "UseSmallestSize": { "other": "Użyj najmniejszego rozmiaru, {{.Size}}" }, - "WillGoTo": { "other": "zapis do {{.Directory}}" }, - "WindowRefused": { "other": "Nie udało się otworzyć okna. Rysuje ono przez OpenGL 2.1, a biblioteka graficzna nie zdołała uzyskać go od sterownika na tym komputerze. Komunikat biblioteki: {{.Cause}}. Wszystko, co robi okno, jest też dostępne w linii komend - uruchom „tfg --help” - i ona nie potrzebuje sterownika grafiki. Żeby mieć okno, użyj sterownika grafiki obsługującego OpenGL 2.1." }, - "WindowRefusedNoCause": { "other": "Nie udało się otworzyć okna. Rysuje ono przez OpenGL 2.1, a biblioteka graficzna nie zdołała uzyskać go od sterownika na tym komputerze. Wszystko, co robi okno, jest też dostępne w linii komend - uruchom „tfg --help” - i ona nie potrzebuje sterownika grafiki. Żeby mieć okno, użyj sterownika grafiki obsługującego OpenGL 2.1." }, - "WindowRefusedTitle": { "other": "Testing Files Generator nie mógł otworzyć okna" }, - "WorkingOutTheCost": { "other": "Liczę, ile to będzie kosztować..." }, - "WritingTo": { "other": "Pliki trafią do {{.Directory}}" }, - "Written": { "other": "Zapisano: {{.Files}}." }, - "WrittenWithFailures": { "other": "Zapisano: {{.Files}}, nie udało się wytworzyć: {{.Failed}}." } + "AboutLicenceBinds": { "hash": "sha256-e3b0c44298fc", "other": "To tłumaczenie jest informacyjne. Wiążący jest angielski tekst powyższej informacji i licencji GNU GPL." }, + "AboutTagline": { "hash": "sha256-3677496ffba0", "other": "Generuj pliki testowe i wiedz, jak system pod testem powinien na nie zareagować." }, + "AllowedDefault": { "hash": "sha256-22ad56774302", "other": "domyślnie {{.Default}}" }, + "AllowedJointly": { "hash": "sha256-890007fb5262", "other": "{{.Of}} razy {{.By}} nie może przekroczyć {{.Max}} {{.Unit}}, bo {{.Why}}" }, + "AllowedOneOf": { "hash": "sha256-96a180377b5d", "other": "jedna z wartości: {{.Values}}" }, + "AllowedRange": { "hash": "sha256-430416192be6", "other": "liczba całkowita od {{.Min}} do {{.Max}}" }, + "AllowedRangeOf": { "hash": "sha256-f52248fe6440", "other": "liczba {{.Unit}} od {{.Min}} do {{.Max}}" }, + "AllowedSize": { "hash": "sha256-ab006a92eb28", "other": "rozmiar, na przykład 2mb, albo sama liczba bajtów" }, + "AllowedTrueOrFalse": { "hash": "sha256-ed50d4017aaf", "other": "true albo false" }, + "AllowedWholeNumber": { "hash": "sha256-24c327302769", "other": "liczba całkowita" }, + "AllowedWholeNumberOf": { "hash": "sha256-3d353f1038c7", "other": "liczba {{.Unit}}" }, + "AllowedWithDefault": { "hash": "sha256-a8076abaa30c", "other": "{{.What}}, domyślnie {{.Default}}" }, + "BatchHeading": { "hash": "sha256-f26edb187ab1", "other": "Partia {{.Number}}" }, + "ButtonAddBatch": { "hash": "sha256-cf795e51114f", "other": "Dodaj partię" }, + "ButtonAddContents": { "hash": "sha256-4b12027a5240", "other": "Dodaj pliki do środka" }, + "ButtonCancel": { "hash": "sha256-19766ed6ccb2", "other": "Anuluj" }, + "ButtonChoose": { "hash": "sha256-895ba517a993", "other": "Wybierz..." }, + "ButtonCopy": { "hash": "sha256-e21f935f11d7", "other": "Kopiuj" }, + "ButtonCopyAll": { "hash": "sha256-96e3f09a5f16", "other": "Kopiuj wszystko" }, + "ButtonDonate": { "hash": "sha256-c91ee0f2799d", "other": "Wesprzyj" }, + "ButtonDuplicateBatch": { "hash": "sha256-02cdaabfca80", "other": "Powiel" }, + "ButtonForget": { "hash": "sha256-a6bd489d9550", "other": "Zapomnij" }, + "ButtonGenerate": { "hash": "sha256-49e49bb4401e", "other": "Generuj" }, + "ButtonOpenFolder": { "hash": "sha256-6a9084025c4b", "other": "Otwórz folder" }, + "ButtonOpenInstructions": { "hash": "sha256-d5ca5d9e594b", "other": "Otwórz instrukcje" }, + "ButtonOpenManifest": { "hash": "sha256-520b7632c1e9", "other": "Otwórz manifest" }, + "ButtonPreview": { "hash": "sha256-324b134f57c7", "other": "Podgląd" }, + "ButtonRemoveBatch": { "hash": "sha256-c3812fc4acb8", "other": "Usuń" }, + "ButtonRemoveContents": { "hash": "sha256-c3812fc4acb8", "other": "Usuń" }, + "ButtonRestart": { "hash": "sha256-09af1002abaf", "other": "Uruchom ponownie" }, + "ButtonRunTool": { "hash": "sha256-00d60e31a4e6", "other": "Uruchom" }, + "ChoiceSameAsSystem": { "hash": "sha256-bb58ca050419", "other": "Jak w systemie ({{.Language}})" }, + "ContentsHeading": { "hash": "sha256-5aa71ee1325d", "other": "Pliki w każdym archiwum" }, + "DamageNone": { "hash": "sha256-140bedbf9c3f", "other": "brak" }, + "DamageSettingsFor": { "hash": "sha256-ef202c268786", "other": "Ustawienia dla {{.Damage}}" }, + "DecimalMark": { "hash": "sha256-cdb4ee2aea69", "other": "," }, + "Defect": { "hash": "sha256-2241be2ea8a6", "other": "W programie coś poszło nie tak, a nie powinno. Zgłoś to razem z tym opisem: {{.Detail}}" }, + "DetailBasePreset": { "hash": "sha256-e04b69a605dc", "other": "Ustawienia pod nim należą do presetu. Puste bierze swoją wartość domyślną, a manifest zapisuje, które tak zrobiły." }, + "DetailBoundary": { "hash": "sha256-eaf5b74b1f70", "other": "Podaj limit, który deklaruje twój system, na przykład 10mb. Jednostki liczą się po 1024, a przebieg wypisuje liczbę, której użył." }, + "DetailBuildOnPreset": { "hash": "sha256-5f88fed7a993", "other": "Bez zaznaczenia partie poniżej są całym przepisem. Z zaznaczeniem najpierw idą pliki wybranego presetu, a partie są dodawane po nich." }, + "DetailDamage": { "hash": "sha256-96921213f617", "other": "Pliki mają rozmiar, o który prosisz, i żaden czytnik ich nie przyjmie - a właśnie to walidator ma odrzucić. Manifest zapisuje, co zostało zepsute, i mówi, że plik powinien zostać odrzucony." }, + "DetailDonate": { "hash": "sha256-8531eb297aaa", "other": "Otwiera stronę wsparcia w przeglądarce. Narzędzie jest darmowe i takie zostanie - to płaci za czas, który w nie idzie." }, + "DetailExpected": { "hash": "sha256-7923af561fdd", "other": "Trafia do manifestu i nic innego go nie czyta. Zostaw puste tam, gdzie właściwa odpowiedź zależy od polityki samej aplikacji." }, + "DetailGroup": { "hash": "sha256-37e7791bbda7", "other": "Trafia do manifestu, więc test może sprawdzić całą klasę przypadków naraz." }, + "DetailLabel": { "hash": "sha256-aeb24fe1804e", "other": "Zapisuje w pliku, czym jest i jaki miał mieć rozmiar. Wyłącz dla pliku, który ma zawierać wyłącznie swoją treść." }, + "DetailManifest": { "hash": "sha256-98009d7dd5ba", "other": "Trafia do katalogu wyjściowego obok plików." }, + "DetailNameTemplate": { "hash": "sha256-3e64e80529fb", "other": "Puste - pliki dostają nazwę partii i numer, na przykład invoices_0001.pdf. Wypełnione - nazwa jest użyta dokładnie tak, jak ją napisano, razem z rozszerzeniem. Jedyny znacznik to {index:04}, który staje się 0001, 0002 i tak dalej, a partia z więcej niż jednym plikiem go potrzebuje." }, + "DetailOutputDir": { "hash": "sha256-55317cd71eaa", "other": "Zostanie utworzony, jeśli go nie ma." }, + "DetailPreset": { "hash": "sha256-9c28266cd16b", "other": "Zestaw jest wyliczany z odpowiedzi." }, + "DetailPurpose": { "hash": "sha256-d3b3309352dc", "other": "Trafia do manifestu i do instrukcji obok niego, które mówią, do czego służy każdy plik przebiegu. Nie zmienia żadnego bajtu żadnego pliku." }, + "DetailReason": { "hash": "sha256-d7f3c443d829", "other": "Z zamkniętej listy, żeby raport mógł grupować po powodzie. Nazywa regułę, o którą chodzi, niezależnie od wyniku - plik o bajt mniejszy od limitu powinien zostać przyjęty, a regułą w grze wciąż jest limit rozmiaru. Powód wymaga obok siebie oczekiwanego wyniku." }, + "DetailSeed": { "hash": "sha256-823564b2ed53", "other": "Na każdej maszynie i w każdej wersji tego narzędzia. 0 to ziarno, którego przebieg używa, gdy nikt nie poprosi o inne. To ziarno jak każde inne, a nie prośba o losowe pliki - takich to narzędzie nigdy nie tworzy." }, + "DetailSize": { "hash": "sha256-794528f6abe6", "other": "Jednostki liczą się po 1024, więc 10mb to 10485760 bajtów. Sama liczba to liczba bajtów." }, + "DetailSizeRange": { "hash": "sha256-421d366dbd3e", "other": "Dwa rozmiary z łącznikiem, na przykład 1kb-8kb. Każdy plik dostaje własny rozmiar wyprowadzony z ziarna, więc przebieg się powtarza." }, + "DetailSizeWay": { "hash": "sha256-b65673254620", "other": "Jeden rozmiar daje każdemu plikowi ten sam rozmiar. Zakres losuje inny rozmiar dla każdego pliku. Wokół limitu tworzy trzy pliki: bajt poniżej limitu, dokładnie na nim i bajt powyżej." }, + "DetailTargetID": { "hash": "sha256-1a2680483a4d", "other": "Trafia do manifestu i do nazw plików, więc test odróżni te pliki od reszty przebiegu. Ziarna są z niej wyprowadzane, więc jej zmiana zmienia bajty." }, + "DetailTool": { "hash": "sha256-1fdd99516e02", "other": "To, na czym narzędzie pracuje, i jego ustawienia zmieniają się razem z wybranym narzędziem." }, + "DirectoryWithFreeSpace": { "hash": "sha256-3e96ff18edeb", "other": "{{.Directory}} (wolne {{.Free}})" }, + "FieldBasePreset": { "hash": "sha256-6daf826260dd", "other": "Preset bazowy" }, + "FieldBoundary": { "hash": "sha256-0db0580b20bd", "other": "Testowany limit" }, + "FieldBuildOnPreset": { "hash": "sha256-765fe22ce5d4", "other": "Zacznij od presetu" }, + "FieldCount": { "hash": "sha256-5369b7db3d69", "other": "Liczba plików" }, + "FieldDamage": { "hash": "sha256-0c874f581a45", "other": "Uszkodzenie" }, + "FieldExpected": { "hash": "sha256-08159997039d", "other": "Oczekiwany wynik" }, + "FieldFormat": { "hash": "sha256-2f343666aaa8", "other": "Format" }, + "FieldGroup": { "hash": "sha256-6f87ee9cb9dc", "other": "Rodzaj przypadku" }, + "FieldLabel": { "hash": "sha256-98e8b12087c4", "other": "Etykieta w każdym pliku" }, + "FieldLanguage": { "hash": "sha256-703df606ae88", "other": "Język okna" }, + "FieldManifest": { "hash": "sha256-882e40f5910b", "other": "Nazwa pliku manifestu" }, + "FieldNameTemplate": { "hash": "sha256-6d00d6e32977", "other": "Nazwy plików" }, + "FieldOutputDir": { "hash": "sha256-c94c0d681c64", "other": "Katalog wyjściowy" }, + "FieldPreset": { "hash": "sha256-7252e7ce0085", "other": "Preset" }, + "FieldPurpose": { "hash": "sha256-d4e8830a71c7", "other": "Cel" }, + "FieldReason": { "hash": "sha256-d697fea77188", "other": "Testowana reguła" }, + "FieldSeed": { "hash": "sha256-367ef06b8fbf", "other": "Ziarno" }, + "FieldSize": { "hash": "sha256-1af851907331", "other": "Rozmiar" }, + "FieldSizeRange": { "hash": "sha256-131fb459b1a7", "other": "Zakres rozmiarów" }, + "FieldSizeWay": { "hash": "sha256-8ab998e87d55", "other": "Jak podany jest rozmiar" }, + "FieldTargetID": { "hash": "sha256-cb719956a85c", "other": "Nazwa partii" }, + "FieldTool": { "hash": "sha256-2e53bdcd0740", "other": "Narzędzie" }, + "Files": { "hash": "sha256-70212ca62bcd", "one": "1 plik", "few": "{{.Count}} pliki", "many": "{{.Count}} plików", "other": "{{.Count}} pliku" }, + "FormatCount": { "hash": "sha256-12de0241f4a5", "one": "1 format", "few": "{{.Count}} formaty", "many": "{{.Count}} formatów", "other": "{{.Count}} formatu" }, + "HintBasePreset": { "hash": "sha256-d75c8b442fd9", "other": "Jego pliki idą pierwsze." }, + "HintBoundary": { "hash": "sha256-3ef02abcdddf", "other": "Trzy pliki: bajt poniżej limitu, dokładnie na nim i bajt powyżej." }, + "HintDamage": { "hash": "sha256-67e5c4c4940a", "other": "Celowo psuje pliki." }, + "HintExpected": { "hash": "sha256-a1b4f34d2e0e", "other": "Co system pod testem powinien zrobić z tymi plikami." }, + "HintFormat": { "hash": "sha256-f5616fe5a139", "other": "Jaki rodzaj pliku wytworzyć." }, + "HintGroup": { "hash": "sha256-e95a20274902", "other": "Oznacza kilka partii jako jeden rodzaj przypadku." }, + "HintManifest": { "hash": "sha256-986221f924c6", "other": "Zapis tego, co wytworzył przebieg." }, + "HintNameTemplate": { "hash": "sha256-c92ff0a07510", "other": "Jak nazywa się każdy plik tej partii." }, + "HintOutputDir": { "hash": "sha256-35dfac65eb8a", "other": "Dokąd trafiają pliki i manifest." }, + "HintPreset": { "hash": "sha256-e86685eae6f6", "other": "Co testujesz." }, + "HintPurpose": { "hash": "sha256-ff421b5abcc8", "other": "Czym są te pliki i dlaczego są w zestawie." }, + "HintReason": { "hash": "sha256-6307e1666cea", "other": "Której reguły to dotyczy." }, + "HintSeed": { "hash": "sha256-5cd4cde003a4", "other": "To samo ziarno daje te same bajty." }, + "HintSize": { "hash": "sha256-70aad1580c12", "other": "Dokładny rozmiar każdego pliku." }, + "HintSizeExact": { "hash": "sha256-ee94fadda67a", "other": "Jeden rozmiar dla każdego pliku." }, + "HintSizeRange": { "hash": "sha256-a7f91764d39d", "other": "Inny rozmiar dla każdego pliku." }, + "HintTargetID": { "hash": "sha256-16b346acd09e", "other": "Krótka nazwa tej partii." }, + "HintTool": { "hash": "sha256-6a3014a90ca2", "other": "Czego chcesz się dowiedzieć." }, + "HowToUseChoose": { "hash": "sha256-d3826a08883e", "other": "Wybierz preset albo wypełnij jedną partię na pierwszym ekranie." }, + "HowToUsePress": { "hash": "sha256-efe55efa1794", "other": "Naciśnij Generuj. Pliki i manifest trafią do katalogu wyjściowego." }, + "HowToUseRead": { "hash": "sha256-ef0d5f7632c8", "other": "Skieruj swój test na manifest. Dla każdego pliku mówi, co system pod testem powinien z nim zrobić - przyjąć, odrzucić albo oczyścić - albo zapisuje wynik jako unspecified, gdy właściwa odpowiedź należy do polityki samej aplikacji." }, + "InstructionsNotSaved": { "hash": "sha256-bfb2e6c568e3", "other": "Nie udało się zapisać instrukcji do {{.Path}} ({{.Cause}}). Manifest został zapisany i zawiera te same informacje." }, + "LanguageName": { "hash": "sha256-ba118bf7fc9c", "other": "Polski" }, + "ListHeadingCount": { "hash": "sha256-058722bef091", "other": "{{.Kind}} · {{.Count}}" }, + "ListJoinedAnd": { "hash": "sha256-40953da1c535", "other": "{{.Head}} i {{.Last}}" }, + "ListJoinedOr": { "hash": "sha256-c9be11158971", "other": "{{.Head}} albo {{.Last}}" }, + "ListKindArchives": { "hash": "sha256-e404aa80d8df", "other": "Archiwa" }, + "ListKindDocuments": { "hash": "sha256-b4e929d8bcfe", "other": "Dokumenty" }, + "ListKindPictures": { "hash": "sha256-1e6245009d67", "other": "Obrazy" }, + "ListKindSound": { "hash": "sha256-46124e023603", "other": "Dźwięk" }, + "ListKindText": { "hash": "sha256-c04c0f52d26a", "other": "Tekst i dane" }, + "ListKindVideo": { "hash": "sha256-d534be829e32", "other": "Wideo" }, + "ListNothingMatches": { "hash": "sha256-7b2c3b9853ac", "other": "Żaden format nie pasuje - wyczyść pole, żeby zobaczyć wszystkie" }, + "ManifestNamed": { "hash": "sha256-f9540bb6e465", "other": "Manifest: {{.Name}}" }, + "ManifestNotSaved": { "hash": "sha256-bb722052cb9e", "other": "pliki zostały zapisane, a manifestu nie udało się zapisać do {{.Path}}" }, + "ManifestTooLargeToRead": { "hash": "sha256-0b5d14c5fc58", "other": "zapis tego przebiegu ma około {{.Size}}, a ta wersja czyta najwyżej {{.Limit}}, więc polecenia verify i cleanup nie zdołają go odczytać. Podziel przebieg, żeby każdy zapis dał się odczytać." }, + "NotAWholeNumber": { "hash": "sha256-2537ac57d3c7", "other": "{{.Field}} ma wartość {{.Value}}, która nie jest liczbą całkowitą. Wpisz same cyfry, na przykład 1 albo 500" }, + "NoteBase": { "hash": "sha256-e48510060140", "other": "Z zaznaczonym polem najpierw idą pliki wybranego presetu, a partie poniżej są dodawane po nich. Bez niego partie są całym przepisem." }, + "NoteManifestOnly": { "hash": "sha256-794d5d9e2043", "other": "Te pola opisują przypadek. Trafiają do manifestu i niczego nie zmieniają w plikach." }, + "NothingProduced": { "hash": "sha256-7d90c4a68d2c", "other": "Nic nie zostało wytworzone." }, + "PlaceholderChooseOne": { "hash": "sha256-fdfe6bf17ea5", "other": "(wybierz)" }, + "PlaceholderFilter": { "hash": "sha256-7647095cb6a8", "other": "wpisz, aby zawęzić" }, + "PlaceholderLeftEmpty": { "hash": "sha256-aaa21a9c2770", "other": "domyślnie: {{.Value}}" }, + "PlaceholderNotSet": { "hash": "sha256-1aef93991721", "other": "nie ustawiono" }, + "PlaceholderNotStated": { "hash": "sha256-109df22c5a75", "other": "nie podano" }, + "PlaceholderWorkedOut": { "hash": "sha256-195191b2dc3a", "other": "wyliczane z rozmiaru" }, + "PreferencesForgetWhat": { "hash": "sha256-f7c12f863d64", "other": "Zapomnij czyści trzy rzeczy powyżej. To okno nie zachowa potem niczego przy zamknięciu, a następne otworzy się tak jak pierwsze." }, + "PreferencesForgotten": { "hash": "sha256-8e950dfcda31", "other": "Zapomniane. Następne okno otworzy się tak jak pierwsze." }, + "PreferencesKept": { "hash": "sha256-896c43e06cdb", "other": "Między uruchomieniami to okno zachowuje trzy rzeczy: katalog wyjściowy, rozmiar okna i język wybrany powyżej. Okno wyboru katalogu zachowuje też ostatni pokazany katalog. Nic nie opuszcza tego komputera." }, + "PreferencesKeptIn": { "hash": "sha256-65ceb0c15ff4", "other": "Są przechowywane w tym folderze:" }, + "PreferencesLanguageScope": { "hash": "sha256-bc4174717ca7", "other": "Język zmienia tylko okno, a nie pliki, które tworzy, przepisy, manifesty ani linię komend." }, + "PreferencesMissing": { "hash": "sha256-7679a7684a73", "other": "Język wybrany wcześniej, {{.Tag}}, nie jest dostępny w tej wersji. Okno używa języka systemu, dopóki nie wybierzesz ponownie." }, + "PreferencesNextStart": { "hash": "sha256-6fee24596599", "other": "Okno mówi wybranym językiem od następnego otwarcia." }, + "PreferencesRestartBusy": { "hash": "sha256-1d940383b3e9", "other": "Trwa tworzenie plików. Uruchom ponownie, gdy się skończy." }, + "PreferencesRestartClears": { "hash": "sha256-29776eba6622", "other": "Ponowne uruchomienie zamyka to okno i otwiera je od nowa. Wszystko, co wpisano na innych zakładkach, zostanie wyczyszczone." }, + "PreferencesRestartFailed": { "hash": "sha256-8d92a5a49edb", "other": "Nie udało się ponownie uruchomić okna: {{.Reason}}. Wybrany język jest zapisany. Otwórz program ponownie, żeby go zobaczyć." }, + "PresetCatchesHeading": { "hash": "sha256-110703eac5e9", "other": "Zwykle znajduje:" }, + "PreviewNothingWritten": { "hash": "sha256-9aea7a90a504", "other": "jeszcze nic nie zapisano" }, + "Progress": { "hash": "sha256-a9de146c2bba", "other": "pliki {{.Done}}/{{.Total}} {{.BytesDone}} z {{.BytesTotal}} {{.Percent}}%" }, + "RefusedBeforeWriting": { "hash": "sha256-531c1a087f5f", "other": "Nic nie zostało zapisane. Sprawdź ustawienia zaznaczone powyżej." }, + "RendererDidNotHelp": { "hash": "sha256-cb20cd3fbbee", "other": "Programowy renderer dostarczony obok programu został załadowany, a biblioteka graficzna i tak nie zdołała otworzyć z nim okna." }, + "RendererDrawing": { "hash": "sha256-b94c25a817e3", "other": "To okno rysuje programowy renderer dostarczony obok programu (Mesa llvmpipe), a nie sterownik grafiki. Wszystko działa. Rysowanie jest wolniejsze niż ze sterownikiem obsługującym OpenGL 2.1." }, + "RendererNotBeside": { "hash": "sha256-b0e260ef6617", "other": "Obok programu nie ma programowego renderera z archiwum dla Windows - brakuje {{.Path}}. Przywróć folder opengl z archiwum obok tfg-gui.exe albo użyj sterownika grafiki obsługującego OpenGL 2.1." }, + "RendererNotLoaded": { "hash": "sha256-55832142c3eb", "other": "Nie udało się załadować programowego renderera dostarczonego obok programu: {{.Error}}." }, + "RendererNotReadable": { "hash": "sha256-453a6da2bea7", "other": "Nie udało się odczytać pliku programowego renderera obok programu: {{.Error}}. Sprawdź, kto może czytać folder opengl obok tfg-gui.exe, albo użyj sterownika grafiki obsługującego OpenGL 2.1." }, + "RendererNotShipped": { "hash": "sha256-bfadaecbdd36", "other": "Dla tego systemu nie jest dostarczany programowy renderer, więc ta flaga niczego tu nie zmienia." }, + "RendererStartFailed": { "hash": "sha256-3540e225c773", "other": "Ponowne uruchomienie programu z programowym rendererem nie powiodło się: {{.Error}}." }, + "RendererStartingAgain": { "hash": "sha256-86c4b674d84e", "other": "Pierwsza próba otwarcia okna nie powiodła się - zwykle to sterownik grafiki bez OpenGL 2.1. Uruchamiam ponownie z programowym rendererem dostarczonym obok programu." }, + "SectionBase": { "hash": "sha256-cbef193cc958", "other": "Budowanie na presecie" }, + "SectionCarriedBeside": { "hash": "sha256-4cc79104fe33", "other": "Dostarczane obok programu w Windows" }, + "SectionCarriedCode": { "hash": "sha256-b0f1aba6039b", "other": "Wkompilowany kod innych autorów" }, + "SectionCarriedFiles": { "hash": "sha256-81da952490ec", "other": "Wkompilowane pliki, które nie są kodem" }, + "SectionConfiguration": { "hash": "sha256-b8c1cc7e0ce4", "other": "Konfiguracja pliku" }, + "SectionHowToUse": { "hash": "sha256-ffa04c25b105", "other": "Jak używać" }, + "SectionLanguage": { "hash": "sha256-a4fe65264ef7", "other": "Język" }, + "SectionLicence": { "hash": "sha256-f3ec8e880a46", "other": "Licencja" }, + "SectionManifestNotes": { "hash": "sha256-524730d883af", "other": "Uwagi do manifestu" }, + "SectionOutput": { "hash": "sha256-b2439bcb8dee", "other": "Wyjście" }, + "SectionPreset": { "hash": "sha256-ed65b5dde19f", "other": "Pytanie" }, + "SectionRemembered": { "hash": "sha256-3969bfb60b8d", "other": "Co pamięta to okno" }, + "SectionSettings": { "hash": "sha256-74a883a037bc", "other": "Ustawienia" }, + "SectionSupport": { "hash": "sha256-be91940b79f4", "other": "Wsparcie" }, + "SectionTool": { "hash": "sha256-ed65b5dde19f", "other": "Pytanie" }, + "SectionToolInput": { "hash": "sha256-4f8655e48218", "other": "Na czym pracuje" }, + "SectionToolResult": { "hash": "sha256-6e7d50e84f47", "other": "Wynik" }, + "SettingKey": { "hash": "sha256-3cd70f268cbd", "other": "W przepisie zapisywane jako {{.Key}}." }, + "SettingsFor": { "hash": "sha256-234ec8849d74", "other": "Ustawienia dla {{.Format}}" }, + "SizeAndBytes": { "hash": "sha256-7d36ca31e3e6", "other": "{{.Human}} ({{.Exact}})" }, + "SizeBetween": { "hash": "sha256-d5d1a79f10e9", "other": "od {{.Least}} do {{.Most}}" }, + "SizeFromContents": { "hash": "sha256-fc3aa1e2b2c3", "other": "zależy od plików w środku, znany po Podglądzie" }, + "SizeWayBoundary": { "hash": "sha256-6cea6d4c9fee", "other": "Wokół limitu" }, + "SizeWayExact": { "hash": "sha256-225cdeb34c4b", "other": "Jeden rozmiar" }, + "SizeWayRange": { "hash": "sha256-8879adb25c7d", "other": "Zakres" }, + "StoppedAfter": { "hash": "sha256-f08278015581", "other": "Zatrzymano. Zapisano: {{.Files}}. Manifest opisuje dokładnie te pliki." }, + "SubtitleGenerate": { "hash": "sha256-e7dcd4001e28", "other": "Pliki jednego formatu i jednego rozmiaru, z manifestem, który mówi, jak system pod testem powinien na nie zareagować." }, + "SubtitlePreferences": { "hash": "sha256-0e310c7f4078", "other": "Jak mówi to okno i co zachowuje między uruchomieniami. Nic tutaj nie zmienia plików, które tworzy." }, + "SubtitlePreset": { "hash": "sha256-7962df9b6a39", "other": "Gotowe zestawy plików, każdy zbudowany, by odpowiedzieć na jedno pytanie o system pod testem." }, + "SubtitleRecipe": { "hash": "sha256-784e4d30ec24", "other": "Partie różnych formatów i rozmiarów, generowane razem w jednym przebiegu." }, + "SubtitleTools": { "hash": "sha256-53bdfa16d586", "other": "Drobne czynności na plikach, które już masz. Każde narzędzie z tej zakładki jest też w linii poleceń, jako tfg tool." }, + "SystemAlreadyThere": { "hash": "sha256-5907673e30cf", "other": "coś już tam jest (błąd systemu {{.Number}})" }, + "SystemFailure": { "hash": "sha256-1f0b47895b42", "other": "{{.Path}}: {{.Reason}}" }, + "SystemNoPermission": { "hash": "sha256-cbe9b67f544e", "other": "system odmówił dostępu (błąd systemu {{.Number}})" }, + "SystemNothingThere": { "hash": "sha256-1d328ea28fdf", "other": "pod tą ścieżką nic nie ma (błąd systemu {{.Number}})" }, + "SystemRefused": { "hash": "sha256-67f5a8f55d87", "other": "system odmówił (błąd systemu {{.Number}})" }, + "TabAbout": { "hash": "sha256-4efca0d10c5f", "other": "O programie" }, + "TabOneTarget": { "hash": "sha256-3daaa5676f79", "other": "Jedna partia" }, + "TabPreferences": { "hash": "sha256-66962f72a088", "other": "Preferencje" }, + "TabPresets": { "hash": "sha256-954f93fe1c66", "other": "Presety" }, + "TabRecipe": { "hash": "sha256-354aa55e37ba", "other": "Kilka partii" }, + "TabTools": { "hash": "sha256-ea93d6a262ec", "other": "Narzędzia" }, + "TimeHours": { "hash": "sha256-a8b56babab0f", "other": "{{.Hours}} h {{.Minutes}} min" }, + "TimeLeft": { "hash": "sha256-635fa180859f", "other": "zostało {{.Roughly}}" }, + "TimeMinutes": { "hash": "sha256-c41f2cfc6d02", "other": "{{.Minutes}} min" }, + "TimeSeconds": { "hash": "sha256-1a952843df91", "other": "{{.Seconds}} s" }, + "TooManyFiles": { "hash": "sha256-8c9c48f84a9d", "other": "liczba plików w tym przebiegu to {{.Count}} - {{.Reason}}" }, + "ToolChoice": { "hash": "sha256-d1b261482ceb", "other": "{{.Tool}} - {{.Question}}" }, + "ToolCopied": { "hash": "sha256-a2fde6f3d590", "other": "Skopiowano do schowka: {{.What}}." }, + "ToolCopiedAll": { "hash": "sha256-3f2e59dcb582", "other": "Skopiowano do schowka cały wynik." }, + "ToolDoesNotMatch": { "hash": "sha256-c31d578f1544", "other": "Nie zgadza się: {{.About}} to {{.Got}}, a oczekiwano {{.Wanted}}." }, + "ToolFlag": { "hash": "sha256-841ea229d74b", "other": "W linii poleceń zapisuje się jako --{{.Key}} po tfg tool." }, + "ToolInputWrittenAs": { "hash": "sha256-75e45e99f51f", "other": "W linii poleceń zapisuje się jako ścieżka po tfg tool {{.Tool}}." }, + "ToolListDoesNotMatch": { "hash": "sha256-bc5aeb5346c9", "other": "Nie zgadza się: {{.About}} wymienia pliki, które nie są takie, jak tam zapisano." }, + "ToolListMatches": { "hash": "sha256-5a4ebb280d05", "other": "Zgadza się: każdy plik wymieniony w {{.About}} jest taki, jak tam zapisano." }, + "ToolMatches": { "hash": "sha256-7964a2497450", "other": "Zgadza się: {{.About}} to {{.Got}}, tak jak oczekiwano." }, + "ToolMoreItems": { "hash": "sha256-16093fa01b04", "other": "I jeszcze {{.More}}. tfg tool {{.Tool}} wypisuje wszystkie." }, + "ToolNothingYet": { "hash": "sha256-6184988bdded", "other": "Jeszcze nic nie policzono. Wybierz, na czym narzędzie ma pracować, i naciśnij Uruchom." }, + "ToolReading": { "hash": "sha256-d0e0b4b2b81f", "other": "Przeczytano {{.Done}} z {{.Total}}." }, + "ToolResultLine": { "hash": "sha256-c8b9b00643c2", "other": "{{.Name}} {{.Value}}" }, + "ToolStopped": { "hash": "sha256-1da20fb189f8", "other": "Zatrzymano przed końcem. Nic nie zostało zapisane ani zmienione." }, + "UseSmallestSize": { "hash": "sha256-c42521834dd8", "other": "Użyj najmniejszego rozmiaru, {{.Size}}" }, + "WillGoTo": { "hash": "sha256-88a5bdf9be0b", "other": "zapis do {{.Directory}}" }, + "WindowRefused": { "hash": "sha256-1b746ceec688", "other": "Nie udało się otworzyć okna. Rysuje ono przez OpenGL 2.1, a biblioteka graficzna nie zdołała uzyskać go od sterownika na tym komputerze. Komunikat biblioteki: {{.Cause}}. Wszystko, co robi okno, jest też dostępne w linii komend - uruchom „tfg --help” - i ona nie potrzebuje sterownika grafiki. Żeby mieć okno, użyj sterownika grafiki obsługującego OpenGL 2.1." }, + "WindowRefusedNoCause": { "hash": "sha256-413b5a197004", "other": "Nie udało się otworzyć okna. Rysuje ono przez OpenGL 2.1, a biblioteka graficzna nie zdołała uzyskać go od sterownika na tym komputerze. Wszystko, co robi okno, jest też dostępne w linii komend - uruchom „tfg --help” - i ona nie potrzebuje sterownika grafiki. Żeby mieć okno, użyj sterownika grafiki obsługującego OpenGL 2.1." }, + "WindowRefusedTitle": { "hash": "sha256-304b31204244", "other": "Testing Files Generator nie mógł otworzyć okna" }, + "WorkingOutTheCost": { "hash": "sha256-76988f639628", "other": "Liczę, ile to będzie kosztować..." }, + "WritingTo": { "hash": "sha256-57f16f2a6428", "other": "Pliki trafią do {{.Directory}}" }, + "Written": { "hash": "sha256-951270031dfe", "other": "Zapisano: {{.Files}}." }, + "WrittenWithFailures": { "hash": "sha256-32d2d6f1006a", "other": "Zapisano: {{.Files}}, nie udało się wytworzyć: {{.Failed}}." } } diff --git a/internal/gui/text/locale/registry/en.json b/internal/gui/text/locale/registry/en.json index a3d2149e..7418c30f 100644 --- a/internal/gui/text/locale/registry/en.json +++ b/internal/gui/text/locale/registry/en.json @@ -124,6 +124,696 @@ "hash": "sha256-b918ae8fc864", "other": "a name with spaces, brackets or characters outside ASCII written to disk unchanged" }, + "Choice.algorithm.md5": { + "description": "One entry of the algorithm list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-3ebff31b62c0", + "other": "md5" + }, + "Choice.algorithm.sha1": { + "description": "One entry of the algorithm list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-b1565820a5cd", + "other": "sha1" + }, + "Choice.algorithm.sha256": { + "description": "One entry of the algorithm list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-5d5b09f6dcb2", + "other": "sha256" + }, + "Choice.algorithm.sha512": { + "description": "One entry of the algorithm list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-a396244643b5", + "other": "sha512" + }, + "Choice.bit_depth.16": { + "description": "One entry of the bit_depth list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-b17ef6d19c7a", + "other": "16" + }, + "Choice.bit_depth.24": { + "description": "One entry of the bit_depth list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-c2356069e9d1", + "other": "24" + }, + "Choice.bit_depth.32": { + "description": "One entry of the bit_depth list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-e29c9c180c62", + "other": "32" + }, + "Choice.bit_depth.8": { + "description": "One entry of the bit_depth list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-2c624232cdd2", + "other": "8" + }, + "Choice.compression.best": { + "description": "One entry of the compression list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-8f4375ec65c2", + "other": "best" + }, + "Choice.compression.default": { + "description": "One entry of the compression list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-37a8eec1ce19", + "other": "default" + }, + "Choice.compression.fast": { + "description": "One entry of the compression list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-115dc3606fbf", + "other": "fast" + }, + "Choice.compression.none": { + "description": "One entry of the compression list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-140bedbf9c3f", + "other": "none" + }, + "Choice.content.noise": { + "description": "One entry of the content list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-efe2a5c09e6d", + "other": "noise" + }, + "Choice.content.silence": { + "description": "One entry of the content list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-e6c18fdbe597", + "other": "silence" + }, + "Choice.content.sweep": { + "description": "One entry of the content list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-dfd33796972c", + "other": "sweep" + }, + "Choice.content.tone": { + "description": "One entry of the content list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-00cf4ce3935c", + "other": "tone" + }, + "Choice.delimiter.comma": { + "description": "One entry of the delimiter list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-f36a4e586705", + "other": "comma" + }, + "Choice.delimiter.pipe": { + "description": "One entry of the delimiter list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-400829473916", + "other": "pipe" + }, + "Choice.delimiter.semicolon": { + "description": "One entry of the delimiter list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-1e69fcfadd9d", + "other": "semicolon" + }, + "Choice.delimiter.tab": { + "description": "One entry of the delimiter list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-7508386a2056", + "other": "tab" + }, + "Choice.embed.bmp": { + "description": "One entry of the embed list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-7685a5cb2b57", + "other": "bmp" + }, + "Choice.embed.png": { + "description": "One entry of the embed list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-8f8cbb7dcf46", + "other": "png" + }, + "Choice.encoding.utf-16be": { + "description": "One entry of the encoding list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-43b2bae67086", + "other": "utf-16be" + }, + "Choice.encoding.utf-16le": { + "description": "One entry of the encoding list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-4ffbae688018", + "other": "utf-16le" + }, + "Choice.encoding.utf-8": { + "description": "One entry of the encoding list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-5cf1fa8d836a", + "other": "utf-8" + }, + "Choice.encryption.aes-128": { + "description": "One entry of the encryption list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-8cf9659cdea5", + "other": "aes-128" + }, + "Choice.encryption.aes-192": { + "description": "One entry of the encryption list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-5498c4450db4", + "other": "aes-192" + }, + "Choice.encryption.aes-256": { + "description": "One entry of the encryption list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-7237b089a3fc", + "other": "aes-256" + }, + "Choice.encryption.none": { + "description": "One entry of the encryption list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-140bedbf9c3f", + "other": "none" + }, + "Choice.encryption.zipcrypto": { + "description": "One entry of the encryption list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-42ea6e5dc44a", + "other": "zipcrypto" + }, + "Choice.entry_format.apache-combined": { + "description": "One entry of the entry_format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-eb5679006aba", + "other": "apache-combined" + }, + "Choice.entry_format.apache-common": { + "description": "One entry of the entry_format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-5385bfe0ab10", + "other": "apache-common" + }, + "Choice.entry_format.json-lines": { + "description": "One entry of the entry_format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-9e26c12b9d04", + "other": "json-lines" + }, + "Choice.entry_format.nginx": { + "description": "One entry of the entry_format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-5be1ecc7935f", + "other": "nginx" + }, + "Choice.entry_format.plain": { + "description": "One entry of the entry_format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-a116c9ed46d6", + "other": "plain" + }, + "Choice.entry_format.syslog": { + "description": "One entry of the entry_format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-4d75f832ea26", + "other": "syslog" + }, + "Choice.entry_mode.000": { + "description": "One entry of the entry_mode list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-2ac9a6746aca", + "other": "000" + }, + "Choice.entry_mode.400": { + "description": "One entry of the entry_mode list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-26d228663f13", + "other": "400" + }, + "Choice.entry_mode.444": { + "description": "One entry of the entry_mode list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-3538a1ef2e11", + "other": "444" + }, + "Choice.entry_mode.600": { + "description": "One entry of the entry_mode list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-284b7e6d788f", + "other": "600" + }, + "Choice.entry_mode.644": { + "description": "One entry of the entry_mode list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-87e50b287059", + "other": "644" + }, + "Choice.entry_mode.664": { + "description": "One entry of the entry_mode list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-09eac95eb995", + "other": "664" + }, + "Choice.entry_mode.666": { + "description": "One entry of the entry_mode list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-c7e616822f36", + "other": "666" + }, + "Choice.entry_mode.700": { + "description": "One entry of the entry_mode list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-99ee50221221", + "other": "700" + }, + "Choice.entry_mode.755": { + "description": "One entry of the entry_mode list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-86ab8cbe5869", + "other": "755" + }, + "Choice.entry_mode.777": { + "description": "One entry of the entry_mode list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-eaf89db71084", + "other": "777" + }, + "Choice.entry_owner.root": { + "description": "One entry of the entry_owner list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-4813494d137e", + "other": "root" + }, + "Choice.entry_owner.unset": { + "description": "One entry of the entry_owner list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-6cbf83e08093", + "other": "unset" + }, + "Choice.entry_owner.user": { + "description": "One entry of the entry_owner list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-04f8996da763", + "other": "user" + }, + "Choice.expected.accept": { + "description": "One entry of the list of what the system under test should do with a file. The manifest writes the value itself.", + "hash": "sha256-c125d0397b60", + "other": "accept" + }, + "Choice.expected.reason.content_malformed": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-14484a3d8ff7", + "other": "content_malformed" + }, + "Choice.expected.reason.count_limit": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-636449b3d71c", + "other": "count_limit" + }, + "Choice.expected.reason.dimensions_limit": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-539dd060c765", + "other": "dimensions_limit" + }, + "Choice.expected.reason.duplicate": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-e24a5a32c9b8", + "other": "duplicate" + }, + "Choice.expected.reason.encoding_invalid": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-1ed4a2806f5f", + "other": "encoding_invalid" + }, + "Choice.expected.reason.extension_rule": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-87ad72d63a7f", + "other": "extension_rule" + }, + "Choice.expected.reason.filename_invalid": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-816b1c0c80ea", + "other": "filename_invalid" + }, + "Choice.expected.reason.filename_too_long": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-394b764c42c4", + "other": "filename_too_long" + }, + "Choice.expected.reason.filename_traversal": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-51dbdb34a500", + "other": "filename_traversal" + }, + "Choice.expected.reason.malware_signature": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-adb5581b7ea9", + "other": "malware_signature" + }, + "Choice.expected.reason.mime_mismatch": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-a1c2c3069328", + "other": "mime_mismatch" + }, + "Choice.expected.reason.nesting_depth": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-ddc6be14fb52", + "other": "nesting_depth" + }, + "Choice.expected.reason.none": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-140bedbf9c3f", + "other": "none" + }, + "Choice.expected.reason.size_limit": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-7fba5810a3a2", + "other": "size_limit" + }, + "Choice.expected.reason.size_zero": { + "description": "One entry of the list of the rule a file tests. The manifest writes the value itself.", + "hash": "sha256-2ccd5d11434a", + "other": "size_zero" + }, + "Choice.expected.reject": { + "description": "One entry of the list of what the system under test should do with a file. The manifest writes the value itself.", + "hash": "sha256-084e990a49ba", + "other": "reject" + }, + "Choice.expected.sanitize": { + "description": "One entry of the list of what the system under test should do with a file. The manifest writes the value itself.", + "hash": "sha256-af6a3bb0c72f", + "other": "sanitize" + }, + "Choice.expected.unspecified": { + "description": "One entry of the list of what the system under test should do with a file. The manifest writes the value itself.", + "hash": "sha256-f416648f8086", + "other": "unspecified" + }, + "Choice.far-over.10x": { + "description": "One entry of the far-over list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-687f5e475cbd", + "other": "10x" + }, + "Choice.far-over.2x": { + "description": "One entry of the far-over list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-b595b214b5d4", + "other": "2x" + }, + "Choice.far-over.off": { + "description": "One entry of the far-over list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-b4dc66dde806", + "other": "off" + }, + "Choice.format.avif": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-2f47c57aae31", + "other": "avif" + }, + "Choice.format.bmp": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-7685a5cb2b57", + "other": "bmp" + }, + "Choice.format.csv": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-53bb9b0b38ac", + "other": "csv" + }, + "Choice.format.docx": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-584cb925e6ad", + "other": "docx" + }, + "Choice.format.gif": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-3b34c6906f7e", + "other": "gif" + }, + "Choice.format.html": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-e5e239560bc1", + "other": "html" + }, + "Choice.format.ico": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-c51052ef06b6", + "other": "ico" + }, + "Choice.format.jpg": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-f8146b6cb496", + "other": "jpg" + }, + "Choice.format.json": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-02bd175f3297", + "other": "json" + }, + "Choice.format.jxl": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-87953914d2ad", + "other": "jxl" + }, + "Choice.format.log": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-836ff184e7b4", + "other": "log" + }, + "Choice.format.md": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-21262a3cb533", + "other": "md" + }, + "Choice.format.pdf": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-c35b21d6ca39", + "other": "pdf" + }, + "Choice.format.png": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-8f8cbb7dcf46", + "other": "png" + }, + "Choice.format.pptx": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-1512e97a4b84", + "other": "pptx" + }, + "Choice.format.svg": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-acdb1373d176", + "other": "svg" + }, + "Choice.format.targz": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-fc53fb509109", + "other": "targz" + }, + "Choice.format.tiff": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-904749a4869b", + "other": "tiff" + }, + "Choice.format.toml": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-bf0a90221595", + "other": "toml" + }, + "Choice.format.txt": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-486b9a6fc855", + "other": "txt" + }, + "Choice.format.wav": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-61e25c2cdda1", + "other": "wav" + }, + "Choice.format.webp": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-a57bb082e728", + "other": "webp" + }, + "Choice.format.xlsx": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-1c980b22bca3", + "other": "xlsx" + }, + "Choice.format.xml": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-bf92a04e60b4", + "other": "xml" + }, + "Choice.format.yaml": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-9831daaaa0a9", + "other": "yaml" + }, + "Choice.format.zip": { + "description": "One entry of the format list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-4a70fe9aa643", + "other": "zip" + }, + "Choice.formatting.indented": { + "description": "One entry of the formatting list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-a258f543b0a6", + "other": "indented" + }, + "Choice.formatting.minified": { + "description": "One entry of the formatting list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-1ddd424e287f", + "other": "minified" + }, + "Choice.formatting.record-per-line": { + "description": "One entry of the formatting list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-6f0d86374d60", + "other": "record-per-line" + }, + "Choice.ip_version.mixed": { + "description": "One entry of the ip_version list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-3f8fee624f43", + "other": "mixed" + }, + "Choice.ip_version.v4": { + "description": "One entry of the ip_version list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-8e38a1ea5c68", + "other": "v4" + }, + "Choice.ip_version.v6": { + "description": "One entry of the ip_version list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-3e8286044388", + "other": "v6" + }, + "Choice.level_mix.debug": { + "description": "One entry of the level_mix list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-0b8e9e995d8d", + "other": "debug" + }, + "Choice.level_mix.errors": { + "description": "One entry of the level_mix list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-be4bd5677277", + "other": "errors" + }, + "Choice.level_mix.quiet": { + "description": "One entry of the level_mix list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-008f0747f4e2", + "other": "quiet" + }, + "Choice.level_mix.realistic": { + "description": "One entry of the level_mix list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-9d2f45fd706a", + "other": "realistic" + }, + "Choice.line_ending.crlf": { + "description": "One entry of the line_ending list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-c613dc516f32", + "other": "crlf" + }, + "Choice.line_ending.lf": { + "description": "One entry of the line_ending list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-39e741eddb03", + "other": "lf" + }, + "Choice.methods.get": { + "description": "One entry of the methods list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-2998b3232d29", + "other": "get" + }, + "Choice.methods.mixed": { + "description": "One entry of the methods list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-3f8fee624f43", + "other": "mixed" + }, + "Choice.methods.read": { + "description": "One entry of the methods list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-3316348dbadf", + "other": "read" + }, + "Choice.orientation.landscape": { + "description": "One entry of the orientation list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-acb955df82b8", + "other": "landscape" + }, + "Choice.orientation.mixed": { + "description": "One entry of the orientation list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-3f8fee624f43", + "other": "mixed" + }, + "Choice.orientation.portrait": { + "description": "One entry of the orientation list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-51c5a8296a03", + "other": "portrait" + }, + "Choice.page_size.a3": { + "description": "One entry of the page_size list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-f46dd28a5499", + "other": "a3" + }, + "Choice.page_size.a4": { + "description": "One entry of the page_size list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-4539e4b48890", + "other": "a4" + }, + "Choice.page_size.a5": { + "description": "One entry of the page_size list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-66220e71591b", + "other": "a5" + }, + "Choice.page_size.legal": { + "description": "One entry of the page_size list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-a708df92c9e4", + "other": "legal" + }, + "Choice.page_size.letter": { + "description": "One entry of the page_size list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-f80105efab6d", + "other": "letter" + }, + "Choice.page_size.mixed": { + "description": "One entry of the page_size list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-3f8fee624f43", + "other": "mixed" + }, + "Choice.pdf_version.1.4": { + "description": "One entry of the pdf_version list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-1a948f1b4374", + "other": "1.4" + }, + "Choice.pdf_version.1.7": { + "description": "One entry of the pdf_version list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-8f65223004a7", + "other": "1.7" + }, + "Choice.quote_style.all": { + "description": "One entry of the quote_style list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-5ef5ef0364b6", + "other": "all" + }, + "Choice.quote_style.minimal": { + "description": "One entry of the quote_style list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-a703788f8320", + "other": "minimal" + }, + "Choice.quote_style.none": { + "description": "One entry of the quote_style list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-140bedbf9c3f", + "other": "none" + }, + "Choice.rotate.0": { + "description": "One entry of the rotate list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-5feceb66ffc8", + "other": "0" + }, + "Choice.rotate.180": { + "description": "One entry of the rotate list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-7b69759630f8", + "other": "180" + }, + "Choice.rotate.270": { + "description": "One entry of the rotate list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-d8d1790737d5", + "other": "270" + }, + "Choice.rotate.90": { + "description": "One entry of the rotate list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-69f59c273b6e", + "other": "90" + }, + "Choice.status_mix.client-errors": { + "description": "One entry of the status_mix list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-cc47f4e83c84", + "other": "client-errors" + }, + "Choice.status_mix.realistic": { + "description": "One entry of the status_mix list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-9d2f45fd706a", + "other": "realistic" + }, + "Choice.status_mix.server-errors": { + "description": "One entry of the status_mix list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-af6f28d08a12", + "other": "server-errors" + }, + "Choice.status_mix.success": { + "description": "One entry of the status_mix list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-aee408847d35", + "other": "success" + }, + "Choice.structure.document": { + "description": "One entry of the structure list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-43cc23fa52b8", + "other": "document" + }, + "Choice.structure.fragment": { + "description": "One entry of the structure list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-7587669ca42e", + "other": "fragment" + }, + "Choice.timestamps.advancing": { + "description": "One entry of the timestamps list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-3656fa4d5539", + "other": "advancing" + }, + "Choice.timestamps.fixed": { + "description": "One entry of the timestamps list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.", + "hash": "sha256-992a93455c71", + "other": "fixed" + }, "Detail.damage/zero-head.bytes": { "description": "The sentence under the bytes setting of the zero-head damage, after what it takes.", "hash": "sha256-8700126f6712", @@ -624,6 +1314,136 @@ "hash": "sha256-d2a5105f8783", "other": "The checksum the file should have. The algorithm is told from its length, and worked out even when it is not chosen above." }, + "Format.avif": { + "description": "What the avif format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-96d7c99ae908", + "other": "AV1 Image File Format" + }, + "Format.bmp": { + "description": "What the bmp format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-a9f24d64c3d9", + "other": "Windows Bitmap" + }, + "Format.csv": { + "description": "What the csv format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-6c61f1d4908c", + "other": "Comma-Separated Values" + }, + "Format.docx": { + "description": "What the docx format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-a1fb88e6372c", + "other": "Word (Office Open XML)" + }, + "Format.gif": { + "description": "What the gif format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-9a8783e957eb", + "other": "Graphics Interchange Format" + }, + "Format.html": { + "description": "What the html format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-0bd4039bc33f", + "other": "HyperText Markup Language" + }, + "Format.ico": { + "description": "What the ico format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-a4da38db8c6a", + "other": "Windows Icon" + }, + "Format.jpg": { + "description": "What the jpg format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-f4dc1c96a30f", + "other": "JPEG" + }, + "Format.json": { + "description": "What the json format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-777439b29237", + "other": "JavaScript Object Notation" + }, + "Format.jxl": { + "description": "What the jxl format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-5dab1bf80f83", + "other": "JPEG XL" + }, + "Format.log": { + "description": "What the log format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-b6ef69eb3499", + "other": "Server and application log" + }, + "Format.md": { + "description": "What the md format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-0e52f6b9d025", + "other": "Markdown" + }, + "Format.pdf": { + "description": "What the pdf format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-ccf9cd24dc07", + "other": "Portable Document Format" + }, + "Format.png": { + "description": "What the png format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-016d63e71b45", + "other": "Portable Network Graphics" + }, + "Format.pptx": { + "description": "What the pptx format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-33f651fb2245", + "other": "PowerPoint (Office Open XML)" + }, + "Format.svg": { + "description": "What the svg format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-ccd66e031e79", + "other": "Scalable Vector Graphics" + }, + "Format.targz": { + "description": "What the targz format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-57446f685bff", + "other": "tar + gzip" + }, + "Format.tiff": { + "description": "What the tiff format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-0a835e8afa67", + "other": "Tagged Image File Format" + }, + "Format.toml": { + "description": "What the toml format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-280d1ed55943", + "other": "TOML" + }, + "Format.txt": { + "description": "What the txt format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-cf4166250b80", + "other": "Plain text" + }, + "Format.wav": { + "description": "What the wav format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-126c919e4ee7", + "other": "Waveform Audio" + }, + "Format.webp": { + "description": "What the webp format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-98a439e3b19a", + "other": "WebP" + }, + "Format.xlsx": { + "description": "What the xlsx format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-39745fa7507d", + "other": "Excel (Office Open XML)" + }, + "Format.xml": { + "description": "What the xml format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-f4210757fbab", + "other": "Extensible Markup Language" + }, + "Format.yaml": { + "description": "What the yaml format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-9b6e8abe425e", + "other": "YAML" + }, + "Format.zip": { + "description": "What the zip format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.", + "hash": "sha256-eaca4b306928", + "other": "ZIP" + }, "Group.format/pdf.Document properties": { "description": "The heading over a block of settings of the pdf format.", "hash": "sha256-7115a778ab47", @@ -674,6 +1494,16 @@ "hash": "sha256-e1ef283f3c5d", "other": "the sheet is built in memory before it is packaged" }, + "Kind.file": { + "description": "What a tool works on, as a word, in a refusal that one was not given: no file was given.", + "hash": "sha256-3b9c358f36f0", + "other": "file" + }, + "Kind.folder": { + "description": "What a tool works on, as a word, in a refusal that one was not given: no file was given.", + "hash": "sha256-034a00624882", + "other": "folder" + }, "Label.algorithm": { "description": "The name beside the box of a setting a recipe writes as algorithm.", "hash": "sha256-d704d8af25be", @@ -999,6 +1829,31 @@ "hash": "sha256-3019947b6daf", "other": "Width" }, + "Licence.1": { + "description": "One paragraph of the licence notice on the About screen. The first is the name and the copyright line and stays as it is. The program's name and the licence's name stay as they are.", + "hash": "sha256-97d4df3a6420", + "other": "Testing Files Generator\nCopyright (C) 2026 DonislawDev" + }, + "Licence.2": { + "description": "One paragraph of the licence notice on the About screen. The first is the name and the copyright line and stays as it is. The program's name and the licence's name stay as they are.", + "hash": "sha256-2f8247096f6c", + "other": "Released under the GNU General Public License, version 3. The full text is at\nhttps://www.gnu.org/licenses/gpl-3.0.html, and in the LICENSE file if you have\nthe source or the full release." + }, + "Licence.3": { + "description": "One paragraph of the licence notice on the About screen. The first is the name and the copyright line and stays as it is. The program's name and the licence's name stay as they are.", + "hash": "sha256-329ebd54a420", + "other": "There is no warranty, to the extent the law allows." + }, + "Licence.4": { + "description": "One paragraph of the licence notice on the About screen. The first is the name and the copyright line and stays as it is. The program's name and the licence's name stay as they are.", + "hash": "sha256-13dec924ea00", + "other": "The files you generate are yours. Generated files, recipes and manifests are\noutput of this program and not derived works of it, so this licence does not\nreach them. You can generate fixtures, commit them and ship them inside a\nclosed source product with no obligation of any kind." + }, + "Licence.5": { + "description": "One paragraph of the licence notice on the About screen. The first is the name and the copyright line and stays as it is. The program's name and the licence's name stay as they are.", + "hash": "sha256-0e75d1e26c8b", + "other": "Code from other projects is compiled into this program. Their licences and\ncopyright notices are in THIRD-PARTY-NOTICES.md, which comes with the source\nand with the full release." + }, "Note.size-boundaries.limit": { "description": "What a run of the size-boundaries preset says when nobody gave its limit.", "hash": "sha256-d65173d5db01", @@ -1089,6 +1944,36 @@ "hash": "sha256-eb266034b6a8", "other": "the password, in plain text" }, + "Title.empty-and-minimal": { + "description": "What the empty-and-minimal preset is called, beside its id in the list of presets.", + "hash": "sha256-4cdca50e33b5", + "other": "Empty and minimal" + }, + "Title.filename-handling": { + "description": "What the filename-handling preset is called, beside its id in the list of presets.", + "hash": "sha256-314f9b08db3f", + "other": "File name handling" + }, + "Title.size-boundaries": { + "description": "What the size-boundaries preset is called, beside its id in the list of presets.", + "hash": "sha256-21dd0ecec76d", + "other": "Size boundaries" + }, + "Title.tabular-import": { + "description": "What the tabular-import preset is called, beside its id in the list of presets.", + "hash": "sha256-a08572cdb93c", + "other": "Tabular import" + }, + "Title.text-encoding": { + "description": "What the text-encoding preset is called, beside its id in the list of presets.", + "hash": "sha256-f6726ca0b9fe", + "other": "Text encoding" + }, + "Title.upload-validation": { + "description": "What the upload-validation preset is called, beside its id in the list of presets.", + "hash": "sha256-ba4463cda83e", + "other": "Upload validation" + }, "Tool.checksum-check.Detail": { "description": "One sentence under the question of the checksum-check tool, saying what it does.", "hash": "sha256-3480ee8eb9ae", diff --git a/internal/gui/text/locale/registry/pl.json b/internal/gui/text/locale/registry/pl.json index f34b3e05..be61a26b 100644 --- a/internal/gui/text/locale/registry/pl.json +++ b/internal/gui/text/locale/registry/pl.json @@ -24,6 +24,144 @@ "Catch.upload-validation.4": { "hash": "sha256-5f1862f0a060", "other": "formularz, który wczytuje całe żądanie do pamięci, zanim sprawdzi, jak jest duże" }, "Catch.upload-validation.5": { "hash": "sha256-3d1670bd44fd", "other": "plik PHOTO.JPG odrzucony tam, gdzie photo.jpg przechodzi, albo odwrotnie" }, "Catch.upload-validation.6": { "hash": "sha256-b918ae8fc864", "other": "nazwa ze spacjami, nawiasami albo znakami spoza ASCII zapisana na dysk bez zmian" }, + "Choice.algorithm.md5": { "hash": "sha256-3ebff31b62c0", "other": "md5" }, + "Choice.algorithm.sha1": { "hash": "sha256-b1565820a5cd", "other": "sha1" }, + "Choice.algorithm.sha256": { "hash": "sha256-5d5b09f6dcb2", "other": "sha256" }, + "Choice.algorithm.sha512": { "hash": "sha256-a396244643b5", "other": "sha512" }, + "Choice.bit_depth.16": { "hash": "sha256-b17ef6d19c7a", "other": "16" }, + "Choice.bit_depth.24": { "hash": "sha256-c2356069e9d1", "other": "24" }, + "Choice.bit_depth.32": { "hash": "sha256-e29c9c180c62", "other": "32" }, + "Choice.bit_depth.8": { "hash": "sha256-2c624232cdd2", "other": "8" }, + "Choice.compression.best": { "hash": "sha256-8f4375ec65c2", "other": "najlepsza" }, + "Choice.compression.default": { "hash": "sha256-37a8eec1ce19", "other": "domyślna" }, + "Choice.compression.fast": { "hash": "sha256-115dc3606fbf", "other": "szybka" }, + "Choice.compression.none": { "hash": "sha256-140bedbf9c3f", "other": "brak" }, + "Choice.content.noise": { "hash": "sha256-efe2a5c09e6d", "other": "szum" }, + "Choice.content.silence": { "hash": "sha256-e6c18fdbe597", "other": "cisza" }, + "Choice.content.sweep": { "hash": "sha256-dfd33796972c", "other": "przemiatanie" }, + "Choice.content.tone": { "hash": "sha256-00cf4ce3935c", "other": "ton" }, + "Choice.delimiter.comma": { "hash": "sha256-f36a4e586705", "other": "przecinek" }, + "Choice.delimiter.pipe": { "hash": "sha256-400829473916", "other": "kreska pionowa" }, + "Choice.delimiter.semicolon": { "hash": "sha256-1e69fcfadd9d", "other": "średnik" }, + "Choice.delimiter.tab": { "hash": "sha256-7508386a2056", "other": "tabulator" }, + "Choice.embed.bmp": { "hash": "sha256-7685a5cb2b57", "other": "bmp" }, + "Choice.embed.png": { "hash": "sha256-8f8cbb7dcf46", "other": "png" }, + "Choice.encoding.utf-16be": { "hash": "sha256-43b2bae67086", "other": "utf-16be" }, + "Choice.encoding.utf-16le": { "hash": "sha256-4ffbae688018", "other": "utf-16le" }, + "Choice.encoding.utf-8": { "hash": "sha256-5cf1fa8d836a", "other": "utf-8" }, + "Choice.encryption.aes-128": { "hash": "sha256-8cf9659cdea5", "other": "aes-128" }, + "Choice.encryption.aes-192": { "hash": "sha256-5498c4450db4", "other": "aes-192" }, + "Choice.encryption.aes-256": { "hash": "sha256-7237b089a3fc", "other": "aes-256" }, + "Choice.encryption.none": { "hash": "sha256-140bedbf9c3f", "other": "brak" }, + "Choice.encryption.zipcrypto": { "hash": "sha256-42ea6e5dc44a", "other": "zipcrypto" }, + "Choice.entry_format.apache-combined": { "hash": "sha256-eb5679006aba", "other": "apache-combined" }, + "Choice.entry_format.apache-common": { "hash": "sha256-5385bfe0ab10", "other": "apache-common" }, + "Choice.entry_format.json-lines": { "hash": "sha256-9e26c12b9d04", "other": "json-lines" }, + "Choice.entry_format.nginx": { "hash": "sha256-5be1ecc7935f", "other": "nginx" }, + "Choice.entry_format.plain": { "hash": "sha256-a116c9ed46d6", "other": "zwykły" }, + "Choice.entry_format.syslog": { "hash": "sha256-4d75f832ea26", "other": "syslog" }, + "Choice.entry_mode.000": { "hash": "sha256-2ac9a6746aca", "other": "000" }, + "Choice.entry_mode.400": { "hash": "sha256-26d228663f13", "other": "400" }, + "Choice.entry_mode.444": { "hash": "sha256-3538a1ef2e11", "other": "444" }, + "Choice.entry_mode.600": { "hash": "sha256-284b7e6d788f", "other": "600" }, + "Choice.entry_mode.644": { "hash": "sha256-87e50b287059", "other": "644" }, + "Choice.entry_mode.664": { "hash": "sha256-09eac95eb995", "other": "664" }, + "Choice.entry_mode.666": { "hash": "sha256-c7e616822f36", "other": "666" }, + "Choice.entry_mode.700": { "hash": "sha256-99ee50221221", "other": "700" }, + "Choice.entry_mode.755": { "hash": "sha256-86ab8cbe5869", "other": "755" }, + "Choice.entry_mode.777": { "hash": "sha256-eaf89db71084", "other": "777" }, + "Choice.entry_owner.root": { "hash": "sha256-4813494d137e", "other": "root" }, + "Choice.entry_owner.unset": { "hash": "sha256-6cbf83e08093", "other": "nieustawiony" }, + "Choice.entry_owner.user": { "hash": "sha256-04f8996da763", "other": "użytkownik" }, + "Choice.expected.accept": { "hash": "sha256-c125d0397b60", "other": "przyjęcie" }, + "Choice.expected.reason.content_malformed": { "hash": "sha256-14484a3d8ff7", "other": "uszkodzona zawartość" }, + "Choice.expected.reason.count_limit": { "hash": "sha256-636449b3d71c", "other": "limit liczby plików" }, + "Choice.expected.reason.dimensions_limit": { "hash": "sha256-539dd060c765", "other": "limit wymiarów" }, + "Choice.expected.reason.duplicate": { "hash": "sha256-e24a5a32c9b8", "other": "duplikat" }, + "Choice.expected.reason.encoding_invalid": { "hash": "sha256-1ed4a2806f5f", "other": "niepoprawne kodowanie" }, + "Choice.expected.reason.extension_rule": { "hash": "sha256-87ad72d63a7f", "other": "reguła rozszerzeń" }, + "Choice.expected.reason.filename_invalid": { "hash": "sha256-816b1c0c80ea", "other": "niedozwolona nazwa" }, + "Choice.expected.reason.filename_too_long": { "hash": "sha256-394b764c42c4", "other": "za długa nazwa" }, + "Choice.expected.reason.filename_traversal": { "hash": "sha256-51dbdb34a500", "other": "wyjście poza katalog" }, + "Choice.expected.reason.malware_signature": { "hash": "sha256-adb5581b7ea9", "other": "sygnatura złośliwego kodu" }, + "Choice.expected.reason.mime_mismatch": { "hash": "sha256-a1c2c3069328", "other": "niezgodny typ MIME" }, + "Choice.expected.reason.nesting_depth": { "hash": "sha256-ddc6be14fb52", "other": "głębokość zagnieżdżenia" }, + "Choice.expected.reason.none": { "hash": "sha256-140bedbf9c3f", "other": "brak" }, + "Choice.expected.reason.size_limit": { "hash": "sha256-7fba5810a3a2", "other": "limit rozmiaru" }, + "Choice.expected.reason.size_zero": { "hash": "sha256-2ccd5d11434a", "other": "pusty plik" }, + "Choice.expected.reject": { "hash": "sha256-084e990a49ba", "other": "odrzucenie" }, + "Choice.expected.sanitize": { "hash": "sha256-af6a3bb0c72f", "other": "oczyszczenie" }, + "Choice.expected.unspecified": { "hash": "sha256-f416648f8086", "other": "nieokreślony" }, + "Choice.far-over.10x": { "hash": "sha256-687f5e475cbd", "other": "10x" }, + "Choice.far-over.2x": { "hash": "sha256-b595b214b5d4", "other": "2x" }, + "Choice.far-over.off": { "hash": "sha256-b4dc66dde806", "other": "wyłączone" }, + "Choice.format.avif": { "hash": "sha256-2f47c57aae31", "other": "avif" }, + "Choice.format.bmp": { "hash": "sha256-7685a5cb2b57", "other": "bmp" }, + "Choice.format.csv": { "hash": "sha256-53bb9b0b38ac", "other": "csv" }, + "Choice.format.docx": { "hash": "sha256-584cb925e6ad", "other": "docx" }, + "Choice.format.gif": { "hash": "sha256-3b34c6906f7e", "other": "gif" }, + "Choice.format.html": { "hash": "sha256-e5e239560bc1", "other": "html" }, + "Choice.format.ico": { "hash": "sha256-c51052ef06b6", "other": "ico" }, + "Choice.format.jpg": { "hash": "sha256-f8146b6cb496", "other": "jpg" }, + "Choice.format.json": { "hash": "sha256-02bd175f3297", "other": "json" }, + "Choice.format.jxl": { "hash": "sha256-87953914d2ad", "other": "jxl" }, + "Choice.format.log": { "hash": "sha256-836ff184e7b4", "other": "log" }, + "Choice.format.md": { "hash": "sha256-21262a3cb533", "other": "md" }, + "Choice.format.pdf": { "hash": "sha256-c35b21d6ca39", "other": "pdf" }, + "Choice.format.png": { "hash": "sha256-8f8cbb7dcf46", "other": "png" }, + "Choice.format.pptx": { "hash": "sha256-1512e97a4b84", "other": "pptx" }, + "Choice.format.svg": { "hash": "sha256-acdb1373d176", "other": "svg" }, + "Choice.format.targz": { "hash": "sha256-fc53fb509109", "other": "targz" }, + "Choice.format.tiff": { "hash": "sha256-904749a4869b", "other": "tiff" }, + "Choice.format.toml": { "hash": "sha256-bf0a90221595", "other": "toml" }, + "Choice.format.txt": { "hash": "sha256-486b9a6fc855", "other": "txt" }, + "Choice.format.wav": { "hash": "sha256-61e25c2cdda1", "other": "wav" }, + "Choice.format.webp": { "hash": "sha256-a57bb082e728", "other": "webp" }, + "Choice.format.xlsx": { "hash": "sha256-1c980b22bca3", "other": "xlsx" }, + "Choice.format.xml": { "hash": "sha256-bf92a04e60b4", "other": "xml" }, + "Choice.format.yaml": { "hash": "sha256-9831daaaa0a9", "other": "yaml" }, + "Choice.format.zip": { "hash": "sha256-4a70fe9aa643", "other": "zip" }, + "Choice.formatting.indented": { "hash": "sha256-a258f543b0a6", "other": "z wcięciami" }, + "Choice.formatting.minified": { "hash": "sha256-1ddd424e287f", "other": "zminifikowany" }, + "Choice.formatting.record-per-line": { "hash": "sha256-6f0d86374d60", "other": "rekord na wiersz" }, + "Choice.ip_version.mixed": { "hash": "sha256-3f8fee624f43", "other": "mieszane" }, + "Choice.ip_version.v4": { "hash": "sha256-8e38a1ea5c68", "other": "v4" }, + "Choice.ip_version.v6": { "hash": "sha256-3e8286044388", "other": "v6" }, + "Choice.level_mix.debug": { "hash": "sha256-0b8e9e995d8d", "other": "szczegółowo" }, + "Choice.level_mix.errors": { "hash": "sha256-be4bd5677277", "other": "błędy" }, + "Choice.level_mix.quiet": { "hash": "sha256-008f0747f4e2", "other": "cicho" }, + "Choice.level_mix.realistic": { "hash": "sha256-9d2f45fd706a", "other": "realistycznie" }, + "Choice.line_ending.crlf": { "hash": "sha256-c613dc516f32", "other": "crlf" }, + "Choice.line_ending.lf": { "hash": "sha256-39e741eddb03", "other": "lf" }, + "Choice.methods.get": { "hash": "sha256-2998b3232d29", "other": "get" }, + "Choice.methods.mixed": { "hash": "sha256-3f8fee624f43", "other": "mieszane" }, + "Choice.methods.read": { "hash": "sha256-3316348dbadf", "other": "odczyt" }, + "Choice.orientation.landscape": { "hash": "sha256-acb955df82b8", "other": "poziomo" }, + "Choice.orientation.mixed": { "hash": "sha256-3f8fee624f43", "other": "mieszana" }, + "Choice.orientation.portrait": { "hash": "sha256-51c5a8296a03", "other": "pionowo" }, + "Choice.page_size.a3": { "hash": "sha256-f46dd28a5499", "other": "a3" }, + "Choice.page_size.a4": { "hash": "sha256-4539e4b48890", "other": "a4" }, + "Choice.page_size.a5": { "hash": "sha256-66220e71591b", "other": "a5" }, + "Choice.page_size.legal": { "hash": "sha256-a708df92c9e4", "other": "legal" }, + "Choice.page_size.letter": { "hash": "sha256-f80105efab6d", "other": "letter" }, + "Choice.page_size.mixed": { "hash": "sha256-3f8fee624f43", "other": "mieszany" }, + "Choice.pdf_version.1.4": { "hash": "sha256-1a948f1b4374", "other": "1.4" }, + "Choice.pdf_version.1.7": { "hash": "sha256-8f65223004a7", "other": "1.7" }, + "Choice.quote_style.all": { "hash": "sha256-5ef5ef0364b6", "other": "wszystkie" }, + "Choice.quote_style.minimal": { "hash": "sha256-a703788f8320", "other": "minimalne" }, + "Choice.quote_style.none": { "hash": "sha256-140bedbf9c3f", "other": "brak" }, + "Choice.rotate.0": { "hash": "sha256-5feceb66ffc8", "other": "0" }, + "Choice.rotate.180": { "hash": "sha256-7b69759630f8", "other": "180" }, + "Choice.rotate.270": { "hash": "sha256-d8d1790737d5", "other": "270" }, + "Choice.rotate.90": { "hash": "sha256-69f59c273b6e", "other": "90" }, + "Choice.status_mix.client-errors": { "hash": "sha256-cc47f4e83c84", "other": "błędy klienta" }, + "Choice.status_mix.realistic": { "hash": "sha256-9d2f45fd706a", "other": "realistycznie" }, + "Choice.status_mix.server-errors": { "hash": "sha256-af6f28d08a12", "other": "błędy serwera" }, + "Choice.status_mix.success": { "hash": "sha256-aee408847d35", "other": "sukcesy" }, + "Choice.structure.document": { "hash": "sha256-43cc23fa52b8", "other": "dokument" }, + "Choice.structure.fragment": { "hash": "sha256-7587669ca42e", "other": "fragment" }, + "Choice.timestamps.advancing": { "hash": "sha256-3656fa4d5539", "other": "rosnące" }, + "Choice.timestamps.fixed": { "hash": "sha256-992a93455c71", "other": "stałe" }, "Detail.damage/zero-head.bytes": { "hash": "sha256-8700126f6712", "other": "Ile bajtów na początku zostaje wyzerowanych. Poniżej czterech niektóre formaty wychodzą z uszkodzeniem, na które żaden czytnik nie narzeka." }, "Detail.format/avif.height": { "hash": "sha256-4b336c279584", "other": "Jak wysoki jest obraz. Gdy pole zostaje puste, wybierany jest rozmiar, który mieści się w zamówionych bajtach." }, "Detail.format/avif.quality": { "hash": "sha256-f6e1c3e1af83", "other": "Ile szczegółów zachowuje obraz. Wyższa wartość wygląda lepiej i zajmuje więcej pliku, zostawiając mniej miejsca na dopełnienie." }, @@ -31,10 +169,10 @@ "Detail.format/bmp.height": { "hash": "sha256-c5f8bc5c8f49", "other": "Jak wysoki jest obraz. Gdy pole zostaje puste, obraz jest dobierany tak, żeby wypełnił zamówione bajty." }, "Detail.format/bmp.width": { "hash": "sha256-0c629fee898a", "other": "Jak szeroki jest obraz. Gdy pole zostaje puste, obraz jest dobierany tak, żeby wypełnił zamówione bajty." }, "Detail.format/csv.columns": { "hash": "sha256-a8c37c7f2d97", "other": "Ile kolumn ma każdy wiersz. Powyżej 16384 arkusz kalkulacyjny może pokazać tylko pierwsze 16384 i bez słowa pominąć resztę." }, - "Detail.format/csv.delimiter": { "hash": "sha256-1cc7b45d9a95", "other": "Co rozdziela pola. Wybierz semicolon, żeby dostać kształt, jaki eksportuje europejski arkusz kalkulacyjny." }, + "Detail.format/csv.delimiter": { "hash": "sha256-1cc7b45d9a95", "other": "Co rozdziela pola. Wybierz średnik, żeby dostać kształt, jaki eksportuje europejski arkusz kalkulacyjny." }, "Detail.format/csv.header": { "hash": "sha256-29dcadb7c881", "other": "Czy pierwszy wiersz nazywa kolumny. Wyłącz, żeby dostać tabelę zrzuconą prosto z bazy danych." }, "Detail.format/csv.line_ending": { "hash": "sha256-d802b082ce8b", "other": "Jak kończy się każdy wiersz. Wybierz crlf, żeby dostać kształt, którego wymaga RFC 4180 i który zapisuje Excel." }, - "Detail.format/csv.quote_style": { "hash": "sha256-a2427ed0240b", "other": "Które pola mają cudzysłowy. Przy none opis przestaje też zawierać separatory, bo pole bez cudzysłowu nie może go pomieścić." }, + "Detail.format/csv.quote_style": { "hash": "sha256-a2427ed0240b", "other": "Które pola mają cudzysłowy. Gdy wybierzesz brak, opis przestaje też zawierać separatory, bo pole bez cudzysłowu nie może go pomieścić." }, "Detail.format/docx.paragraphs": { "hash": "sha256-5d0141e9b1da", "other": "Ile akapitów tekstu zawiera dokument. Resztę zamówionego rozmiaru niesie niewidoczna część paczki." }, "Detail.format/gif.frames": { "hash": "sha256-d2291fedd06c", "other": "Ile klatek ma animacja. Ustaw 1, żeby dostać nieruchomy obraz." }, "Detail.format/gif.height": { "hash": "sha256-4b336c279584", "other": "Jak wysoki jest obraz. Gdy pole zostaje puste, wybierany jest rozmiar, który mieści się w zamówionych bajtach." }, @@ -46,18 +184,18 @@ "Detail.format/jpg.height": { "hash": "sha256-4b336c279584", "other": "Jak wysoki jest obraz. Gdy pole zostaje puste, wybierany jest rozmiar, który mieści się w zamówionych bajtach." }, "Detail.format/jpg.quality": { "hash": "sha256-55a9a664be71", "other": "Ile szczegółów przetrwa kompresję, od 1 do 100. JPEG z założenia odrzuca szczegóły, więc niska liczba daje widocznie bardziej miękki obraz. Zmienia to, do jakiego rozmiaru obraz się koduje, a nie rozmiar zamówionego pliku." }, "Detail.format/jpg.width": { "hash": "sha256-3085ac44f0e0", "other": "Jak szeroki jest obraz. Gdy pole zostaje puste, wybierany jest rozmiar, który mieści się w zamówionych bajtach." }, - "Detail.format/json.formatting": { "hash": "sha256-fc49d1b8da7d", "other": "Jak ułożony jest dokument. Każdy czytnik przyjmuje wszystkie trzy, więc to zmienia to, co spotyka otoczenie parsera, a nie sam parser - minified stawia cały plik w jednej linii i kończy go bez znaku nowej linii, a indented robi z tych samych rekordów plik kilka razy większy." }, + "Detail.format/json.formatting": { "hash": "sha256-fc49d1b8da7d", "other": "Jak ułożony jest dokument. Każdy czytnik przyjmuje wszystkie trzy, więc to zmienia to, co spotyka otoczenie parsera, a nie sam parser - zminifikowany stawia cały plik w jednej linii i kończy go bez znaku nowej linii, a z wcięciami robi z tych samych rekordów plik kilka razy większy." }, "Detail.format/jxl.height": { "hash": "sha256-4b336c279584", "other": "Jak wysoki jest obraz. Gdy pole zostaje puste, wybierany jest rozmiar, który mieści się w zamówionych bajtach." }, "Detail.format/jxl.quality": { "hash": "sha256-f6e1c3e1af83", "other": "Ile szczegółów zachowuje obraz. Wyższa wartość wygląda lepiej i zajmuje więcej pliku, zostawiając mniej miejsca na dopełnienie." }, "Detail.format/jxl.width": { "hash": "sha256-3085ac44f0e0", "other": "Jak szeroki jest obraz. Gdy pole zostaje puste, wybierany jest rozmiar, który mieści się w zamówionych bajtach." }, "Detail.format/log.entry_format": { "hash": "sha256-96bf1c74dcf1", "other": "Jaki rodzaj logu zapisać. Kształty serwera WWW niosą żądanie i status, pozostałe niosą poziom i komunikat." }, "Detail.format/log.ip_version": { "hash": "sha256-870d5b20c405", "other": "Jaki rodzaj adresu klienta się pojawia. Wybierz v6, żeby sprawdzić, czy czytnik go obsługuje." }, - "Detail.format/log.level_mix": { "hash": "sha256-45f87458cd88", "other": "Które poziomy ważności się pojawiają. Niosą je tylko kształty plain i json-lines, a prośba o nie przy innym kształcie jest odrzucana." }, + "Detail.format/log.level_mix": { "hash": "sha256-45f87458cd88", "other": "Które poziomy ważności się pojawiają. Niosą je tylko kształty zwykły i json-lines, a prośba o nie przy innym kształcie jest odrzucana." }, "Detail.format/log.line_ending": { "hash": "sha256-8595ad80ed76", "other": "Jak kończy się każda linia. Wybierz crlf, żeby dostać log zapisany przez usługę Windows." }, - "Detail.format/log.methods": { "hash": "sha256-c621eebad3fc", "other": "Które metody żądań się pojawiają. Read to GET i HEAD, mixed dodaje POST, PUT, PATCH i DELETE." }, + "Detail.format/log.methods": { "hash": "sha256-c621eebad3fc", "other": "Które metody żądań się pojawiają. Odczyt to GET i HEAD, mieszane dodaje POST, PUT, PATCH i DELETE." }, "Detail.format/log.rate": { "hash": "sha256-f8f4b55f9769", "other": "Jak szybko przychodzą wpisy. Ma znaczenie tylko wtedy, gdy znaczniki czasu idą naprzód." }, - "Detail.format/log.status_mix": { "hash": "sha256-6555eacaebb2", "other": "Które kody odpowiedzi się pojawiają. Realistic to głównie sukcesy z ogonem błędów." }, - "Detail.format/log.timestamps": { "hash": "sha256-b7c69e2424bc", "other": "Czy każdy wpis dzieje się później niż poprzedni. Fixed stawia każdy wpis w tej samej chwili, tak jak ten format robił, zanim umiał iść naprzód." }, + "Detail.format/log.status_mix": { "hash": "sha256-6555eacaebb2", "other": "Które kody odpowiedzi się pojawiają. Realistycznie daje głównie sukcesy z ogonem błędów." }, + "Detail.format/log.timestamps": { "hash": "sha256-b7c69e2424bc", "other": "Czy każdy wpis dzieje się później niż poprzedni. Stałe stawiają każdy wpis w tej samej chwili, tak jak ten format robił, zanim umiał iść naprzód." }, "Detail.format/md.bom": { "hash": "sha256-a2d6a88583d6", "other": "Czy plik zaczyna się od znacznika kolejności bajtów (BOM). Czytnik, który musi zgadywać kodowanie, go potrzebuje, a czytnik, który się go nie spodziewa, pokazuje go jako obce znaki na początku pliku." }, "Detail.format/md.encoding": { "hash": "sha256-9b6f8a3338ec", "other": "W jakim kodowaniu zapisane są znaki. UTF-16 zapisuje dwa bajty na znak, więc plik w nim ma zawsze parzystą liczbę bajtów, a nieparzysty rozmiar jest odrzucany." }, "Detail.format/pdf.author": { "hash": "sha256-33b1ee363d86", "other": "Autor we właściwościach dokumentu. Gdy pole zostaje puste, dokument nie podaje żadnego." }, @@ -65,8 +203,8 @@ "Detail.format/pdf.creator": { "hash": "sha256-57ed4066a447", "other": "Program, w którym według dokumentu go napisano, na przykład Microsoft Word. Gdy pole zostaje puste, dokument nie podaje żadnego." }, "Detail.format/pdf.keywords": { "hash": "sha256-fa35deba8ee4", "other": "Słowa kluczowe we właściwościach dokumentu, jako jedna linia tekstu. Gdy pole zostaje puste, dokument nie ma żadnych." }, "Detail.format/pdf.modified": { "hash": "sha256-67960ecb9bad", "other": "Kiedy według dokumentu zmieniono go ostatni raz, zapisane tak samo jak created. Gdy pole zostaje puste, dokument nie ma takiej daty." }, - "Detail.format/pdf.orientation": { "hash": "sha256-e3b372e39fb9", "other": "Czy strony stoją pionowo, czy leżą poziomo. Mixed zmienia je na przemian, zaczynając od pionowej, i wymaga co najmniej dwóch stron." }, - "Detail.format/pdf.page_size": { "hash": "sha256-5bac95d9b758", "other": "Rozmiar papieru każdej strony. Mixed przechodzi po kolei przez a4, letter, legal, a3 i a5, jeden na stronę, i wymaga co najmniej dwóch stron." }, + "Detail.format/pdf.orientation": { "hash": "sha256-e3b372e39fb9", "other": "Czy strony stoją pionowo, czy leżą poziomo. Mieszana zmienia je na przemian, zaczynając od pionowej, i wymaga co najmniej dwóch stron." }, + "Detail.format/pdf.page_size": { "hash": "sha256-5bac95d9b758", "other": "Rozmiar papieru każdej strony. Mieszany przechodzi po kolei przez a4, letter, legal, a3 i a5, jeden na stronę, i wymaga co najmniej dwóch stron." }, "Detail.format/pdf.pages": { "hash": "sha256-298488d25c0b", "other": "Ile stron ma dokument." }, "Detail.format/pdf.pdf_version": { "hash": "sha256-be65b8f13fa3", "other": "Wersja zapisana na początku pliku. Nic innego się nie zmienia, bo dokument nie używa niczego nowszego niż 1.4." }, "Detail.format/pdf.producer": { "hash": "sha256-d38b44bea162", "other": "Program, który według dokumentu zamienił go na PDF." }, @@ -84,7 +222,7 @@ "Detail.format/targz.entries": { "hash": "sha256-e8a3f919b300", "other": "Ile plików zawiera archiwum. Gdy pliki nie są jednakowe, użyj zamiast tego contains." }, "Detail.format/targz.entry_format": { "hash": "sha256-f4dab5f30a79", "other": "Format plików w środku. Uruchom tfg formats, żeby zobaczyć, co obsługuje ta wersja." }, "Detail.format/targz.entry_mode": { "hash": "sha256-6afb3c26590f", "other": "Uprawnienia zapisane dla każdego pliku w środku. To mówi archiwum, a nie to, co dostaje plik - to zależy od tego, kto i jak je rozpakuje." }, - "Detail.format/targz.entry_owner": { "hash": "sha256-e4fa8df2c217", "other": "Do kogo należy każdy plik w środku. Zostaw unset, a archiwum nie wskaże nikogo, tak jak większość archiwów zapisanych przez build." }, + "Detail.format/targz.entry_owner": { "hash": "sha256-e4fa8df2c217", "other": "Do kogo należy każdy plik w środku. Zostaw nieustawiony, a archiwum nie wskaże nikogo, tak jak większość archiwów zapisanych przez build." }, "Detail.format/targz.entry_size": { "hash": "sha256-c3687f6606e8", "other": "Jak duży jest każdy plik w środku." }, "Detail.format/tiff.height": { "hash": "sha256-c5f8bc5c8f49", "other": "Jak wysoki jest obraz. Gdy pole zostaje puste, obraz jest dobierany tak, żeby wypełnił zamówione bajty." }, "Detail.format/tiff.width": { "hash": "sha256-0c629fee898a", "other": "Jak szeroki jest obraz. Gdy pole zostaje puste, obraz jest dobierany tak, żeby wypełnił zamówione bajty." }, @@ -124,6 +262,32 @@ "Detail.tool/checksum-write.algorithm": { "hash": "sha256-bc3d55ed0c3c", "other": "Który skrót zapisać. sha256 to ten, którego używa sha256sum i większość stron z wydaniami." }, "Detail.tool/checksum.algorithm": { "hash": "sha256-b2c6c5ddeaa1", "other": "Które sumy kontrolne policzyć: md5, sha1, sha256, sha512 albo crc32. Tylko sha256 i sha512 nadal pokazują, że nikt celowo nie zmienił pliku. crc32 to ta, której używają ZIP i PNG, a nie ta, którą wypisuje cksum." }, "Detail.tool/checksum.expected": { "hash": "sha256-d2a5105f8783", "other": "Suma kontrolna, jaką plik powinien mieć. Algorytm rozpoznaje się po jej długości i liczy go, nawet jeśli nie wybrano go wyżej." }, + "Format.avif": { "hash": "sha256-96d7c99ae908", "other": "AV1 Image File Format" }, + "Format.bmp": { "hash": "sha256-a9f24d64c3d9", "other": "Windows Bitmap" }, + "Format.csv": { "hash": "sha256-6c61f1d4908c", "other": "Comma-Separated Values" }, + "Format.docx": { "hash": "sha256-a1fb88e6372c", "other": "Word (Office Open XML)" }, + "Format.gif": { "hash": "sha256-9a8783e957eb", "other": "Graphics Interchange Format" }, + "Format.html": { "hash": "sha256-0bd4039bc33f", "other": "HyperText Markup Language" }, + "Format.ico": { "hash": "sha256-a4da38db8c6a", "other": "Ikona Windows" }, + "Format.jpg": { "hash": "sha256-f4dc1c96a30f", "other": "JPEG" }, + "Format.json": { "hash": "sha256-777439b29237", "other": "JavaScript Object Notation" }, + "Format.jxl": { "hash": "sha256-5dab1bf80f83", "other": "JPEG XL" }, + "Format.log": { "hash": "sha256-b6ef69eb3499", "other": "Log serwera i aplikacji" }, + "Format.md": { "hash": "sha256-0e52f6b9d025", "other": "Markdown" }, + "Format.pdf": { "hash": "sha256-ccf9cd24dc07", "other": "Portable Document Format" }, + "Format.png": { "hash": "sha256-016d63e71b45", "other": "Portable Network Graphics" }, + "Format.pptx": { "hash": "sha256-33f651fb2245", "other": "PowerPoint (Office Open XML)" }, + "Format.svg": { "hash": "sha256-ccd66e031e79", "other": "Scalable Vector Graphics" }, + "Format.targz": { "hash": "sha256-57446f685bff", "other": "tar + gzip" }, + "Format.tiff": { "hash": "sha256-0a835e8afa67", "other": "Tagged Image File Format" }, + "Format.toml": { "hash": "sha256-280d1ed55943", "other": "TOML" }, + "Format.txt": { "hash": "sha256-cf4166250b80", "other": "Zwykły tekst" }, + "Format.wav": { "hash": "sha256-126c919e4ee7", "other": "Waveform Audio" }, + "Format.webp": { "hash": "sha256-98a439e3b19a", "other": "WebP" }, + "Format.xlsx": { "hash": "sha256-39745fa7507d", "other": "Excel (Office Open XML)" }, + "Format.xml": { "hash": "sha256-f4210757fbab", "other": "Extensible Markup Language" }, + "Format.yaml": { "hash": "sha256-9b6e8abe425e", "other": "YAML" }, + "Format.zip": { "hash": "sha256-eaca4b306928", "other": "ZIP" }, "Group.format/pdf.Document properties": { "hash": "sha256-7115a778ab47", "other": "Właściwości dokumentu" }, "Input.checksum-check.checksum_file": { "hash": "sha256-90d7baf0d228", "other": "Plik sum kontrolnych. Ścieżki w nim są czytane z folderu, w którym leży, i nic nie jest zapisywane." }, "Input.checksum-write.folder": { "hash": "sha256-3040f185111b", "other": "Folder do spisania. Każdy plik w nim jest czytany, a plik sum kontrolnych jest zapisywany do niego." }, @@ -134,6 +298,8 @@ "Joint.format/jxl.width.height": { "hash": "sha256-269a6c476761", "other": "koder trzyma cały obraz w pamięci podczas pracy" }, "Joint.format/png.width.height": { "hash": "sha256-0bed7aa664d8", "other": "obraz jest trzymany w pamięci podczas kodowania" }, "Joint.format/xlsx.rows.columns": { "hash": "sha256-e1ef283f3c5d", "other": "arkusz powstaje w pamięci, zanim zostanie spakowany" }, + "Kind.file": { "hash": "sha256-3b9c358f36f0", "other": "plik" }, + "Kind.folder": { "hash": "sha256-034a00624882", "other": "folder" }, "Label.algorithm": { "hash": "sha256-d704d8af25be", "other": "Algorytm" }, "Label.allow": { "hash": "sha256-e213c161d5ce", "other": "Dozwolone" }, "Label.author": { "hash": "sha256-d95082a2ee57", "other": "Autor" }, @@ -199,6 +365,11 @@ "Label.timestamps": { "hash": "sha256-755f099dbf49", "other": "Znaczniki czasu" }, "Label.title": { "hash": "sha256-7e8cd2056da7", "other": "Tytuł" }, "Label.width": { "hash": "sha256-3019947b6daf", "other": "Szerokość" }, + "Licence.1": { "hash": "sha256-97d4df3a6420", "other": "Testing Files Generator\nCopyright (C) 2026 DonislawDev" }, + "Licence.2": { "hash": "sha256-2f8247096f6c", "other": "Wydano na licencji GNU General Public License w wersji 3. Pełny tekst jest pod adresem https://www.gnu.org/licenses/gpl-3.0.html oraz w pliku LICENSE, jeśli masz źródła albo pełne wydanie." }, + "Licence.3": { "hash": "sha256-329ebd54a420", "other": "Nie ma żadnej gwarancji w zakresie, na jaki pozwala prawo." }, + "Licence.4": { "hash": "sha256-13dec924ea00", "other": "Pliki, które generujesz, należą do Ciebie. Wygenerowane pliki, przepisy i manifesty są wynikiem działania tego programu, a nie utworami od niego zależnymi, więc ta licencja ich nie obejmuje. Możesz generować pliki testowe, dodawać je do repozytorium i dostarczać w produkcie o zamkniętym kodzie bez żadnych zobowiązań." }, + "Licence.5": { "hash": "sha256-0e75d1e26c8b", "other": "W ten program wkompilowano kod innych projektów. Ich licencje i informacje o prawach autorskich są w pliku THIRD-PARTY-NOTICES.md, dołączonym do źródeł i do pełnego wydania." }, "Note.size-boundaries.limit": { "hash": "sha256-d65173d5db01", "other": "nie podano limitu, więc ten zestaw jest zbudowany wokół 10mb - to nasza wartość zastępcza, a nie limit twojego systemu. Podaj limit, który deklaruje twój system, inaczej pliki nic o nim nie mówią." }, "Note.upload-validation.limit": { "hash": "sha256-c30ac7e13539", "other": "nie podano limitu, więc ten zestaw jest zbudowany wokół 10mb - to nasza wartość zastępcza, a nie limit twojego formularza. Podaj limit, który deklaruje twój formularz, inaczej pliki nic o nim nie mówią." }, "Question.empty-and-minimal": { "hash": "sha256-9a69d7227e12", "other": "Czy plik poprawny i najmniejszy, na jaki format pozwala, przechodzi?" }, @@ -217,6 +388,12 @@ "Shape.sizes separated by commas": { "hash": "sha256-f7e5e8c54e6f", "other": "rozmiary rozdzielone przecinkami" }, "Shape.the id of a format, as tfg formats lists them": { "hash": "sha256-b6784882fdeb", "other": "identyfikator formatu, tak jak wypisuje go tfg formats" }, "Shape.the password, in plain text": { "hash": "sha256-eb266034b6a8", "other": "hasło, zwykłym tekstem" }, + "Title.empty-and-minimal": { "hash": "sha256-4cdca50e33b5", "other": "Pliki puste i minimalne" }, + "Title.filename-handling": { "hash": "sha256-314f9b08db3f", "other": "Obsługa nazw plików" }, + "Title.size-boundaries": { "hash": "sha256-21dd0ecec76d", "other": "Granice rozmiaru" }, + "Title.tabular-import": { "hash": "sha256-a08572cdb93c", "other": "Import tabel" }, + "Title.text-encoding": { "hash": "sha256-f6726ca0b9fe", "other": "Kodowanie tekstu" }, + "Title.upload-validation": { "hash": "sha256-ba4463cda83e", "other": "Walidacja uploadu" }, "Tool.checksum-check.Detail": { "hash": "sha256-3480ee8eb9ae", "other": "Czyta plik sum kontrolnych - SHA256SUMS, MD5SUMS albo w postaci ze znacznikami, którą pisze shasum - i sprawdza każdy wymieniony w nim plik w folderze, w którym ten plik leży. Plików, których nie wymienia, nie ogląda, tak samo jak sha256sum -c." }, "Tool.checksum-check.Note.changed": { "hash": "sha256-3f2770c1e166", "other": "Zmienione w trakcie czytania, więc bez sumy kontrolnej:" }, "Tool.checksum-check.Note.checked": { "hash": "sha256-cb6fcab36d77", "other": "Sprawdzone pliki:" }, diff --git a/internal/gui/text/locale/said/en.json b/internal/gui/text/locale/said/en.json new file mode 100644 index 00000000..7c1b4f00 --- /dev/null +++ b/internal/gui/text/locale/said/en.json @@ -0,0 +1,3126 @@ +{ + "archive.AskForNone": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.None}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1c9a11fe7f69", + "other": "Ask for {{.None}} to store the files as they are." + }, + "archive.ExplicitSizeOrNone": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Compression}}, {{.None}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-f624161851a0", + "other": "Give the archive an explicit size, or ask for {{.Compression}}: {{.None}}." + }, + "archive.FlatNoDirectories": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Depth}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-24dda239d49e", + "other": "a flat archive has no directories to list, and {{.Depth}} is 0" + }, + "archive.FlatNoDirectoriesFix": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Depth}}, {{.Directories}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-54339aa5e96a", + "other": "Ask for {{.Depth}} of 1 or more, or leave {{.Directories}} off." + }, + "archive.GiveAPassword": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.None}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-2d40fa5d2f6b", + "other": "Give a password, or set encryption to {{.None}}." + }, + "archive.LockedNoCompression": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Password}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-04d93d355279", + "other": "the archive is locked with a {{.Password}}, and a locked entry states its length before its data is written - so a compressed one would have to be held in memory whole" + }, + "archive.LockedNoCompressionFix": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Compression}}, {{.None}}, {{.Password}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-82613b7fae43", + "other": "Ask for {{.Compression}}: {{.None}}, or take the {{.Password}} off." + }, + "archive.LockedWithoutPassword": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Method}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-b5d56de9a9b9", + "other": "the archive is set to be locked with {{.Method}} and there is no password to lock it with" + }, + "archive.SetEncryption": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Method}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-0b2a90b45006", + "other": "Set encryption to {{.Method}} to lock the archive, or remove the password to leave it open." + }, + "archive.WriteTrueOrFalse": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Key}}, {{.Again}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-213afa915dd9", + "other": "Write {{.Key}}: true or {{.Again}}: false." + }, + "avif.MinimumReason": { + "description": "Said by internal/format/avif - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Coded}}, {{.Box}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-97b2a4a72e3a", + "other": "the smallest picture this format draws codes to {{.Coded}} B at worst, and the file always carries a free box, which costs {{.Box}} B even when it holds nothing" + }, + "avif.MinimumReasonOwn": { + "description": "Said by internal/format/avif - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Width}}, {{.Height}}, {{.Quality}}, {{.Coded}}, {{.Box}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-ff69f6fa9b64", + "other": "a {{.Width}}x{{.Height}} picture at quality {{.Quality}} codes to {{.Coded}} B, and the file always carries a free box, which costs {{.Box}} B even when it holds nothing" + }, + "checksum.AChecksumDigitsLongIsNot": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Digits}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-0dcdc03bd2f7", + "other": "a checksum {{.Digits}} digits long is not one this tool works out" + }, + "checksum.AChecksumFileIsNeverWritten": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-49681362425b", + "other": "a checksum file is never written over, since it may be the only record of what the folder held" + }, + "checksum.AChecksumFilePromisesEveryFile": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-354be45714f3", + "other": "a checksum file promises every file in the folder, and one with a gap in it would not say where the gap is" + }, + "checksum.AChecksumFileWithoutALine": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-fcc3b0268478", + "other": "a checksum file without a line in it is refused by sha256sum -c, so it would check nothing" + }, + "checksum.AChecksumLineIsAChecksum": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-acbe4fd99bc0", + "other": "a checksum line is a checksum and a path, written as hex path or as SHA256 (path) = hex, and without one there is nothing to check" + }, + "checksum.AChecksumOfAFileBeing": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-3992fc15f6b5", + "other": "a checksum of a file being written is the checksum of no version of it" + }, + "checksum.ADirectoryHasNoBytesOf": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-c5451d4ddaae", + "other": "a directory has no bytes of its own to work a checksum out of" + }, + "checksum.AnAlgorithmIsNamedExactlyBecause": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-5c67b39f12b3", + "other": "an algorithm is named exactly, because two that sound alike give different checksums" + }, + "checksum.ChangedWhileItWasBeingRead": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-7b323b86f3c7", + "other": "{{.Path}} changed while it was being read" + }, + "checksum.CheckTheFolderAgainstItWith": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-f2f1b7172ace", + "other": "check the folder against it with tfg tool checksum-check, or move it away and run this again" + }, + "checksum.CheckTheFolderInPartsEach": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-e9771dc952f5", + "other": "check the folder in parts, each with a checksum file of its own" + }, + "checksum.FolderUnreadable": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Sentence}}, {{.Problems}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-07ba49413258", + "other": "{{.Sentence}}:\n {{.Problems}}" + }, + "checksum.FreeSomeSpaceOnThatDisk": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-0e09b5bc8590", + "other": "free some space on that disk and run this again" + }, + "checksum.HoldsNoChecksumLine": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-073a243f0ed8", + "other": "{{.Path}} holds no checksum line" + }, + "checksum.HoldsNoFileToList": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Folder}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-8f2183b75bf4", + "other": "{{.Folder}} holds no file to list" + }, + "checksum.HoldsNoFileToListOnly": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Folder}}, {{.Names}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-9f4a648b05cc", + "other": "{{.Folder}} holds no file to list, only {{.Names}} that are not files" + }, + "checksum.IsADirectoryAndThisReads": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-0af9eb952a74", + "other": "{{.Path}} is a directory and this reads one checksum file" + }, + "checksum.IsADirectoryAndThisWorks": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-3cba141e419e", + "other": "{{.Path}} is a directory and this works out the checksum of one file" + }, + "checksum.IsAFileAndThisLists": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-e43a949bd38b", + "other": "{{.Path}} is a file and this lists a folder" + }, + "checksum.IsAlreadyThereSoNothingWas": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-f3786e37e4f3", + "other": "{{.Path}} is already there, so nothing was written" + }, + "checksum.IsLargerThanAChecksumFile": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, {{.Bytes}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-aead292e6e34", + "other": "{{.Path}} is {{.Bytes}}, larger than a checksum file this reads" + }, + "checksum.IsNotAChecksumWrittenIn": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Given}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a48e593200f8", + "other": "{{.Given}} is not a checksum written in hexadecimal" + }, + "checksum.IsNotAFileButA": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-28957d41213f", + "other": "{{.Path}} is not a file but a device, a pipe or a socket" + }, + "checksum.ItTakesAndTheDiskHas": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Need}}, {{.Have}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-c053efc918f1", + "other": "it takes {{.Need}} and the disk has {{.Have}} free" + }, + "checksum.MakeTheseReadableOrMoveThem": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-0d223d82147d", + "other": "make these readable or move them out of the folder, and run this again" + }, + "checksum.NameAFolderWithAtLeast": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-5a322343ecd1", + "other": "name a folder with at least one file in it" + }, + "checksum.NameOneFile": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-74c2c0a6f263", + "other": "name one file" + }, + "checksum.NameTheChecksumFileItselfSuch": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-c75ab67b49df", + "other": "name the checksum file itself, such as SHA256SUMS, rather than a page or a signature beside it" + }, + "checksum.NameTheFolderOrUseTfg": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-9457caf28208", + "other": "name the folder, or use tfg tool checksum for the one file" + }, + "checksum.Names": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1b28298a4324", + "one": "{{.Count}} name", + "other": "{{.Count}} names" + }, + "checksum.NotEverythingUnderCouldBeRead": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Folder}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-7d3694b98961", + "other": "not everything under {{.Folder}} could be read, so nothing was written" + }, + "checksum.PasteAlone": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Lengths}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-c174b1551cbd", + "other": "paste the checksum alone, without the file name. The lengths are: {{.Lengths}}" + }, + "checksum.PathProblem": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, {{.Error}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-8bcdf2cccf02", + "other": "{{.Path}} - {{.Error}}" + }, + "checksum.ReadingOneMayNeverEndSo": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-9055c322986c", + "other": "reading one may never end, so it is not started" + }, + "checksum.TheChecksumFileIsWrittenBeside": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-a62cd68c242f", + "other": "the checksum file is written beside the files it lists, so it needs the folder they are in" + }, + "checksum.TheLengthOfAChecksumSays": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-4add58be1e11", + "other": "the length of a checksum says which algorithm made it, and that is the one worked out to compare" + }, + "checksum.TheMostIsAboutHalfA": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.SumsMostBytes}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-7cc63fb0d3f6", + "other": "the most is {{.SumsMostBytes}}, about half a million files, and a larger file is more likely not a checksum file at all" + }, + "checksum.ThereIsNoAlgorithmCalled": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-6de5decc9e2d", + "other": "there is no algorithm called {{.Name}}" + }, + "checksum.ThereIsNoRoomForSo": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-dfaa099cc3ba", + "other": "there is no room for {{.Path}}, so nothing was written" + }, + "checksum.UnknownLine": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Line}}, {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a8662d5c6650", + "other": "line {{.Line}}: {{.Name}}" + }, + "checksum.UseAlgorithms": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Algorithms}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-34a4c307745f", + "other": "use {{.Algorithms}}, several separated by commas, or all on its own" + }, + "checksum.WaitUntilWhateverIsWritingIt": { + "description": "Said by internal/tool/checksum - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-161d36039e9c", + "other": "wait until whatever is writing it has finished, and run this again" + }, + "core.BoundaryTooLarge": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-7c6185922a32", + "other": "a boundary set needs one size above the limit, and there is no number above this one. Use a limit at least one byte below the largest, or check that the number is the one you meant" + }, + "core.BoundaryTooSmall": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Why}}, {{.Fix}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a0fd66f30b3f", + "other": "{{.Why}}. {{.Fix}}" + }, + "core.BoundaryTooSmallFix": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-d3ccef8f1f9d", + "other": "Use a limit of at least 1 B" + }, + "core.BoundaryTooSmallWhy": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-54b8e801756c", + "other": "the set needs a size one byte below the limit, and there is nothing below zero" + }, + "core.Error": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Error}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-6dc38cd1cb85", + "other": "{{.Error}}" + }, + "core.PlanTooLargeFix": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-cb36e3de1bfb", + "other": "Ask for fewer files, or make each one cheaper to plan - fewer pages, fewer entries - or split the work into several runs" + }, + "core.PlanTooLargeWhy": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Ceiling}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-52bd0c2594f6", + "other": "the whole plan is held in memory before anything is written, and this build works to a ceiling of {{.Ceiling}} for it - how much a file costs to plan depends on the format, so a ceiling on the number of files alone cannot see this" + }, + "core.RangeBackwards": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.From}}, {{.To}}, {{.Low}}, {{.High}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1d0ff9d1af62", + "other": "the range runs backwards, from {{.From}} B down to {{.To}} B. The smaller end comes first, and a range that runs backwards holds no sizes at all. Write it as {{.Low}}-{{.High}}" + }, + "core.RangeNotARange": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-ec7fa6194ef2", + "other": "{{.Value}} is not a range. A range is exactly two sizes with one hyphen between them, such as 1kb-8kb" + }, + "core.SizeBelowZero": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-a2509071b044", + "other": "a file cannot be smaller than zero bytes" + }, + "core.SizeEmpty": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-93de7d541324", + "other": "empty {{.Setting}}: write a number of bytes or a size such as 10mb, 1.5gib or 700kB" + }, + "core.SizeNegative": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-d1fb1aecfb6b", + "other": "{{.Setting}} {{.Value}} is negative: a file cannot be smaller than zero bytes" + }, + "core.SizeNoNumber": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-19e757667a89", + "other": "{{.Setting}} {{.Value}} has no number: write something like 10mb or 1048576" + }, + "core.SizeNotANumber": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-3a5e57f1b8ed", + "other": "{{.Setting}} {{.Value}} is not a number: write something like 10mb, 1.5gib or 1048576" + }, + "core.SizeNotPlain": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-0b892bed8bee", + "other": "{{.Setting}} {{.Value}} is not a plain number: write the digits out, as 10mb, 1.5gib or 1048576. Spellings such as 1e5, 0x10 or 1_000 are refused rather than guessed at, the same as in a recipe" + }, + "core.SizeNotWhole": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Value}}, {{.Exact}}, {{.Below}}, {{.Above}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-e52c1a8c1d1f", + "other": "{{.Setting}} {{.Value}} is not a whole number of bytes: it works out to {{.Exact}} bytes. Ask for {{.Below}} or {{.Above}} instead" + }, + "core.SizeTooLarge": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-dd73ed43bac3", + "other": "{{.Setting}} {{.Value}} is too large to express in bytes" + }, + "core.SizeUnknownUnit": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Value}}, {{.Unit}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-3cd2ef1da79b", + "other": "{{.Setting}} {{.Value}} uses an unknown unit {{.Unit}}: use b, kb, mb, gb or tb, all counting in 1024s, and kib, mib, gib or tib for the same thing spelled out" + }, + "core.SizesOverflow": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-58d85993b3db", + "other": "the sizes in this run add up to more than a number of bytes can hold, so the total cannot be measured and the free space check cannot be trusted. Ask for fewer files or smaller ones" + }, + "core.TooManyFiles": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Why}}, {{.Fix}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a0fd66f30b3f", + "other": "{{.Why}}. {{.Fix}}" + }, + "core.TooManyFilesFix": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-f03079434408", + "other": "Ask for fewer files, or split the work into several runs" + }, + "core.TooManyFilesWhy": { + "description": "Said by internal/core - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Most}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-84bf666cf3a3", + "other": "this build plans at most {{.Most}} files in one run, because the whole plan is worked out in memory before anything is written - that is what lets a run that cannot succeed be refused before the first byte" + }, + "csv.HasToBeOneOf": { + "description": "Said by internal/format/csvfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Values}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-150fed02f771", + "other": "it has to be {{.Values}}" + }, + "csv.MinimumHeader": { + "description": "Said by internal/format/csvfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-002d78cd1305", + "other": "a table holds a header and whole rows, and one of each needs that much" + }, + "csv.MinimumRows": { + "description": "Said by internal/format/csvfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-8feb9483f089", + "other": "a table holds whole rows, and one of them needs that much" + }, + "damage.Conflict": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.What}}, {{.Instead}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-6749ebcea05d", + "other": "{{.What}}. {{.Instead}}" + }, + "damage.ConflictInstead": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-e4ac08c1d195", + "other": "leave expected out and get reject, or write sanitize if the system under test is meant to repair the file, or unspecified if that is the question" + }, + "damage.ConflictWhat": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Outcome}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a9099dc25467", + "other": "this target damages the file and expects {{.Outcome}}" + }, + "damage.ConflictWhy": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-b90221bf868f", + "other": "a damaged file is one a judge was measured to refuse, so expecting it to be accepted is an expectation nothing could meet" + }, + "damage.NoChange": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.What}}, {{.Why}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-14c3ed651919", + "other": "{{.What}}. {{.Why}}" + }, + "damage.NoChangeInstead": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-7b4599e7c06c", + "other": "give the damage a larger file, different settings, or take it off this target" + }, + "damage.NoChangeWhat": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Damage}}, {{.File}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-4f2698e2b793", + "other": "{{.Damage}} changed nothing in {{.File}}" + }, + "damage.NoChangeWhy": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-c7feb2a26728", + "other": "the file would be accepted by every reader while the manifest called it broken, so the run stops rather than writing it" + }, + "damage.TooSmall": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.What}}, {{.Instead}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-6749ebcea05d", + "other": "{{.What}}. {{.Instead}}" + }, + "damage.TooSmallInstead": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Floor}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-cfc27ac4d05a", + "other": "Ask for {{.Floor}} or more, or take the damage off this target." + }, + "damage.TooSmallWhat": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Damage}}, {{.Floor}}, {{.Requested}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1b4679cf4415", + "other": "{{.Damage}} needs at least {{.Floor}} and the file is {{.Requested}}" + }, + "damage.TooSmallWhy": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-17c864ed0120", + "other": "a file smaller than the damage would come out unchanged, and an unchanged file described as broken is the one thing this tool must not write" + }, + "damage.Unknown": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.What}}, {{.Known}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-8ac340989b2f", + "other": "{{.What}}. This build has: {{.Known}}" + }, + "damage.UnknownInstead": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Known}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a53588601259", + "other": "use one of: {{.Known}}" + }, + "damage.UnknownInsteadRemove": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-0e2033823316", + "other": "remove the damage line" + }, + "damage.UnknownWhat": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Damage}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-cfd2875327a9", + "other": "there is no damage called {{.Damage}}" + }, + "damage.UnknownWhy": { + "description": "Said by internal/damage - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-f2720ea32a8a", + "other": "a damage has to be one this build knows, and one it does not would leave the file intact while the manifest called it broken" + }, + "docx.Paragraphs": { + "description": "Said by internal/format/docx - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-45316182b77a", + "one": "{{.Count}} paragraph", + "other": "{{.Count}} paragraphs" + }, + "engine.CannotCreateOutDir": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Dir}}, {{.Cause}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-324037f71709", + "other": "cannot create the output directory {{.Dir}}: {{.Cause}}" + }, + "engine.CannotStart": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Dir}}, {{.Cause}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-114c2e153b9d", + "other": "cannot start a run in {{.Dir}}: {{.Cause}}" + }, + "engine.Character": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Character}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-add48a171334", + "other": "the character {{.Character}}" + }, + "engine.ChooseTheDirectory": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-111bd1b3e020", + "other": "Choose the directory with the output setting instead" + }, + "engine.Collision": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-9b5b48b73ad2", + "other": "{{.Path}} already exists and this run will not write over it. Generate into an empty directory, or remove the file first" + }, + "engine.CollisionInstructions": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-f380a98d3a7f", + "other": "{{.Path}} already exists and this run will not write over it. It says what the files of an earlier run are for. Generate into an empty directory, or name this run's manifest something else" + }, + "engine.CollisionManifest": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-cc110e79b974", + "other": "{{.Path}} already exists and this run will not write over it. It is the only record of what an earlier run wrote, so replacing it would leave those files with nothing to remove them by. Generate into an empty directory, or move the old manifest aside" + }, + "engine.ControlCharacter": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Code}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-519de8af1277", + "other": "a control character, U+{{.Code}}" + }, + "engine.CrashPlanning": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-90f7d5da615f", + "other": "planning a file" + }, + "engine.CrashWhile": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.While}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-071bf3d7823e", + "other": "{{.While}}" + }, + "engine.CrashWriting": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.File}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1e90995301d1", + "other": "writing {{.File}}" + }, + "engine.Files": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-83589d643302", + "one": "{{.Count}} file", + "other": "{{.Count}} files" + }, + "engine.FilesSoFar": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-83589d643302", + "one": "{{.Count}} file", + "other": "{{.Count}} files" + }, + "engine.GeneratorCrash": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.While}}, {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-82716d977373", + "other": "the {{.Format}} generator stopped with an internal error while {{.While}}: {{.Value}}. This is a defect in this tool rather than something a recipe can ask for. Please report it with the settings that produced it" + }, + "engine.IDTwice": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.ID}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-724959681477", + "other": "target id {{.ID}} is used twice" + }, + "engine.IDTwiceWhy": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-ded29dfee98c", + "other": "ids identify targets, so a duplicate is an error rather than a silent overwrite" + }, + "engine.InstructionsNotWritten": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, {{.Cause}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-7a1261279b19", + "other": "cannot write the instructions to {{.Path}}: {{.Cause}}" + }, + "engine.NameAbsolute": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-2dc5711eaa7d", + "other": "{{.Where}} produces the absolute path {{.Name}}" + }, + "engine.NameAbsoluteWhy": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-61f6a0c76deb", + "other": "a recipe carries no absolute paths, because then it only works on the machine it was written on" + }, + "engine.NameColon": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-9346cfb2e4a8", + "other": "{{.Where}} produces the name {{.Name}}, which holds a colon" + }, + "engine.NameColonFix": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-08dfaced15cc", + "other": "Take the colon out, or ask for the file inside an archive where the name survives" + }, + "engine.NameColonWhy": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-f033766b9cf1", + "other": "Windows reads that as a drive or as an alternate data stream rather than as part of the name, so the file arrives called something else or not at all. It is refused on every system so that a recipe means one thing everywhere" + }, + "engine.NameEmpty": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a8beb5c74396", + "other": "{{.Where}} produces a file with no name" + }, + "engine.NameForbidden": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Name}}, {{.Character}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-bf6388aa9fbb", + "other": "{{.Where}} produces the name {{.Name}}, which holds {{.Character}}" + }, + "engine.NameForbiddenFix": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-1d1b81343f2c", + "other": "Take the character out, or ask for the file inside an archive where the name survives" + }, + "engine.NameForbiddenWhy": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-c7a749a53ea6", + "other": "Windows refuses that character in a file name, so the file is not written there at all. It is refused on every system so that a recipe means one thing everywhere" + }, + "engine.NameIsADirectory": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-93b2fa7a0d98", + "other": "{{.Where}} produces the name {{.Name}}, which names a directory rather than a file" + }, + "engine.NameIsAPath": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-934e299eb38c", + "other": "{{.Where}} produces the name {{.Name}}, which is a path rather than a file name" + }, + "engine.NameIsAPathWhy": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-8caafc0ee913", + "other": "names stay inside the output directory, and a separator is refused on every system so that a recipe works everywhere" + }, + "engine.NameIsTheInstructions": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Target}}, {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-dfd7477f00a7", + "other": "target {{.Target}} produces a file named {{.Name}}, and that is the name this run gives the instructions beside its manifest" + }, + "engine.NameIsTheInstructionsWhy": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-496cccf01d31", + "other": "both are written into the output directory, and the instructions say what every file of the run is for, so one of the two would be lost" + }, + "engine.NameIsTheManifest": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Target}}, {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-7ee240c4a6a6", + "other": "target {{.Target}} produces a file named {{.Name}}, and that is the name this run gives its manifest" + }, + "engine.NameIsTheManifestWhy": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-19b2818db573", + "other": "both are written into the output directory, so the file would take the name the manifest needs and the run would end with files and nothing to remove them by" + }, + "engine.NameNul": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-5453f2b2ff0c", + "other": "{{.Where}} produces the name {{.Name}}, which names the null device on Windows rather than a file" + }, + "engine.NameNulFix": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-018e40bbb5cb", + "other": "Give it an extension, nul.txt is an ordinary name, or choose another one" + }, + "engine.NameNulWhy": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-3c22a55bc686", + "other": "writing there succeeds and the bytes go nowhere, so the run would record a file that is not on the disk. It is refused on every system so that a recipe means one thing everywhere" + }, + "engine.NameTemplateFix": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Placeholder}}, {{.Example}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-483ceb34910b", + "other": "The only placeholder is {{.Placeholder}}, so a name looks like invoice_{{.Example}}.pdf" + }, + "engine.NameTemplateUnknown": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Target}}, {{.Template}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-2eaea446230c", + "other": "target {{.Target}} has a name template this build does not understand: {{.Template}}" + }, + "engine.NameTheManifestElse": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Placeholder}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-d5c9c9065282", + "other": "Give the target a name template containing {{.Placeholder}}, or name the manifest something else" + }, + "engine.NameTooLong": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Name}}, {{.Length}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a45124f3b035", + "other": "{{.Where}} produces the name {{.Name}}, which is {{.Length}} bytes long" + }, + "engine.NameTooLongFix": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Most}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-345b133ae94d", + "other": "Shorten it to {{.Most}} bytes or fewer, or ask for the file inside an archive where the name survives" + }, + "engine.NameTooLongWhy": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Most}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-08a626e33035", + "other": "Linux stores at most {{.Most}} bytes in a file name, and a letter outside ASCII takes two to four of them. It is refused on every system so that a recipe means one thing everywhere" + }, + "engine.NameTrailing": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-9e6def79fc88", + "other": "{{.Where}} produces the name {{.Name}}, which ends in a dot or a space" + }, + "engine.NameTrailingFix": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-92a29aace20d", + "other": "Take the last character off, or ask for the file inside an archive where the name survives" + }, + "engine.NameTrailingWhy": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-b080589d9f64", + "other": "Windows stores such a name without it, so the file on disk would not be the file the manifest describes and verify would report both" + }, + "engine.NamesClash": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.First}}, {{.Second}}, {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-64fede2af0f4", + "other": "targets {{.First}} and {{.Second}} both produce a file named {{.Name}}" + }, + "engine.NamesClashCase": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.First}}, {{.Second}}, {{.FirstName}}, {{.SecondName}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-54d889c75915", + "other": "targets {{.First}} and {{.Second}} produce the names {{.FirstName}} and {{.SecondName}}, which differ only in case. Most filesystems treat those as one file, so one would be written over the other and the manifest would describe both" + }, + "engine.NamesClashFix": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Placeholder}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-224675591d56", + "other": "Give one of them a name template containing {{.Placeholder}}" + }, + "engine.NamesClashFolded": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.First}}, {{.Second}}, {{.FirstName}}, {{.SecondName}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-906d3beae424", + "other": "targets {{.First}} and {{.Second}} produce the names {{.FirstName}} and {{.SecondName}}. Those are different letters that mean the same one - the sharp s against ss, the long s against s, a ligature against the letters in it. macOS stores both under one name, so one file would be written over the other and the manifest would describe both" + }, + "engine.NamesClashSpelling": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.First}}, {{.Second}}, {{.FirstName}}, {{.SecondName}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-26f82646a0e5", + "other": "targets {{.First}} and {{.Second}} produce the names {{.FirstName}} and {{.SecondName}}. Those print the same because they are one name spelled two ways, an accented letter against the plain letter with its accent as a separate character. macOS stores both under one name, so one file would be written over the other and the manifest would describe both" + }, + "engine.NoFiles": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Target}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-8adff4655ee0", + "other": "target {{.Target}} asks for 0 files" + }, + "engine.NoFilesFix": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-b43efdfdc1cf", + "other": "Ask for at least one" + }, + "engine.NoID": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-9788e1abb850", + "other": "a target has no id" + }, + "engine.NoIDWhy": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-363605924eb4", + "other": "every target needs a stable id, it anchors the seed and links to the manifest" + }, + "engine.NoOutDir": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-9ff19de0e58b", + "other": "the output directory is empty" + }, + "engine.NoOutDirFix": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-c14426ffc389", + "other": "Name a directory, for example ./fixtures" + }, + "engine.NoSpace": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Needed}}, {{.Dir}}, {{.Free}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-0e6457cf7a3b", + "other": "this run needs {{.Needed}} B and {{.Dir}} has {{.Free}} B free - nothing was written. Ask for fewer files or a smaller size, or write to another disk by changing the output directory" + }, + "engine.NotProduced": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-46fb82f6985b", + "other": "This file was not produced. The run carried on and ended with the partial exit code." + }, + "engine.OutDirIsAFile": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Dir}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-55a340f0b67d", + "other": "the output directory {{.Dir}} is a file, not a directory" + }, + "engine.OutDirIsAFileFix": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-f9e5f71f3822", + "other": "Point the output directory at a directory, or at one that does not exist yet and it will be created" + }, + "engine.PlanTooLarge": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Used}}, {{.Files}}, {{.Ceiling}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-bc308275bb7d", + "other": "the plan for this run has reached {{.Used}} after {{.Files}} and the ceiling is {{.Ceiling}}" + }, + "engine.RefusalFix": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.What}}, {{.Fix}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-fbb793af1f23", + "other": "{{.What}}. {{.Fix}}" + }, + "engine.RefusalWhy": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.What}}, {{.Why}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-657cfcd8b903", + "other": "{{.What}} - {{.Why}}" + }, + "engine.RefusalWhyFix": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.What}}, {{.Why}}, {{.Fix}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-ba47f922ee57", + "other": "{{.What}} - {{.Why}}. {{.Fix}}" + }, + "engine.RunInProgress": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Dir}}, {{.Lock}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-790ae36784b4", + "other": "another run is already writing into {{.Dir}}, so this one will not start. Two runs writing into one directory can write over each other's files without either of them saying so. Wait for it to finish, or generate into a different directory. If nothing is running, that run was killed before it could tidy up - remove {{.Lock}} and try again" + }, + "engine.RunTooLarge": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Target}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-aff817760566", + "other": "target {{.Target}} brings the run to a size that is too large to measure" + }, + "engine.SizeMoved": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-e0c05c6f958b", + "other": "A size drawn from the range is not one this format can write, so the nearest size it can write was used instead. The file is still inside the range that was asked for." + }, + "engine.Target": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Target}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-f4062d7dbd6c", + "other": "target {{.Target}}" + }, + "engine.Targets": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-7f9636c1f771", + "one": "{{.Count}} target", + "other": "{{.Count}} targets" + }, + "engine.TheInstructions": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-074853502278", + "other": "the instructions beside the manifest" + }, + "engine.TheManifest": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-b802ab9f53ca", + "other": "the manifest" + }, + "engine.TooManyFiles": { + "description": "Said by internal/engine - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Files}}, {{.Targets}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a18427eed894", + "other": "this run asks for {{.Files}} across {{.Targets}}" + }, + "format.AAlreadyNeedsThatMuchBefore": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Opts}}, {{.Carrying}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-c48d23d9a134", + "other": "a {{.Opts}}{{.Carrying}} already needs that much before any padding" + }, + "format.ABMPStatesItsOwnSize": { + "description": "Said by internal/format/bmp - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-dbd63989b4da", + "other": "a BMP states its own size in a four byte field, so the format cannot describe a file this large" + }, + "format.AByteOrderMarkIsB": { + "description": "Said by internal/format/textenc - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Mark}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-4efd8df69acd", + "other": "a byte order mark is {{.Mark}} B and the file has to hold it" + }, + "format.ADeckOfAlreadyPackagesTo": { + "description": "Said by internal/format/pptx - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-34617932d22a", + "other": "a deck of {{.Count}} already packages to that much - a presentation needs a master, a layout and a theme whether it shows them or not" + }, + "format.ADocumentHoldsADeclarationA": { + "description": "Said by internal/format/xmlfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-7f2c76645ed2", + "other": "a document holds a declaration, a root element and whole records, and one of each needs that much" + }, + "format.ADocumentHoldsARootKey": { + "description": "Said by internal/format/yamlfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-e288d9c236e7", + "other": "a document holds a root key and whole records, and one of each needs that much" + }, + "format.ADocumentHoldsWholeRecordsAnd": { + "description": "Said by internal/format/jsonfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-c002a9e365fb", + "other": "a document holds whole records, and one {{.Name}} record with every value type needs that much" + }, + "format.ADocumentHoldsWholeRecordsAnd2": { + "description": "Said by internal/format/tomlfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-651b6768a03d", + "other": "a document holds whole records, and one table of them with every value type needs that much" + }, + "format.ADocumentOfAlreadyPackagesTo": { + "description": "Said by internal/format/docx - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-9f7b489fb2f9", + "other": "a document of {{.Count}} already packages to that much, and a Word file cannot leave out its content types or its relationships" + }, + "format.ADrawingHoldsADeclarationA": { + "description": "Said by internal/format/svgfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-bd5f623311d9", + "other": "a drawing holds a declaration, a root element and whole shapes, and one of each needs that much" + }, + "format.AFileCannotHoldFewerThan": { + "description": "Said by internal/format/txt - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-bb90f7e86bf0", + "other": "a file cannot hold fewer than zero bytes" + }, + "format.ALogHoldsWholeEntriesAnd": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.ID}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-6e80433af493", + "other": "a log holds whole entries and one entry in the {{.ID}} shape needs that much" + }, + "format.APasswordWasGivenAndThis": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-9dead9140bc0", + "other": "a password was given and this says the archive is not locked, so one of the two is not what you meant" + }, + "format.ARIFFFileStatesItsOwn": { + "description": "Said by internal/format/wav - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-beaa6ef535a9", + "other": "a RIFF file states its own length in a four byte field, so the format cannot describe a file this large" + }, + "format.ARateSaysHowFastEntries": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-d38f8ddf9901", + "other": "a rate says how fast entries arrive and timestamps=fixed puts them all at one instant" + }, + "format.ASheetOfByAlreadyPackages": { + "description": "Said by internal/format/xlsx - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Rows}}, {{.Columns}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-4246af222774", + "other": "a sheet of {{.Rows}} by {{.Columns}} already packages to that much, and a workbook cannot leave out its content types or its relationships" + }, + "format.ATIFFLocatesItsPixelsWith": { + "description": "Said by internal/format/tiff - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-0e6ed3d21e3b", + "other": "a TIFF locates its pixels with a four byte offset, so the format cannot describe a file this large" + }, + "format.AWAVNeedsItsRIFFHeader": { + "description": "Said by internal/format/wav - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-bca949f9e7fd", + "other": "a WAV needs its RIFF header, its format chunk and a data chunk header before a single sample" + }, + "format.AWebPDeclaresItsLengthIn": { + "description": "Said by internal/format/webp - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-4beea662d79f", + "other": "a WebP declares its length in a four byte field, so the format cannot describe a file this large" + }, + "format.AXIconHoldingAIs": { + "description": "Said by internal/format/ico - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.W}}, {{.H}}, {{.Embed}}, {{.Picture}}, {{.Preamble}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-9aaa563130b9", + "other": "a {{.W}}x{{.H}} icon holding a {{.Embed}} is {{.Picture}} B of picture, and the {{.Preamble}} B directory sits in front of it" + }, + "format.AXPictureAlreadyEncodesTo": { + "description": "Said by internal/format/png - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Width}}, {{.Height}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1ce454591837", + "other": "a {{.Width}}x{{.Height}} picture already encodes to that much before any padding" + }, + "format.AXPictureAtQualityEncodes": { + "description": "Said by internal/format/jpg - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.W}}, {{.H}}, {{.Quality}}, {{.Bare}}, {{.ComOverhead}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-69d0bf02a8f6", + "other": "a {{.W}}x{{.H}} picture at quality {{.Quality}} encodes to {{.Bare}} B and always carries a comment segment, which costs {{.ComOverhead}} B even when empty" + }, + "format.AXPictureEncodesToExactly": { + "description": "Said by internal/format/gif - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Width}}, {{.Height}}, {{.Bare}}, {{.EmptyComment}}, {{.SmallestCarryingComment}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a13cd5c964ec", + "other": "a {{.Width}}x{{.Height}} picture encodes to exactly {{.Bare}} B, an empty comment adds {{.EmptyComment}} B and the smallest comment that carries any padding adds {{.SmallestCarryingComment}} B, so nothing else in between is reachable" + }, + "format.AXPictureEncodesToExactly2": { + "description": "Said by internal/format/png - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.W}}, {{.H}}, {{.Bare}}, {{.ChunkOverhead}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-f4e7a12edc2d", + "other": "a {{.W}}x{{.H}} picture encodes to exactly {{.Bare}} B, and the padding chunk that makes up any difference costs {{.ChunkOverhead}} B on its own, so nothing between those two is reachable" + }, + "format.AXPictureIsBOf": { + "description": "Said by internal/format/bmp - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.W}}, {{.H}}, {{.PixelBytes}}, {{.Headers}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-17ec22f5de5d", + "other": "a {{.W}}x{{.H}} picture is {{.PixelBytes}} B of pixels once each row is rounded up to four bytes, and the {{.Headers}} B header sits in front of it" + }, + "format.AXPictureIsBOf2": { + "description": "Said by internal/format/tiff - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.W}}, {{.H}}, {{.PixelBytes}}, {{.Overhead}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-e4b53453234d", + "other": "a {{.W}}x{{.H}} picture is {{.PixelBytes}} B of pixels at three bytes each, and the header, the directory and its values take another {{.Overhead}} B" + }, + "format.AXPictureIsBOf3": { + "description": "Said by internal/format/webp - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.W}}, {{.H}}, {{.W2}}, {{.Bare}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-eb9dad08cc09", + "other": "a {{.W}}x{{.H}} picture is {{.W2}} B of pixels at three bytes each, and the container and the coding tables take another {{.Bare}} B" + }, + "format.AboveMaximum": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Maximum}}, {{.Why}}, {{.Requested}}, {{.Fix}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-595465db6fd3", + "other": "{{.Format}} cannot be larger than {{.Maximum}} B - {{.Why}}. Requested: {{.Requested}} B. {{.Fix}}" + }, + "format.AboveMaximumWhat": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Maximum}}, {{.Requested}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-543087dbdc70", + "other": "{{.Format}} cannot be larger than {{.Maximum}} B. Requested: {{.Requested}} B" + }, + "format.AnArchiveHoldingAlreadyNeedsThat": { + "description": "Said by internal/format/targz - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Groups}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-944272a047fc", + "other": "an archive holding {{.Groups}} already needs that much, and nothing is compressed" + }, + "format.AnArchiveHoldingAlreadyNeedsThat2": { + "description": "Said by internal/format/zip - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Groups}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-8ae8f9d3962c", + "other": "an archive holding {{.Groups}} already needs that much" + }, + "format.AnArchiveHoldingNeedsThatMuch": { + "description": "Said by internal/format/zip - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Groups}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-9d357c4329f8", + "other": "an archive holding {{.Groups}} needs that much before anything is squeezed, and how far it squeezes is not known until it has been" + }, + "format.AnArchiveThisLargeWouldNeed": { + "description": "Said by internal/format/zip - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-64b57daffe59", + "other": "an archive this large would need the zip64 records, and this build works out an archive's size before it writes it in a way that cannot account for them" + }, + "format.AnIconStatesWhereItsPicture": { + "description": "Said by internal/format/ico - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-89eb5442cbef", + "other": "an icon states where its picture starts in a four byte field, so the format cannot describe a file this large" + }, + "format.AnOfficeFileIsAZIP": { + "description": "Said by internal/format/xlsx - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-7b6cc559cff5", + "other": "an Office file is a ZIP archive, and this build works out its size before it writes it in a way that cannot account for the zip64 records a larger one needs" + }, + "format.AskFor0BOrMore": { + "description": "Said by internal/format/txt - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-d83d8503ba9c", + "other": "Ask for 0 B or more." + }, + "format.AskFor4GiBOrLess": { + "description": "Said by internal/format/bmp - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-00fdb9c1486c", + "other": "Ask for 4 GiB or less, or pick a format with no size field of its own such as gif." + }, + "format.AskFor4GiBOrLess2": { + "description": "Said by internal/format/ico - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-1e9c7b671e63", + "other": "Ask for 4 GiB or less, or pick a format with no offset field of its own such as gif." + }, + "format.AskFor4GiBOrLess3": { + "description": "Said by internal/format/tiff - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-8853202486d3", + "other": "Ask for 4 GiB or less, or pick a format with no offset of its own such as gif." + }, + "format.AskFor4GiBOrLess4": { + "description": "Said by internal/format/wav - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-312924ab27ec", + "other": "Ask for 4 GiB or less, or pick a format with no length field of its own such as txt." + }, + "format.AskFor4GiBOrLess5": { + "description": "Said by internal/format/webp - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-0ecad3b0b8a9", + "other": "Ask for 4 GiB or less, or pick a format with no length field of its own such as gif." + }, + "format.AskForASmallerSizeOr": { + "description": "Said by internal/format/png - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-424ebdc15921", + "other": "Ask for a smaller size, or set larger dimensions so the picture itself carries more of it." + }, + "format.AskForBOrB": { + "description": "Said by internal/format/textenc - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Requested}}, {{.Next}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-04c46025145c", + "other": "Ask for {{.Requested}} B or {{.Next}} B." + }, + "format.AskForBOrLessOr": { + "description": "Said by internal/format/xlsx - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Below}}, {{.Above}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-d86fed5097fb", + "other": "Ask for {{.Below}} B or less, or {{.Above}} B or more." + }, + "format.AskForBOrMore": { + "description": "Said by internal/format/zip - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Min}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-b8c0c503ff88", + "other": "Ask for {{.Min}} B or more." + }, + "format.AskForBOrMore2": { + "description": "Said by internal/format/wav - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Floor}}, {{.CleanHint}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-af279473fd39", + "other": "Ask for {{.Floor}} B or more{{.CleanHint}}." + }, + "format.AskForBOrMoreOr": { + "description": "Said by internal/format/jxl - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Floor}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-208b2345e8bb", + "other": "Ask for {{.Floor}} B or more, or set a smaller width and height, or a lower quality" + }, + "format.AskForBOrMoreOr10": { + "description": "Said by internal/format/zip - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Target}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-b691ac49b317", + "other": "Ask for {{.Target}} B or more, or ask for compression: none." + }, + "format.AskForBOrMoreOr11": { + "description": "Said by internal/format/targz - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Floor}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-f426b70414a5", + "other": "Ask for {{.Floor}} B or more, or hold fewer or smaller files." + }, + "format.AskForBOrMoreOr2": { + "description": "Said by internal/format/webp - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Bare}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-3f92483ea6fc", + "other": "Ask for {{.Bare}} B or more, or set a smaller width and height" + }, + "format.AskForBOrMoreOr3": { + "description": "Said by internal/format/docx - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Bare}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-c6080ef6701f", + "other": "Ask for {{.Bare}} B or more, or set fewer paragraphs" + }, + "format.AskForBOrMoreOr4": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Min}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-647ee0bda35c", + "other": "Ask for {{.Min}} B or more, or choose a shorter entry_format." + }, + "format.AskForBOrMoreOr5": { + "description": "Said by internal/format/pptx - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Bare}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-09e6c08df925", + "other": "Ask for {{.Bare}} B or more, or set fewer slides" + }, + "format.AskForBOrMoreOr6": { + "description": "Said by internal/format/zip - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Bare}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-cf805d0df5e1", + "other": "Ask for {{.Bare}} B or more, or hold fewer or smaller files." + }, + "format.AskForBOrMoreOr7": { + "description": "Said by internal/format/targz - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Bare}}, {{.Bare2}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-b2a756076baf", + "other": "Ask for {{.Bare}} B or more, or keep the label on and any size from {{.Bare2}} B works." + }, + "format.AskForBOrMoreOr8": { + "description": "Said by internal/format/textenc - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Mark}}, {{.SettingBOM}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-8e38c6016502", + "other": "Ask for {{.Mark}} B or more, or turn the {{.SettingBOM}} setting off." + }, + "format.AskForBOrMoreOr9": { + "description": "Said by internal/format/xlsx - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Bare}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a629bffc5f72", + "other": "Ask for {{.Bare}} B or more, or set fewer rows" + }, + "format.AskForBOrMoreSet": { + "description": "Said by internal/format/ico - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Bare}}, {{.EmbedPNG}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-77d3cf72434d", + "other": "Ask for {{.Bare}} B or more, set a smaller width and height, or set embed={{.EmbedPNG}}" + }, + "format.AskForEntriesOrFewer": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.MaxEntries}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-dba2cac4aaed", + "other": "Ask for {{.MaxEntries}} entries or fewer." + }, + "format.AskForExactlyBExactlyB": { + "description": "Said by internal/format/gif - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Bare}}, {{.Bare2}}, {{.Bare3}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-2e1e3ea291d3", + "other": "Ask for exactly {{.Bare}} B, exactly {{.Bare2}} B, or {{.Bare3}} B or more." + }, + "format.AskForExactlyBOrFor": { + "description": "Said by internal/format/wav - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Bare}}, {{.Bare2}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-b926717011c3", + "other": "Ask for exactly {{.Bare}} B or for {{.Bare2}} B or more." + }, + "format.AskForLessThan4GiB": { + "description": "Said by internal/format/xlsx - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-1f62e6b78697", + "other": "Ask for less than 4 GiB, or split the content across several files." + }, + "format.AskForLessThan4GiB2": { + "description": "Said by internal/format/zip - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-a6bbed8e80cd", + "other": "Ask for less than 4 GiB, or split the contents across several archives." + }, + "format.AtBThisFileHasRoom": { + "description": "Said by internal/format/wav - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Bytes}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-672437efe0de", + "other": "At {{.Bytes}} B this file has room for the headers but not for a single audio frame, so it holds no sound. It is still a valid WAV." + }, + "format.BelowMinimum": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Minimum}}, {{.Why}}, {{.Requested}}, {{.Fix}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-5f4163ec5ef3", + "other": "{{.Format}} cannot be smaller than {{.Minimum}} B - {{.Why}}. Requested: {{.Requested}} B. {{.Fix}}" + }, + "format.BelowMinimumWhat": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Minimum}}, {{.Requested}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-951d66fcbb5f", + "other": "{{.Format}} cannot be smaller than {{.Minimum}} B. Requested: {{.Requested}} B" + }, + "format.BothReadersOnThisMachineRefuse": { + "description": "Said by internal/format/tomlfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-0c8d02726af7", + "other": "both readers on this machine refuse a TOML file that opens with a byte order mark, even in UTF-8" + }, + "format.CannotBe": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Key}}, {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-4c716fd14bea", + "other": "{{.Format}}: {{.Key}} cannot be {{.Value}}" + }, + "format.CannotTake": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Key}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-f4cf771e6ee1", + "other": "{{.Format}} cannot take {{.Key}}" + }, + "format.CannotTakeWhy": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.What}}, {{.Why}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-657cfcd8b903", + "other": "{{.What}} - {{.Why}}" + }, + "format.CannotTakeWhyFix": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.What}}, {{.Why}}, {{.Fix}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-ba47f922ee57", + "other": "{{.What}} - {{.Why}}. {{.Fix}}" + }, + "format.ContentsConflict": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Keys}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-c02ff58536fe", + "other": "{{.Format}}: contains and the {{.Keys}} properties both say what the archive holds. Keep contains and drop the properties, or the other way round" + }, + "format.ContentsConflictOne": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Key}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-274eb866ce96", + "other": "{{.Format}}: contains and the {{.Key}} property both say what the archive holds. Keep contains and drop the properties, or the other way round" + }, + "format.CountOf": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Number}}, {{.Unit}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-8efd2508206c", + "other": "{{.Number}} {{.Unit}}" + }, + "format.ExactlyOf": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Number}}, {{.Unit}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-8efd2508206c", + "other": "{{.Number}} {{.Unit}}" + }, + "format.GroupOf": { + "description": "Said by internal/format/zip - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Count}}, {{.Kind}}, {{.Bytes}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-72d3bb0ede42", + "one": "{{.Count}} {{.Kind}} file of {{.Bytes}} B", + "other": "{{.Count}} {{.Kind}} files of {{.Bytes}} B" + }, + "format.Groups": { + "description": "Said by internal/format/zip - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Groups}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-211cf5f97e38", + "other": "{{.Groups}}" + }, + "format.HasToBeWholeNumberOf": { + "description": "Said by internal/format/imagedim - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Unit}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-5755e0fe2273", + "other": "it has to be a whole number of {{.Unit}}" + }, + "format.ItHasToBeAWhole": { + "description": "Said by internal/format/csvfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-4012124b4c7c", + "other": "it has to be a whole number" + }, + "format.ItHasToBeAdvancingOr": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-47f684fea840", + "other": "it has to be advancing or fixed" + }, + "format.ItHasToBeBetweenAnd": { + "description": "Said by internal/format/imagedim - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.MinColumns}}, {{.MaxColumns}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-baa0b5548801", + "other": "it has to be between {{.MinColumns}} and {{.MaxColumns}}" + }, + "format.ItHasToBeGetRead": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-82e0c4023c3b", + "other": "it has to be get, read or mixed" + }, + "format.ItHasToBeLfOr": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-3c7dd4cf3524", + "other": "it has to be lf or crlf" + }, + "format.ItHasToBeOr": { + "description": "Said by internal/format/ico - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.EmbedBMP}}, {{.EmbedPNG}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-fd792a800341", + "other": "it has to be {{.EmbedBMP}} or {{.EmbedPNG}}" + }, + "format.ItHasToBeRealisticQuiet": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-4333eb6e7404", + "other": "it has to be realistic, quiet, errors or debug" + }, + "format.ItHasToBeRealisticSuccess": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-3aa1143e7c48", + "other": "it has to be realistic, success, client-errors or server-errors" + }, + "format.ItHasToBeTrueOr": { + "description": "Said by internal/format/textenc - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-71042abeb07a", + "other": "it has to be true or false" + }, + "format.ItHasToBeV4V6": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-55bcc5393f4f", + "other": "it has to be v4, v6 or mixed" + }, + "format.ItIsNotAPermissionWritten": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-8a4d58bcc9cf", + "other": "it is not a permission written as octal digits" + }, + "format.ItIsNotOneOfThe": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-4478409a7903", + "other": "it is not one of the shapes this format writes" + }, + "format.ItTakesADateWrittenAs": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-26cf0d660406", + "other": "it takes a date written as 2024-02-29, 2024-02-29T13:45:00 or 2024-02-29T13:45:00+02:00, or none" + }, + "format.ItTakesAWholeNumberFrom": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.MaxEntries}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-3333fcc150b7", + "other": "it takes a whole number from 0 to {{.MaxEntries}}" + }, + "format.ItTakesTextAndThisValue": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-fd5ffed3d609", + "other": "it takes text, and this value is not valid UTF-8" + }, + "format.ItTakesTrueOrFalse": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-20eb24e18b8f", + "other": "it takes true or false" + }, + "format.JointEachSide": { + "description": "Said by internal/format/png - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Why}}, {{.Most}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-c105de306e1f", + "other": "{{.Why}}. Each side may go up to {{.Most}}, but not both at once - ask for a smaller pair" + }, + "format.JointSmallerPair": { + "description": "Said by internal/format/jxl - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Why}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a05cb7a754cc", + "other": "{{.Why}}. Ask for a smaller pair" + }, + "format.JointTooMuch": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Asked}}, {{.Limit}}, {{.Why}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-8c4dbfc1e55c", + "other": "together they come to {{.Asked}} and the limit is {{.Limit}}, because {{.Why}}" + }, + "format.MixedPagesNeedAtLeastTwo": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-338f9f2bd35e", + "other": "mixed pages need at least two pages and this document has one" + }, + "format.NeitherTarNorGzipHasAny": { + "description": "Said by internal/format/targz - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-b3831ecb57f7", + "other": "neither tar nor gzip has any encryption in it, so there is no field in either one to put a password in" + }, + "format.NeitherTarNorGzipHasAny2": { + "description": "Said by internal/format/targz - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-92f4ef797f00", + "other": "neither tar nor gzip has any encryption in it, so there is nothing to choose between" + }, + "format.NestingUnsupported": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Inner}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-f2cd29c6111d", + "other": "{{.Format}} cannot hold {{.Inner}} yet - an archive inside an archive needs a depth limit first. Hold a different format, or build the inner archive as its own target" + }, + "format.NoCalendarHasThatDayTime": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-4771df073166", + "other": "no calendar has that day, time or zone" + }, + "format.NoSuchProperty": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Key}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-d6b4359c3811", + "other": "{{.Format}} does not have a property called {{.Key}}" + }, + "format.NoSuchPropertyKnown": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.What}}, {{.Known}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-129d78741cc6", + "other": "{{.What}}. It takes: {{.Known}}" + }, + "format.NotAContainer": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-4bbda5317b35", + "other": "{{.Format}} holds no other files, so it cannot take contains" + }, + "format.NotAContainerFix": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-2d1a3a9567c1", + "other": "Drop contains, or change the format" + }, + "format.NotAContainerWhole": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.What}}, {{.Why}}, {{.Fix}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-ba47f922ee57", + "other": "{{.What}} - {{.Why}}. {{.Fix}}" + }, + "format.NotAContainerWhy": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Containers}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-ac844612fe99", + "other": "the formats that can are {{.Containers}}" + }, + "format.RemoveTheLine": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-14c26fa261f4", + "other": "remove the line" + }, + "format.SetPagesTo2OrMore": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Other}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-92ea12d95952", + "other": "Set pages to 2 or more, or {{.Other}}." + }, + "format.Setting": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1a0545a47535", + "other": "{{.Name}}" + }, + "format.StoresTwoBytesForEveryCharacter": { + "description": "Said by internal/format/textenc - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-0e9ed2b9d1c1", + "other": "{{.Name}} stores two bytes for every character, so a whole file always has an even number of them" + }, + "format.TOMLIsUTF8ByIts": { + "description": "Said by internal/format/tomlfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-ece7fa6fa508", + "other": "TOML is UTF-8 by its own specification, so there is no other encoding for a document to be in" + }, + "format.TakesNoProperties": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Key}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-68129a25e9b5", + "other": "{{.Format}} takes no properties, so {{.Key}} is not one of them" + }, + "format.TakesOneOf": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Values}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-bc4247426c03", + "other": "it takes one of: {{.Values}}" + }, + "format.TakesRange": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Least}}, {{.Most}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-46bf5c66d883", + "other": "it takes a whole number from {{.Least}} to {{.Most}}" + }, + "format.TakesRangeOf": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Unit}}, {{.Least}}, {{.Most}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a381ff9c7edc", + "other": "it takes a whole number of {{.Unit}} from {{.Least}} to {{.Most}}" + }, + "format.TakesSize": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-df427136497a", + "other": "it takes a size such as 2mb, or a plain byte count" + }, + "format.TakesTrueOrFalse": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-20eb24e18b8f", + "other": "it takes true or false" + }, + "format.TakesWholeNumber": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-b075aeaef437", + "other": "it takes a whole number" + }, + "format.TakesWholeNumberOf": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Unit}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1978d684bebc", + "other": "it takes a whole number of {{.Unit}}" + }, + "format.TargzThePaddingOfThisCompressed": { + "description": "Said by internal/format/targz - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.SolveRounds}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-aacb4fd044e0", + "other": "targz: the padding of this compressed archive does not settle after {{.SolveRounds}} rounds. Ask for a different size, or for compression: none" + }, + "format.ThatIsWhatTheContentsCome": { + "description": "Said by internal/format/targz - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-5a2c6dc3bbe0", + "other": "that is what the contents come to once they are compressed, so nothing can be taken away" + }, + "format.TheArchiveCommentCarriesAtMost": { + "description": "Said by internal/format/xlsx - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.CommentLimit}}, {{.FillerOverhead}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-2b5084f09b68", + "other": "the archive comment carries at most {{.CommentLimit}} B and the smallest extra part costs {{.FillerOverhead}} B, so nothing between those two is reachable" + }, + "format.TheDrawingIsPxTallAnd": { + "description": "Said by internal/format/svgfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.H}}, {{.TextBand}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-39369dd5d5ed", + "other": "The drawing is {{.H}} px tall and the label needs the {{.TextBand}} px strip along the bottom, so this file carries no visible label. Its name and the manifest still identify it." + }, + "format.TheIconIsPxWideAnd": { + "description": "Said by internal/format/ico - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.W}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-aa4c94da4914", + "other": "The icon is {{.W}} px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it." + }, + "format.TheLabelCommentNeedsBAnd": { + "description": "Said by internal/format/yamlfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Length}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-e2a47de40153", + "other": "The label comment needs {{.Length}} B and this file has no room for it beside a whole record. Its name and the manifest still identify it." + }, + "format.TheLabelLineNeedsBAnd": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Length}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-2ad4fd6ac4b9", + "other": "The label line needs {{.Length}} B and this file has no room for it beside a whole entry. Its name and the manifest still identify it." + }, + "format.TheLabelNeedsBAndThe": { + "description": "Said by internal/format/txt - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Length}}, {{.Bytes}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-12f3f0aa811c", + "other": "The label needs {{.Length}} B and the file is {{.Bytes}} B, so this file carries no label. Its name and the manifest still identify it." + }, + "format.TheLabelNeedsBAndThis": { + "description": "Said by internal/format/htmlfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Length}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-009ae9e3d815", + "other": "The label needs {{.Length}} B and this file has no room for it beside a whole block. Its name and the manifest still identify it." + }, + "format.TheLabelNeedsBAndThis2": { + "description": "Said by internal/format/svgfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Length}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-734d54b4fe08", + "other": "The label needs {{.Length}} B and this file has no room for it beside a whole shape. Its name and the manifest still identify it." + }, + "format.ThePaddingEntryTheArchiveNeeds": { + "description": "Said by internal/format/zip - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-307f2bf5a35d", + "other": "the padding entry the archive needs at this size does not fit" + }, + "format.ThePaddingNeededIsLargerThan": { + "description": "Said by internal/format/png - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-cf2e87fbb851", + "other": "the padding needed is larger than one chunk can carry, and this generator writes only one" + }, + "format.ThePaddingThatWouldMakeUp": { + "description": "Said by internal/format/targz - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-303f9f27491e", + "other": "the padding that would make up the difference cannot be one byte long, so this size sits in a gap just above the smallest archive" + }, + "format.ThePictureIsPxWideAnd": { + "description": "Said by internal/format/webp - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Width}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-5eaf84a1feb2", + "other": "The picture is {{.Width}} px wide and the label needs more room, so this file carries no visible label. Its name and the manifest still identify it." + }, + "format.TheSizeIsBeingLeftTo": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-e8e5faeb4de3", + "other": "the size is being left to the contents, and how far they compress is only known once they have been compressed" + }, + "format.TheseContentsComeOutOfThe": { + "description": "Said by internal/format/zip - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-10c68148e9e7", + "other": "these contents come out of the compressor larger than they went in, so the archive cannot be made this small once they are squeezed" + }, + "format.ThisDocumentIsExactlyBAnd": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Bare}}, {{.MinComment}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-c09bf7afd65a", + "other": "this document is exactly {{.Bare}} B and the shortest comment that could pad it is {{.MinComment}} B, so one byte more than the document is the single size in between that cannot be reached" + }, + "format.ThisSizeNeedsAPaddingChunk": { + "description": "Said by internal/format/wav - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.ChunkHeader}}, {{.Fixed}}, {{.Fixed2}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-d2ded7f9dbf9", + "other": "this size needs a padding chunk and the smallest one costs {{.ChunkHeader}} B, so nothing between {{.Fixed}} and {{.Fixed2}} B can be reached" + }, + "format.TwoSettings": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Of}}, {{.By}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a61ae8d11eb8", + "other": "{{.Of}} and {{.By}}" + }, + "format.Unknown": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Known}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-11137b9e59cb", + "other": "unknown format {{.Format}}. Known formats: {{.Known}}" + }, + "format.UnknownPropertyWhy": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-90dde87dda4b", + "other": "a format takes only the settings it declares, and one it does not know would be dropped on the way" + }, + "format.UseOneOf": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Known}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a53588601259", + "other": "use one of: {{.Known}}" + }, + "format.UseTxtOrMdForA": { + "description": "Said by internal/format/tomlfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-9b7a68f87337", + "other": "Use txt or md for a file that opens with a byte order mark." + }, + "format.UseYamlXmlTxtOrMd": { + "description": "Said by internal/format/tomlfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-4172b81dc9c5", + "other": "Use yaml, xml, txt or md for a file in another encoding." + }, + "format.UseZipForAnArchiveWith": { + "description": "Said by internal/format/targz - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-f8842d3575f8", + "other": "Use zip for an archive with a password, or leave this one open." + }, + "format.ValueRefused": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Value}}, {{.Why}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-3fba4a321577", + "other": "{{.Format}}: {{.Setting}} cannot be {{.Value}} - {{.Why}}" + }, + "format.ValueRefusedBare": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Subject}}, {{.Value}}, {{.Why}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-9b1b8007064f", + "other": "{{.Subject}} cannot be {{.Value}} - {{.Why}}" + }, + "format.WriteADateThatExistsSuch": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-324ab7f056b1", + "other": "Write a date that exists, such as 2024-02-29T13:45:00+02:00." + }, + "format.WriteAValue": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-134396e545bd", + "other": "write a value it takes, or leave the line out and the format works it out" + }, + "format.WriteAValueSuchAs": { + "description": "Said by internal/format - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Default}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-b91dd569f6d3", + "other": "write a value it takes, such as {{.Default}}, or leave the line out" + }, + "format.WriteItTheWayChmodTakes": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-233e48fc5f2d", + "other": "Write it the way chmod takes it, so 644 or 755." + }, + "format.WriteItTheWayTheSetting": { + "description": "Said by internal/format/archive - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-59a86a136c8b", + "other": "Write it the way the setting is declared, so root or user, or leave the line out and the entries carry no owner." + }, + "format.WriteTheDateInOneOf": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-95e8e2f5f105", + "other": "Write the date in one of those three ways, with no fraction of a second." + }, + "format.WriteTheValueInUTF8": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-28353b79a463", + "other": "Write the value in UTF-8." + }, + "format.ZipTheDirectoryCouldNotBe": { + "description": "Said by internal/format/zip - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Name}}, {{.Err}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-7eb26b5ea426", + "other": "zip: the directory {{.Name}} could not be named: {{.Err}}" + }, + "format.ZipTheFileInsideCannotBe": { + "description": "Said by internal/format/zip - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.Err}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-8f5ef3b82380", + "other": "zip: the {{.Format}} file inside cannot be made: {{.Err}}" + }, + "html.HasToBeStructure": { + "description": "Said by internal/format/htmlfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Document}}, {{.Fragment}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1a83745fa4b2", + "other": "it has to be {{.Document}} or {{.Fragment}}" + }, + "html.MinimumFragment": { + "description": "Said by internal/format/htmlfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-e716d4f229d7", + "other": "a fragment holds whole blocks, and one of them needs that much" + }, + "html.MinimumPage": { + "description": "Said by internal/format/htmlfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-3745440d090a", + "other": "a page holds a head, a body and whole blocks, and one of each needs that much" + }, + "json.HasToBeStyle": { + "description": "Said by internal/format/jsonfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Indented}}, {{.Minified}}, {{.RecordPerLine}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-c1343585d922", + "other": "it has to be {{.Indented}}, {{.Minified}} or {{.RecordPerLine}}" + }, + "jxl.MinimumReason": { + "description": "Said by internal/format/jxl - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Coded}}, {{.Container}}, {{.Box}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-8031f7cf3cc6", + "other": "the smallest picture this format draws codes to {{.Coded}} B at worst, the container around it costs {{.Container}} B, and the file always carries a free box, which costs {{.Box}} B even when it holds nothing" + }, + "jxl.MinimumReasonOwn": { + "description": "Said by internal/format/jxl - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Width}}, {{.Height}}, {{.Quality}}, {{.Coded}}, {{.Container}}, {{.Box}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-08ce5283cdaa", + "other": "a {{.Width}}x{{.Height}} picture at quality {{.Quality}} codes to {{.Coded}} B, the container around it costs {{.Container}} B, and the file always carries a free box, which costs {{.Box}} B even when it holds nothing" + }, + "log.ClientAddress": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-df4616586839", + "other": "client address" + }, + "log.ConflictDrop": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Why}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-9d99490328fe", + "other": "{{.Why}}. Drop one of the two, or change the other" + }, + "log.RequestMethod": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-eaa67a9c3b30", + "other": "request method" + }, + "log.ShapeCarriesNo": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Shape}}, {{.What}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-70d92750429d", + "other": "the {{.Shape}} shape carries no {{.What}}" + }, + "log.ShapeNoResponseCode": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Shape}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-33e6f87fa6e4", + "other": "the {{.Shape}} shape carries no response code" + }, + "log.ShapeNoSeverity": { + "description": "Said by internal/format/logfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Shape}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-24f6ca4ac678", + "other": "the {{.Shape}} shape carries no severity" + }, + "manifest.EntryHasThePathWhichLands": { + "description": "Said by internal/manifest - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.I}}, {{.Path}}, {{.Problem}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-79cbc1e59f3f", + "other": "entry {{.I}} has the path {{.Path}}, which lands outside the directory the manifest describes - {{.Problem}}. This tool never reads or removes anything outside that directory, so a manifest that asks it to is one it will not act on. Use the manifest the run actually wrote, or correct the path to one inside the directory" + }, + "manifest.Files": { + "description": "Said by internal/manifest - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-83589d643302", + "one": "{{.Count}} file", + "other": "{{.Count}} files" + }, + "manifest.ItCarriesNoManifestVersionSo": { + "description": "Said by internal/manifest - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-00841a615039", + "other": "it carries no manifest_version, so it is not a manifest this tool wrote" + }, + "manifest.ItIsSchemaVersionAndThis": { + "description": "Said by internal/manifest - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.ManifestVersion}}, {{.Version}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-57828974a0e1", + "other": "it is schema version {{.ManifestVersion}} and this build reads {{.Version}}. Use the version of tfg that wrote it" + }, + "manifest.NoteMany": { + "description": "Said by internal/manifest - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Files}}, {{.Note}}, {{.Named}}, {{.Hidden}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-c4c8340dd2e0", + "other": "{{.Files}}: {{.Note}} Named: {{.Named}}. {{.Hidden}} not named here." + }, + "manifest.NoteOne": { + "description": "Said by internal/manifest - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.File}}, {{.Note}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-9d548db5c58a", + "other": "{{.File}}: {{.Note}}" + }, + "manifest.NoteSeveral": { + "description": "Said by internal/manifest - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Files}}, {{.Note}}, {{.Named}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-d0803401c505", + "other": "{{.Files}}: {{.Note}} Named: {{.Named}}." + }, + "manifest.NoteText": { + "description": "Said by internal/manifest - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Text}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-e75b463d20f0", + "other": "{{.Text}}" + }, + "manifest.RunInstructionsIsAndItCan": { + "description": "Said by internal/manifest - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Name}}, {{.InstructionsSuffix}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1c92a916a2de", + "other": "run.instructions is {{.Name}}, and it can only be the name of a file beside the manifest ending in {{.InstructionsSuffix}}. This tool removes that file with the manifest, so it will not act on one that names anything else. Use the manifest the run actually wrote, or remove the key" + }, + "manifest.TooLarge": { + "description": "Said by internal/manifest - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, {{.Bytes}}, {{.Limit}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-e12fe7ac5071", + "other": "{{.Path}} is {{.Bytes}} B and the limit is {{.Limit}} B. A manifest is read into memory to be compared against a directory, so an unbounded one is a way to exhaust it. Check that this is a manifest this tool wrote, or split the run it describes" + }, + "manifest.Unreadable": { + "description": "Said by internal/manifest - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Path}}, {{.Detail}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-bb90fafec850", + "other": "{{.Path}} cannot be read as a manifest: {{.Detail}}" + }, + "pdf.Carrying": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.What}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-83e9af0cb805", + "other": " carrying {{.What}}" + }, + "pdf.ChooseOneSize": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-61da6826cc02", + "other": "choose one size such as a4" + }, + "pdf.ChooseOneWay": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Portrait}}, {{.Landscape}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-5f59b0be4c6d", + "other": "choose {{.Portrait}} or {{.Landscape}}" + }, + "pdf.Document": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Pages}}, {{.Size}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-17da9045f3c4", + "other": "{{.Pages}} page {{.Size}} document" + }, + "pdf.DocumentLandscape": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Pages}}, {{.Size}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-46b233374a70", + "other": "{{.Pages}} page {{.Size}} landscape document" + }, + "pdf.DocumentMixed": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Pages}}, {{.Size}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-5f511b4e2ed1", + "other": "{{.Pages}} page {{.Size}} upright and landscape document" + }, + "pdf.HintAll": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-447a5ca57f7b", + "other": ", ask for fewer pages by setting pages to 1, shorten the document properties, or drop the label" + }, + "pdf.HintLabel": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-142534ffd01d", + "other": ", ask for fewer pages by setting pages to 1, or drop the label" + }, + "pdf.HintPages": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-6f80cbb77708", + "other": " or ask for fewer pages by setting pages to 1" + }, + "pdf.HintProperties": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-4ac9a00fbf06", + "other": ", ask for fewer pages by setting pages to 1, or shorten the document properties" + }, + "pdf.MixedSize": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-20964137735f", + "other": "mixed size" + }, + "pdf.Size": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Size}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a7df568dcb25", + "other": "{{.Size}}" + }, + "pdf.TheLabel": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-a51c21f8bbe2", + "other": "the self describing label" + }, + "pdf.TheProperties": { + "description": "Said by internal/format/pdf - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-a566d1539507", + "other": "the document properties that were set" + }, + "pptx.Slides": { + "description": "Said by internal/format/pptx - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-05da85d44c19", + "one": "{{.Count}} slide", + "other": "{{.Count}} slides" + }, + "preset.ADistanceFromTheLimitHas": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-3fad7b38f5ae", + "other": "a distance from the limit has to be more than nothing" + }, + "preset.ARecipeCanBuildOnlyOn": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-b96ec4594ce9", + "other": "a recipe can build only on a preset the tool it runs on knows" + }, + "preset.AlreadyMeansEveryFormatSoIt": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.EveryFormat}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-af17cbc9bca4", + "other": "{{.EveryFormat}} already means every format, so it cannot stand beside a named one. Write it on its own, or name only the formats you want" + }, + "preset.AnExtensionIsWrittenWithoutIts": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.TrimPrefix}}, {{.Item}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-098698484a13", + "other": "an extension is written without its dot, so {{.TrimPrefix}} rather than {{.Item}}" + }, + "preset.BulkNotWhole": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Param}}, {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-8dd30727905b", + "other": "{{.Param}}: {{.Value}} is not a whole number of files" + }, + "preset.BulkNought": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-9f12af8e80c4", + "other": "bulk is nought, so this set holds no mass upload." + }, + "preset.CannotBuild": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Preset}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-6ae81854228f", + "other": "the preset {{.Preset}} cannot build this set" + }, + "preset.Combinations": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-89e8d79ad737", + "one": "{{.Count}} combination", + "other": "{{.Count}} combinations" + }, + "preset.Defaulted": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Text}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-e75b463d20f0", + "other": "{{.Text}}" + }, + "preset.EncodingLeftOut": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Refused}}, {{.Holds}}, {{.Described}}, {{.Left}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-bdf71c709021", + "other": "this build refuses {{.Refused}} of the encoding set, so it holds {{.Holds}} rather than {{.Described}}. Left out: {{.Left}}." + }, + "preset.EncodingNoGrid": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Encodings}}, {{.LineEndings}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-587807703cd8", + "other": "no format in this build carries an encoding and a line ending at once, so the two halves of this set are separate files rather than one grid. Encodings: {{.Encodings}}. Line endings: {{.LineEndings}}." + }, + "preset.ExtendsNamesPresetWhichThisBuild": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.ID}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-be91635f052f", + "other": "extends names preset:{{.ID}}, which this build does not have" + }, + "preset.FarOverOff": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-767f6cd55319", + "other": "far-over is off, so nothing in this set is well past the limit. The largest file is one byte over it." + }, + "preset.Files": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-83589d643302", + "one": "{{.Count}} file", + "other": "{{.Count}} files" + }, + "preset.ImpossibleWhy": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Preset}}, {{.Why}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1bf156d5da79", + "other": "the preset {{.Preset}} cannot build this set - {{.Why}}." + }, + "preset.ImpossibleWhyFix": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Preset}}, {{.Why}}, {{.Fix}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-568852ea20d3", + "other": "the preset {{.Preset}} cannot build this set - {{.Why}}. {{.Fix}}" + }, + "preset.IsAllowedAndIsDeniedAnd": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.ID}}, {{.Written}}, {{.Extension}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-ea49ead8ea31", + "other": "{{.ID}} is allowed and {{.Written}} is denied, and both name a file ending {{.Extension}} - so the set would hold one of them to be taken and one to be turned away" + }, + "preset.ItHasToBeANumber": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-d6de05796906", + "other": "it has to be a number of times the limit, written with an x - 2x or 10x - or off" + }, + "preset.ItHoldsAndADistanceIs": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Bad}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-e721e19decc5", + "other": "it holds {{.Bad}}, and a distance is written with digits, letters and a dot - such as 1kb, 512 or 1.5mb. Its text becomes the name of a file" + }, + "preset.ItHoldsAndAnExtensionIs": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Bad}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-15909d4361fa", + "other": "it holds {{.Bad}}, and an extension is written with letters and digits - such as exe, sh or svg. Its text becomes the end of a file name" + }, + "preset.ItIsCharactersLongAndAn": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Length}}, {{.LongestExtension}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-78d81ba1a16a", + "other": "it is {{.Length}} characters long and an extension here is at most {{.LongestExtension}}" + }, + "preset.ItIsTheSameDistanceAs": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.First}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-4ad253058b9f", + "other": "it is the same distance as {{.First}} and the set would hold that step twice. Every distance has to be different, because each one names one file either side of the limit" + }, + "preset.ItIsTheSameExtensionAs": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.First}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-7fa2a1b46c69", + "other": "it is the same extension as {{.First}} and the set would hold that file twice. Every denied extension appears once, because each one stands for one rule your form has" + }, + "preset.ItIsTheSameFormatAs": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.First}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-4504e5d7a37e", + "other": "it is the same format as {{.First}} and the set would hold that file twice. Every format appears once, because each one stands for one path through your reader" + }, + "preset.ItIsTheSameTypeAs": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.First}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-e66f4431f6ca", + "other": "it is the same type as {{.First}} and the set would hold that file twice. Every allowed type appears once, because each one stands for one path through your form" + }, + "preset.ItsExtensionIsTooLongFor": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Ext}}, {{.Bytes}}, {{.ID}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-f5d7c69e3a4a", + "other": "its extension {{.Ext}} is too long for a name of {{.Bytes}} bytes, which the file {{.ID}} is about. Choose a format with a shorter extension" + }, + "preset.LeftOutAs": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Format}}, {{.As}}, {{.Why}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-f3020fe59d49", + "other": "{{.Format}} as {{.As}} ({{.Why}})" + }, + "preset.LimitUnusable": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Param}}, {{.Bad}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-5cf12375b81f", + "other": "{{.Param}}: it holds {{.Bad}}, and a limit is written with digits, letters and a dot - such as 10mb, 512 or 1.5gb. Its text becomes part of every file name" + }, + "preset.NoDistancesWereGivenSoThere": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-5493a98292c6", + "other": "no distances were given, so there is nothing either side of the limit" + }, + "preset.NoEmptyForm": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Formats}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-75baf9544d6e", + "other": "no format in this set has a legal empty form, so the set holds no empty files - only the smallest valid one of each. The formats that go down to nought bytes in this build: {{.Formats}}." + }, + "preset.NoExtensionsWereDeniedSoThere": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-732512c09653", + "other": "no extensions were denied, so there is nothing for the form to turn away" + }, + "preset.NoFormatInThisBuildCarries": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-4a6750d6f63d", + "other": "no format in this build carries an encoding or a line ending, so there is no set to build" + }, + "preset.NoFormatsWereGivenSoThere": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-4cdc44101684", + "other": "no formats were given, so there is nothing to build the set from" + }, + "preset.NoSuchParameter": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Preset}}, {{.Name}}, {{.Known}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-929a6264f65a", + "other": "the preset {{.Preset}} does not have a parameter called {{.Name}}. It takes: {{.Known}}" + }, + "preset.NoTypesWereAllowedSoThe": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-bb028b47b8fc", + "other": "no types were allowed, so the set has no positive control and no file to name wrongly" + }, + "preset.OneAllowed": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-bbfaa4682425", + "other": "only one type is allowed, so the set holds no file named as one allowed type and filled with another - that needs two." + }, + "preset.ParamCause": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Param}}, {{.Cause}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-db5c6acdbb8d", + "other": "{{.Param}}: {{.Cause}}" + }, + "preset.RaiseTheSettingAboveBNarrow": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Floor}}, {{.Limit}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1e2c92f16629", + "other": "Raise the {{.Setting}} above {{.Floor}} B, narrow the spread, or choose a format with a smaller minimum. The {{.Setting}} asked for was {{.Limit}} B." + }, + "preset.RaiseTheSettingToBOr": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Need}}, {{.ID}}, {{.Limit}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-588c9bcb6cc3", + "other": "Raise the {{.Setting}} to {{.Need}} B or more, or take {{.ID}} out of the allowed types. The {{.Setting}} asked for was {{.Limit}} B." + }, + "preset.Refused": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Text}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-e75b463d20f0", + "other": "{{.Text}}" + }, + "preset.RemoveTheLine": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-14c26fa261f4", + "other": "remove the line" + }, + "preset.RunPresetShow": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Preset}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-fe7d5528fb3b", + "other": "run \"tfg preset show {{.Preset}}\" for what it takes" + }, + "preset.RunTfgFormatsForTheList": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-923b9bd13d26", + "other": "run \"tfg formats\" for the list" + }, + "preset.RunTfgFormatsToSeeWhat": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-f047d0599c10", + "other": "Run \"tfg formats\" to see what this build has." + }, + "preset.RunTfgPresetListForThe": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-25a1a032c139", + "other": "run \"tfg preset list\" for the ids this build has" + }, + "preset.SetTheSettingToBOr": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Size}}, {{.Minimum}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-5a47f6cb7b98", + "other": "Set the {{.Setting}} to {{.Size}} B or {{.Minimum}} B." + }, + "preset.SetTheSettingToBOr2": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Floor}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-c3ba9e8a8eb4", + "other": "Set the {{.Setting}} to {{.Floor}} B or more." + }, + "preset.StandIns": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Formats}}, {{.Names}}, {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a86a61e3c5aa", + "one": "this build has no format called {{.Formats}}, so {{.Names}} holds plain text under that name. That tests a form reading the end of a name, not one reading what is inside.", + "other": "this build has no format called {{.Formats}}, so {{.Names}} hold plain text under those names. That tests a form reading the end of a name, not one reading what is inside." + }, + "preset.StepBelowFloor": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Step}}, {{.Size}}, {{.Format}}, {{.Floor}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-170136bfee7f", + "other": "{{.Step}} would be {{.Size}} B and the smallest {{.Format}} this build makes is {{.Floor}} B" + }, + "preset.StepBelowZero": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Step}}, {{.Size}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-4a4ed15f64f3", + "other": "{{.Step}} would be {{.Size}} B, and a file cannot be smaller than nothing" + }, + "preset.TakeOutOfTheListOr": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.ID}}, {{.AllowParam}}, {{.Written}}, {{.DenyParam}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-3a45bd78cd55", + "other": "Take {{.ID}} out of the {{.AllowParam}} list, or {{.Written}} out of the {{.DenyParam}} list." + }, + "preset.TakesNoParameters": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Preset}}, {{.Name}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-10127e9faa16", + "other": "the preset {{.Preset}} takes no parameters, so {{.Name}} is not one of them" + }, + "preset.ThePresetGivesEveryFileIt": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-0bd3908db204", + "other": "the preset gives every file it builds this format, so it has to be one the build can write" + }, + "preset.ThePresetRefusedWhatTheRecipe": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-46f82ba266de", + "other": "the preset refused what the recipe gave it" + }, + "preset.ThePresetTakes": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Join}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-078daea8c1a6", + "other": "the preset takes: {{.Join}}" + }, + "preset.TheSetHoldsFilesInAnd": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Name}}, {{.Reason}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-770eaf5a913a", + "other": "the set holds files in {{.Name}}, and {{.Reason}}" + }, + "preset.ThisBuildHas": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Join}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-3787cbba4f98", + "other": "this build has: {{.Join}}" + }, + "preset.ThisBuildHasNoFormatCalled": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Join}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-758e0bae8496", + "other": "this build has no format called that. It has: {{.Join}}" + }, + "preset.Unknown": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Preset}}, {{.Known}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-535352e45ee2", + "other": "unknown preset {{.Preset}}. This build has: {{.Known}}" + }, + "preset.UnknownNone": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Preset}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1ad71c52426d", + "other": "unknown preset {{.Preset}}, and this build registers none" + }, + "preset.WithCannotBe": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Key}}, {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-0e6c86d85911", + "other": "with.{{.Key}} cannot be {{.Value}}" + }, + "preset.WithFillsTheParametersThePreset": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-647f0f97fb96", + "other": "with fills the parameters the preset declares, and this is not one of them" + }, + "preset.WithFormatNamesWhichThisBuild": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.ID}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-d497cb42a8d9", + "other": "with.format names {{.ID}}, which this build does not have" + }, + "preset.WithNamesWhichThePresetDoes": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Name}}, {{.Preset}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-751e28920004", + "other": "with names {{.Name}}, which the preset {{.Preset}} does not take" + }, + "preset.WouldBeBAndTheSmallest": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.ID}}, {{.Bytes}}, {{.ID2}}, {{.Floor}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1879f6cbbc78", + "other": "{{.ID}} would be {{.Bytes}} B and the smallest {{.ID2}} this build makes is {{.Floor}} B" + }, + "preset.WrittenAsCannotBeSmallerThan": { + "description": "Said by internal/preset - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.ID}}, {{.Label}}, {{.Floor}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-7691ec217cc8", + "other": "{{.ID}} written as {{.Label}} cannot be smaller than {{.Floor}} B, and every file of this set is the same size" + }, + "recipe.ABoundaryAlreadyDecidesTheSizes": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-b6aeb28edc93", + "other": "a boundary already decides the sizes, so {{.Setting}} beside it means two different things at once" + }, + "recipe.ABoundarySetIsExactlyThree": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-562f950f5d71", + "other": "a boundary set is exactly three files, one below the limit, one at it and one above" + }, + "recipe.AContainerCannotHoldFewerThan": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-e6eeafdabc57", + "other": "a container cannot hold fewer than no files" + }, + "recipe.AContainerHoldsRealFilesSo": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-134f67142edb", + "other": "a container holds real files, so each group says which format its files are" + }, + "recipe.AContainsEntryDescribesFilesWith": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-0c0612b3ccd3", + "other": "a contains entry describes files with a format, a count and a size, and anything else would be dropped on the way" + }, + "recipe.ADamageIsWrittenAsIts": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-10250751f233", + "other": "a damage is written as its name, or as a mapping with type and the settings it takes" + }, + "recipe.ADamageSettingTakesOneValue": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-c3de566b6c59", + "other": "a damage setting takes one value, the way a format property does" + }, + "recipe.ADamageWrittenAsAMapping": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-8f9dc4da5556", + "other": "a damage written as a mapping names itself with type" + }, + "recipe.AFormatIsTheIdOf": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-c6b847dd03df", + "other": "a format is the id of a format this build supports" + }, + "recipe.AFormatPropertyIsASingle": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-9a608b215b35", + "other": "a format property is a single value" + }, + "recipe.APresetContributesTargetsAndNothing": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-91787ca07023", + "other": "a preset contributes targets and nothing else, so that the seed, the defaults and the output section are always the file's own" + }, + "recipe.APresetSTargetsWereSupplied": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-c52ddca5c58e", + "other": "a preset's targets were supplied to the reader, and the recipe names none" + }, + "recipe.ARangeIsTwoSizesWith": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-5a18946381db", + "other": "a range is two sizes with a hyphen between them" + }, + "recipe.AReasonIsOneValueFrom": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-0111a952089c", + "other": "a reason is one value from a closed list" + }, + "recipe.ARecipeCanBuildOnA": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-0f3adf9d29dc", + "other": "a recipe can build on a preset, and on nothing else in this build - not on another file" + }, + "recipe.ARecipeIsATextDocument": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-8361f48aa1aa", + "other": "a recipe is a text document, and a control character either breaks it or is dropped without a word" + }, + "recipe.ARecipeIsOneDocumentAnd": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-01123bae35c4", + "other": "a recipe is one document, and everything after the first separator would be ignored without a word" + }, + "recipe.ARecipeThatBuildsOnA": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-e559b6752421", + "other": "a recipe that builds on a preset is read by a reader that expands the preset first, and this one does not" + }, + "recipe.ARecipeWithoutTargetsHasNothing": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-f69e05f3bab1", + "other": "a recipe without targets has nothing to produce" + }, + "recipe.ATargetHasToSayWhat": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-21889853a052", + "other": "a target has to say what kind of file it produces" + }, + "recipe.ATargetThatDamagesNothingProduces": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-e3ae55c0f4ef", + "other": "a target that damages nothing produces the same files as one that does not mention it" + }, + "recipe.ATargetThatProducesNothingIs": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-3a4f444ceac5", + "other": "a target that produces nothing is almost always a mistake rather than an intention" + }, + "recipe.ATypoAcceptedInSilenceBecomes": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-0c20fa7ae876", + "other": "a typo accepted in silence becomes an expectation no test will ever check" + }, + "recipe.AddAtLeastOneEntryUnder": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-577a79df5625", + "other": "add at least one entry under targets:" + }, + "recipe.AddExtendsPresetIdAboveIt": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-c0747ed47cd7", + "other": "add extends: preset: above it, or remove with" + }, + "recipe.AddFormatPdf": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-514ee52e95db", + "other": "add format: pdf" + }, + "recipe.AddFormatTxtOrRunTfg": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-1fc7c58ded33", + "other": "add format: txt, or run \"tfg formats\" to see the whole list" + }, + "recipe.AddOutcomeAcceptRejectSanitizeOr": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-906f83c6e332", + "other": "add outcome: accept, reject, sanitize or unspecified" + }, + "recipe.AddSize2mb": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-aaa48a1f0dc8", + "other": "add size: 2mb" + }, + "recipe.AddSize2mbSizeRange1kb": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-75d03ddd7410", + "other": "add size: 2mb, size-range: 1kb-8kb, a boundary, contains, or a plain number of bytes" + }, + "recipe.AddTypeWithTheNameOf": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-a4232a7b5f56", + "other": "add type, with the name of the damage" + }, + "recipe.AddVersionAsTheFirstLine": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.SchemaVersion}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-ffcea33474a0", + "other": "add version: {{.SchemaVersion}} as the first line" + }, + "recipe.AllowNondeterministicTrueHasNothingTo": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-2ef05077fa28", + "other": "allow_nondeterministic: true has nothing to allow in this build" + }, + "recipe.AnExpectationCarriesAnOutcomeAnd": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-c3cdf61efb0d", + "other": "an expectation carries an outcome and a reason, and anything else would be dropped without a word" + }, + "recipe.AnExpectationIsEitherOneWord": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-5ba3e0655b36", + "other": "an expectation is either one word or a block with an outcome" + }, + "recipe.AnExpectationSaysWhatTheSystem": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-4f1054b7ef90", + "other": "an expectation says what the system under test should do with the file" + }, + "recipe.AnOutcomeIsOneOfFour": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-936413f8f699", + "other": "an outcome is one of four words" + }, + "recipe.AskForAtLeastOneOr": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-6af43d0b9605", + "other": "ask for at least one, or delete the target" + }, + "recipe.AsksForFiles": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.At}}, {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-41167ad22021", + "other": "{{.At}} asks for {{.Value}} files" + }, + "recipe.BothEndsOfARangeAre": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-a9a3149f123a", + "other": "both ends of a range are sizes, and units count in 1024s" + }, + "recipe.CannotBe": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Key}}, {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-f03796441e95", + "other": "{{.Where}}: {{.Key}} cannot be {{.Value}}" + }, + "recipe.DeclaresAReasonThatIsNot": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-5b2a54855ceb", + "other": "{{.Where}} declares a reason that is not a word" + }, + "recipe.DeclaresAnExpectationThisBuildCannot": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-eb121a54cc49", + "other": "{{.Where}} declares an expectation this build cannot read" + }, + "recipe.DeclaresAnExpectationWithNoOutcome": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-364dd9dae02a", + "other": "{{.Where}} declares an expectation with no outcome" + }, + "recipe.DeclaresAnOutcomeThatIsNot": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1bdfdc699e0a", + "other": "{{.Where}} declares an outcome that is not a word" + }, + "recipe.DeclaresInsideItsExpectation": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.K}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-83c4a173ba32", + "other": "{{.Where}} declares {{.K}} inside its expectation" + }, + "recipe.DoesNotSayWhichDamageIt": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.At}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-aaa7993ea8ad", + "other": "{{.At}} does not say which damage it is" + }, + "recipe.Entry": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.List}}, {{.Number}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-3d172c936981", + "other": "{{.Where}}: {{.List}} entry {{.Number}}" + }, + "recipe.EveryFormatHereRepeatsToThe": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-a1c5006fdb6d", + "other": "every format here repeats to the byte, so no consent is needed" + }, + "recipe.EveryRecipeDeclaresTheSchemaVersion": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-48bf6f9999fc", + "other": "every recipe declares the schema version it was written against" + }, + "recipe.EveryTargetDeclaresItsSettingWhich": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-df7af7d5d62b", + "other": "every target declares its {{.Setting}}, which is what lets a dry run report exact numbers before anything reaches the disk" + }, + "recipe.ExpectsWhichIsNotAKnown": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Outcome}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-056d10d1cdc7", + "other": "{{.Where}} expects {{.Outcome}}, which is not a known outcome" + }, + "recipe.ExtendsDoesNotNameAPreset": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-96b167b16bf9", + "other": "extends {{.Value}} does not name a preset" + }, + "recipe.GeneratedContentIsEnglishOnlySo": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-730196b0eddb", + "other": "generated content is English only so far" + }, + "recipe.GiveItADifferentSettingOr": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-8dd1a4a5c174", + "other": "give it a different {{.Setting}}, or leave the preset's target out by ejecting the preset and editing the recipe" + }, + "recipe.GiveItOneValueForExample": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-f43e69de623b", + "other": "give it one value, for example bytes: 16" + }, + "recipe.GiveItOneValueForExample2": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-1865e1d0d4fc", + "other": "give it one value, for example pages: 3" + }, + "recipe.GiveItSettingForExampleId": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-9066c56454f2", + "other": "give it {{.Setting}}, for example id: invoices" + }, + "recipe.GiveOneOfThemADifferent": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-f06830c13eb6", + "other": "give one of them a different {{.Setting}}" + }, + "recipe.GivesTheReasonWhichIsNot": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-8f677f25d2b4", + "other": "{{.Where}} gives the reason {{.Value}}, which is not on the list" + }, + "recipe.HasAFormatThatIsNot": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.At}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-e06c14c235fd", + "other": "{{.At}} has a format that is not a name" + }, + "recipe.HasANegativeSetting": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.At}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-608858bc635d", + "other": "{{.At}} has a negative {{.Setting}}" + }, + "recipe.HasNoFormat": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.At}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-54e9815fc753", + "other": "{{.At}} has no format" + }, + "recipe.HasNoSetting": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.At}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-bbb5a764f659", + "other": "{{.At}} has no {{.Setting}}" + }, + "recipe.HasSettingOfB": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Limit}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-9f292ccc35c0", + "other": "{{.Where}} has {{.Setting}} of {{.Limit}} B" + }, + "recipe.HasSettingOfWhichIsNot": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Text}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-121c812b222f", + "other": "{{.Where}} has {{.Setting}} of {{.Text}}, which is not a whole number" + }, + "recipe.HasSettingThatIsNeitherText": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.At}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-ffa08f43b328", + "other": "{{.At}} has {{.Setting}} that is neither text nor a number" + }, + "recipe.HasSettingThatIsNotA": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.At}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-2a5268bfb9cb", + "other": "{{.At}} has {{.Setting}} that is not a whole number" + }, + "recipe.HasSettingThatIsNotText": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-2b479eb7c452", + "other": "{{.Where}} has {{.Setting}} that is not text" + }, + "recipe.HasTheKey": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.At}}, {{.Key}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-1cdc9dd0b5e2", + "other": "{{.At}} has the key {{.Key}}" + }, + "recipe.HasTheSettingOfATarget": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-ff414a2e347a", + "other": "{{.Where}} has the {{.Setting}} of a target the preset already builds" + }, + "recipe.HoldsTheCharacterWhichCannotBe": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.At}}, {{.Bad}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-e6aafb7f9e39", + "other": "{{.At}} holds the character {{.Bad}}, which cannot be written to a recipe" + }, + "recipe.IsAListOrABlock": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.At}}, {{.Key}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-d3df7f29aa77", + "other": "{{.At}}: {{.Key}} is a list or a block" + }, + "recipe.IsNeitherANameNorA": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.At}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-ff6baa7396bc", + "other": "{{.At}} is neither a name nor a set of settings" + }, + "recipe.IsWhichIsNeitherTrueNor": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Subject}}, {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-731e62d7639d", + "other": "{{.Subject}} is {{.Value}}, which is neither true nor false" + }, + "recipe.IsWrittenAsAListOr": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Subject}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-311df656fc11", + "other": "{{.Subject}} is written as a list or a block" + }, + "recipe.KeepBoundaryForTheThreeSizes": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-4e72c705dde7", + "other": "keep boundary for the three sizes around a limit, or keep {{.Setting}} for one exact {{.Setting}}" + }, + "recipe.KeepSettingForIdenticalFilesOr": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-46527448d710", + "other": "keep {{.Setting}} for identical files, or keep size-range for a different {{.Setting}} each" + }, + "recipe.KeepSettingToTestALimit": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-789756ad13c4", + "other": "keep {{.Setting}} to test a limit, or keep size-range for files of varying size" + }, + "recipe.LeaveTheFileAsItIs": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-6b6af820f0ca", + "other": "leave the file as it is, and check it for an anchor or an alias used where a key belongs" + }, + "recipe.LeaveTheFileAsItIs2": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-1a69ba8162a8", + "other": "leave the file as it is, and simplify whatever it uses that has no single written form - a byte order mark that is not at the very start is the usual cause" + }, + "recipe.LocaleIsNotAvailableInThis": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Locale}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-74efd135bbd0", + "other": "locale {{.Locale}} is not available in this build" + }, + "recipe.NameADamageOrRemoveThe": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-3ab62a82689b", + "other": "name a damage, or remove the line" + }, + "recipe.NamesADamageThatIsNot": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.At}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-72cc91016a21", + "other": "{{.At}} names a damage that is not a word" + }, + "recipe.NotYAML": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-4f98e4e1591a", + "other": "this file could not be read as YAML. Look for a tag or anchor marker such as ! or & with nothing after it" + }, + "recipe.NotYet": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Key}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-40d28c193ca6", + "other": "{{.Key}} is not in this build yet" + }, + "recipe.NotYetIn": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Key}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-0092c2fe4e03", + "other": "{{.Where}}: {{.Key}} is not in this build yet" + }, + "recipe.OneNamesAnExactSettingAnd": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-47db418b9d1b", + "other": "one names an exact {{.Setting}} and the other draws one, so together they say two different things" + }, + "recipe.PresetCarries": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Keys}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-b5289d7e61ab", + "other": "the preset's recipe carries {{.Keys}}, which a recipe building on it cannot inherit" + }, + "recipe.PresetTarget": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Target}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a13be873f25d", + "other": "the preset's target {{.Target}}" + }, + "recipe.PresetTargetNumber": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Number}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-6fdb0658c67b", + "other": "the preset's target {{.Number}}" + }, + "recipe.Problem": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.What}}, {{.Why}}, {{.Fix}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-f2a3b0708fd9", + "other": "{{.What}} - {{.Why}}.\n {{.Fix}}." + }, + "recipe.PropertyIsAListOrA": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.K}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-81837b3cdae2", + "other": "{{.Where}}: property {{.K}} is a list or a block" + }, + "recipe.ReadTheFileWithParseOr": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-ebbbbc57d3a1", + "other": "read the file with Parse, or add extends: preset:" + }, + "recipe.ReadTheFileWithTfgGenerate": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-0855d32f4635", + "other": "read the file with \"tfg generate\" or \"tfg validate\", which do" + }, + "recipe.Refused": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Name}}, {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-338516f4d59b", + "one": "{{.Name}} has {{.Count}} problem and nothing was written:", + "other": "{{.Name}} has {{.Count}} problems and nothing was written:" + }, + "recipe.RemoveItItIsUsuallyA": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-9d3d6a72b52c", + "other": "remove it - it is usually a stray tab or a line break from pasting" + }, + "recipe.RemoveSettingOrUseSizeWith": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-2834c992f758", + "other": "remove {{.Setting}}, or use size with {{.Setting}} to ask for identical files" + }, + "recipe.RemoveTheKeyOrMoveIt": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-549770eb0c2f", + "other": "remove the key, or move it to the target itself" + }, + "recipe.RemoveTheLine": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-14c26fa261f4", + "other": "remove the line" + }, + "recipe.RemoveTheLineContentIsGenerated": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-0e80dd6c948d", + "other": "remove the line - content is generated from the seed" + }, + "recipe.RemoveTheLineItWillBe": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-d58a87dfe991", + "other": "remove the line - it will be needed by the formats that use a system encoder" + }, + "recipe.RemoveTheLineOrPutThe": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-339469eb5320", + "other": "remove the line, or put the explanation in reason" + }, + "recipe.RemoveTheLineTheManifestRecords": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-348821455596", + "other": "remove the line - the manifest records the version that ran" + }, + "recipe.RemoveTheSectionTheExpectedField": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-d2455a9faadf", + "other": "remove the section - the expected field on a target already works" + }, + "recipe.RemoveTheSeparatorsOrSplitThe": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-15c435179b86", + "other": "remove the --- separators, or split the file into one recipe per file" + }, + "recipe.SaysDamageAndNamesNone": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-5f72b5dd57ef", + "other": "{{.Where}} says damage and names none" + }, + "recipe.SeedIsNotAWholeNumber": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Text}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-789fd24618af", + "other": "seed {{.Text}} is not a whole number" + }, + "recipe.SettingAnchorsTheSeedOfA": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-77b6c3d3c3ba", + "other": "{{.Setting}} anchors the seed of a target, so editing one target never moves the bytes of another" + }, + "recipe.SettingIdentifiesATargetAnchorsIts": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-3777811417a4", + "other": "{{.Setting}} identifies a target, anchors its seed and links it to the manifest" + }, + "recipe.SettingIsHowManyFilesOf": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-4817aba23867", + "other": "{{.Setting}} is how many files of this group the container holds" + }, + "recipe.SettingIsHowManyFilesThis": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-fc479ec62722", + "other": "{{.Setting}} is how many files this target produces, so it is read exactly as written" + }, + "recipe.SettingIsOneSizeAndThe": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-93e665020c2d", + "other": "{{.Setting}} is one size, and the set is built either side of it" + }, + "recipe.SettingIsThreeChosenSizesAround": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-f4373c72f697", + "other": "{{.Setting}} is three chosen sizes around a limit, so drawing sizes as well would throw away the reason for choosing them" + }, + "recipe.SettingIsWrittenAs2mbOr": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-232713d53bb0", + "other": "{{.Setting}} is written as 2mb or as a plain byte count" + }, + "recipe.SettlingItTwiceGivesTwoDifferent": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-eff74d7341c6", + "other": "settling it twice gives two different files, so a check would never pass and the recipe hash in a manifest would not describe it" + }, + "recipe.StatesBothSettingAndABoundary": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-e26d703b93e3", + "other": "{{.Where}} states both {{.Setting}} and a boundary" + }, + "recipe.StatesBothSettingAndASize": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-fe85fe68a072", + "other": "{{.Where}} states both {{.Setting}} and a size-range" + }, + "recipe.Target": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Target}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-f4062d7dbd6c", + "other": "target {{.Target}}" + }, + "recipe.TargetNumber": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Number}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-7292920a57c5", + "other": "target {{.Number}}" + }, + "recipe.TargetSettingIsUsedTwice": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.ID}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-85e32b5347f6", + "other": "target {{.Setting}} {{.ID}} is used twice" + }, + "recipe.TheFileHoldsYAMLDocuments": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Value}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-180cc02ceaf6", + "other": "the file holds {{.Value}} YAML documents" + }, + "recipe.TheFillModeIsNotSettable": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-baa1d43a605b", + "other": "the fill mode is not settable yet" + }, + "recipe.TheLayoutThisFileNeedsIs": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-3821ee0220a7", + "other": "the layout this file needs is one the formatter cannot write, so settling it would replace it with something no longer readable" + }, + "recipe.TheListIsClosedSoThat": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-bd547801019b", + "other": "the list is closed so that a report can group by reason, and a typo would make a category of one" + }, + "recipe.TheManifestIsAlwaysWrittenAs": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-c91903773952", + "other": "the manifest is always written as one file so far" + }, + "recipe.ThePresetSTargetsComeFirst": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-38dc9b08929a", + "other": "the preset's targets come first and every {{.Setting}} anchors a seed, so a second one would be a target nobody can tell from the first" + }, + "recipe.TheRecipeAsksForNoFiles": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-dfe478303a1a", + "other": "the recipe asks for no files" + }, + "recipe.TheRecipeHasNoVersion": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-328db8bc7fcb", + "other": "the recipe has no version" + }, + "recipe.TheSeedDecidesEveryByteOf": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-d273c2d6fe96", + "other": "the seed decides every byte of the run, so it is read exactly as written and never guessed at" + }, + "recipe.TheSettingOfTheContainerFollows": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-6a5eac5ec73d", + "other": "the {{.Setting}} of the container follows from the {{.Setting}} of what it holds, so every group states one" + }, + "recipe.TheSettledShapeOfThisFile": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-20b623f6d3da", + "other": "the settled shape of this file cannot be read back" + }, + "recipe.TheSettledShapeOfThisFile2": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-23ef3499f4b2", + "other": "the settled shape of this file does not stay settled" + }, + "recipe.TheToolVersionThisRecipeRequires": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-aad8ed8feddf", + "other": "the tool version this recipe requires is not checked yet" + }, + "recipe.TheTypeOfADamageIs": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-c7a22bfecd16", + "other": "the type of a damage is the name this build knows it by" + }, + "recipe.TheVersionDecidesHowTheRest": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-e7a6e86b83ab", + "other": "the version decides how the rest of the file is read, so it is never guessed at" + }, + "recipe.ThisBuildUnderstandsVersion": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.SchemaVersion}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-09815b5705a3", + "other": "this build understands version {{.SchemaVersion}}" + }, + "recipe.ThisIsAMistakeInThe": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-7fa411c5c61a", + "other": "this is a mistake in the preset rather than in the recipe - run \"tfg preset eject\" and edit the result instead" + }, + "recipe.ThisRecipeBuildsOnPresetAnd": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Preset}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-0cdff56485bc", + "other": "this recipe builds on preset:{{.Preset}}, and the preset's targets were not supplied" + }, + "recipe.ThisRecipeDoesNotBuildOn": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-008e7bc28deb", + "other": "this recipe does not build on a preset" + }, + "recipe.ThisSettingIsOneOfTwo": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-3ad6ce9edcc3", + "other": "this setting is one of two words, and anything else would have to be guessed at" + }, + "recipe.ThisSettingIsOneValueWritten": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-342b49446a29", + "other": "this setting is one value, written beside its name rather than indented under it" + }, + "recipe.TooDeep": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Name}}, {{.Depth}}, {{.Limit}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-0d47280182a1", + "other": "{{.Name}} nests lists and mappings {{.Depth}} deep and the limit is {{.Limit}}. Reading a deeply nested document costs memory that grows far faster than the document does, so a small file can exhaust this machine before anything is written. Write the targets out as an ordinary list instead" + }, + "recipe.TooLarge": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Name}}, {{.Bytes}}, {{.Limit}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-c0e7fdfe5e2a", + "other": "{{.Name}} is {{.Bytes}} B and the limit is {{.Limit}} B. A recipe is a document somebody writes, and reading time grows with its size, so an unbounded one is a way to hang a build. Split it into several recipes, or generate the targets with a loop in your own script" + }, + "recipe.UnitsCountIn1024sSo10mb": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-6583bf42dc24", + "other": "units count in 1024s, so 10mb is 10485760 bytes" + }, + "recipe.Unreadable": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Name}}, {{.Detail}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-6db3546dfc2c", + "other": "{{.Name}} cannot be read as a recipe:\n{{.Detail}}" + }, + "recipe.UnspecifiedExpectationsAreLeftInThe": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-94e9e4b033f1", + "other": "unspecified expectations are left in the manifest for the consumer to settle" + }, + "recipe.UpgradeTheToolOrWriteThe": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-191faed33769", + "other": "upgrade the tool, or write the recipe against the version above" + }, + "recipe.UseAcceptRejectSanitizeOrUnspecified": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-85d365e7de8c", + "other": "use accept, reject, sanitize or unspecified" + }, + "recipe.UseBoundary10mbOrAPlain": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-82fc57a9cff3", + "other": "use boundary: 10mb or a plain byte count" + }, + "recipe.UseCount0ForAGroup": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-de61b29c39d0", + "other": "use count: 0 for a group that contributes nothing, or drop the entry" + }, + "recipe.UseCount50": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-ec6634fce2c2", + "other": "use count: 50" + }, + "recipe.UseExpectedAcceptOrABlock": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-d7d3c6dee42f", + "other": "use expected: accept, or a block with outcome:" + }, + "recipe.UseFormatPdfOrRunTfg": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-39cbda389c37", + "other": "use format: pdf, or run tfg formats to see the list" + }, + "recipe.UseLocaleEnOrLeaveThe": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-0a19d43dd48f", + "other": "use locale: en, or leave the line out" + }, + "recipe.UseOneOfReasons": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Reasons}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-4e2410ba8441", + "other": "use one of: {{.Reasons}}" + }, + "recipe.UseSettingAtLeastOneByte": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-810411e520cb", + "other": "use {{.Setting}} at least one byte below the largest number" + }, + "recipe.UseSettingOfOrLessOr": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.MaxFilesPerRun}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a950b91ebbd9", + "other": "use {{.Setting}} of {{.MaxFilesPerRun}} or less, or split the container across several entries" + }, + "recipe.UseSettingOfOrLessOr2": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.MaxFilesPerRun}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-e940b6c564de", + "other": "use {{.Setting}} of {{.MaxFilesPerRun}} or less, or split the target across several recipes" + }, + "recipe.UseSettingSuchAs10mb512kb": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-bead317186c2", + "other": "use {{.Setting}} such as 10mb, 512kb or a plain byte count" + }, + "recipe.UseSettingSuchAs2mb512kb": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-cd8679e4c31b", + "other": "use {{.Setting}} such as 2mb, 512kb or a plain byte count" + }, + "recipe.UseSize2mbOrSize2097152": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-e3c50d33a6be", + "other": "use size: 2mb or size: 2097152" + }, + "recipe.UseSizeRange1kb8kb": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-ae8d05cf1f68", + "other": "use size-range: 1kb-8kb" + }, + "recipe.UseSizeRange1kb8kbOr": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-f1d9e9c21ff4", + "other": "use size-range: 1kb-8kb or a pair of plain byte counts" + }, + "recipe.UseTrueOrFalse": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Subject}}, {{.Subject2}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-27db5e3986d1", + "other": "use {{.Subject}}: true or {{.Subject2}}: false" + }, + "recipe.VersionIsNotASchemaThis": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Version}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-3fe3d2cfbd35", + "other": "version {{.Version}} is not a schema this build knows" + }, + "recipe.VersionIsNotAWholeNumber": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Text}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-82edb8fdc772", + "other": "version {{.Text}} is not a whole number" + }, + "recipe.WhereError": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.Error}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-806cf9b89163", + "other": "{{.Where}}: {{.Error}}" + }, + "recipe.WhereWhat": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Where}}, {{.What}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-d0a8cf451027", + "other": "{{.Where}}: {{.What}}" + }, + "recipe.WithBelongsBesideExtendsItSays": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-f7313a2dbc3d", + "other": "with belongs beside extends: it says what to fill in, and extends says what to fill it into" + }, + "recipe.WithFillsTheParametersOfA": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-91f25e74d772", + "other": "with fills the parameters of a preset, and no preset is named" + }, + "recipe.WriteADecimalNumberSuchAs": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-fd6a6e21bf53", + "other": "write a decimal number such as seed: 20260802" + }, + "recipe.WriteADecimalNumberSuchAs2": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-69ec31d15356", + "other": "write a decimal number such as count: 10" + }, + "recipe.WriteExtendsPresetIdAndRun": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-eec676d41814", + "other": "write extends: preset:, and run \"tfg preset list\" for the ids this build has" + }, + "recipe.WriteItOnOneLineSuch": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Example}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-b00307984821", + "other": "write it on one line, such as {{.Example}}" + }, + "recipe.WriteTheNameAsAWord": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-effad2028c85", + "other": "write the name as a word" + }, + "recipe.WriteTheNameOnItsOwn": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-da431c8ff106", + "other": "write the name on its own, or a mapping starting with type" + }, + "recipe.WriteVersion": { + "description": "Said by internal/recipe - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.SchemaVersion}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-6b426dee7162", + "other": "write version: {{.SchemaVersion}}" + }, + "textenc.HasToBeEncoding": { + "description": "Said by internal/format/textenc - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.UTF8}}, {{.UTF16LE}}, {{.UTF16BE}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-cf8214cf1fa6", + "other": "it has to be {{.UTF8}}, {{.UTF16LE}} or {{.UTF16BE}}" + }, + "tool.AToolIsNamedExactlyAnd": { + "description": "Said by internal/tool - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-756a78c6f204", + "other": "a tool is named exactly, and the nearest name to a mistyped one may do something else" + }, + "tool.NameTheToWorkOn": { + "description": "Said by internal/tool - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Kind}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-3dac06c6319b", + "other": "name the {{.Kind}} to work on" + }, + "tool.NoWasGiven": { + "description": "Said by internal/tool - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Kind}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-c984aa47e200", + "other": "no {{.Kind}} was given" + }, + "tool.Sentence": { + "description": "Said by internal/tool - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.What}}, {{.Why}}, {{.Instead}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-d9a59b987ed3", + "other": "{{.What}} - {{.Why}}. {{.Instead}}" + }, + "tool.TheToolHasNothingToRead": { + "description": "Said by internal/tool - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-8b5043abfb15", + "other": "the tool has nothing to read without one" + }, + "tool.ThereIsNoToolCalled": { + "description": "Said by internal/tool - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.ID}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-151e818f434e", + "other": "there is no tool called {{.ID}}" + }, + "tool.ThisBuildHasNoTools": { + "description": "Said by internal/tool - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-42b26a93974c", + "other": "this build has no tools" + }, + "tool.UseOneOf": { + "description": "Said by internal/tool - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Known}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-a53588601259", + "other": "use one of: {{.Known}}" + }, + "wav.DropTheLabel": { + "description": "Said by internal/format/wav - a refusal under the box it is about or at the foot of the form, or a note after a run.", + "hash": "sha256-b8bfddcdab7d", + "other": ", or drop the label" + }, + "xlsx.Columns": { + "description": "Said by internal/format/xlsx - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-5a53b3c96cb2", + "one": "{{.Count}} column", + "other": "{{.Count}} columns" + }, + "xlsx.JointFewer": { + "description": "Said by internal/format/xlsx - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Why}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-15c3bae806db", + "other": "{{.Why}}. Ask for fewer rows or fewer columns" + }, + "xlsx.Rows": { + "description": "Said by internal/format/xlsx - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Count}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-244cc1fe9311", + "one": "{{.Count}} row", + "other": "{{.Count}} rows" + }, + "xml.NeedsBOM": { + "description": "Said by internal/format/xmlfile - a refusal under the box it is about or at the foot of the form, or a note after a run. Carries {{.Encoding}}, {{.UTF8}}, written the way the program writes them - keep each spelled exactly that way.", + "hash": "sha256-74660e3e12d1", + "other": "XML in {{.Encoding}} has to open with a byte order mark, so this needs bom=true or encoding={{.UTF8}}" + } +} diff --git a/internal/gui/text/locale/said/pl.json b/internal/gui/text/locale/said/pl.json new file mode 100644 index 00000000..4302c919 --- /dev/null +++ b/internal/gui/text/locale/said/pl.json @@ -0,0 +1,624 @@ +{ + "archive.AskForNone": { "hash": "sha256-1c9a11fe7f69", "other": "Ustaw Kompresję na {{.None}}, żeby zapisać pliki takie, jakie są." }, + "archive.ExplicitSizeOrNone": { "hash": "sha256-f624161851a0", "other": "Podaj archiwum dokładny rozmiar albo ustaw pole {{.Compression}} na {{.None}}." }, + "archive.FlatNoDirectories": { "hash": "sha256-24dda239d49e", "other": "płaskie archiwum nie ma katalogów do wypisania, a pole {{.Depth}} ma wartość 0" }, + "archive.FlatNoDirectoriesFix": { "hash": "sha256-54339aa5e96a", "other": "Ustaw pole {{.Depth}} na 1 albo więcej albo wyłącz pole {{.Directories}}." }, + "archive.GiveAPassword": { "hash": "sha256-2d40fa5d2f6b", "other": "Podaj hasło albo ustaw szyfrowanie na {{.None}}." }, + "archive.LockedNoCompression": { "hash": "sha256-04d93d355279", "other": "archiwum jest zamknięte (pole {{.Password}} jest wypełnione), a zamknięty wpis podaje swoją długość przed danymi - więc skompresowany trzeba by trzymać w pamięci w całości" }, + "archive.LockedNoCompressionFix": { "hash": "sha256-82613b7fae43", "other": "Ustaw pole {{.Compression}} na {{.None}} albo wyczyść pole {{.Password}}." }, + "archive.LockedWithoutPassword": { "hash": "sha256-b5d56de9a9b9", "other": "archiwum ma być zamknięte metodą {{.Method}}, a nie ma hasła, którym można by je zamknąć" }, + "archive.SetEncryption": { "hash": "sha256-0b2a90b45006", "other": "Ustaw szyfrowanie na {{.Method}}, żeby zamknąć archiwum, albo usuń hasło, żeby zostało otwarte." }, + "archive.WriteTrueOrFalse": { "hash": "sha256-213afa915dd9", "other": "Wpisz {{.Key}}: true albo {{.Again}}: false." }, + "avif.MinimumReason": { "hash": "sha256-97b2a4a72e3a", "other": "najmniejszy obrazek, jaki rysuje ten format, koduje się w najgorszym razie do {{.Coded}} B, a plik zawsze niesie wolny blok, który kosztuje {{.Box}} B nawet pusty" }, + "avif.MinimumReasonOwn": { "hash": "sha256-ff69f6fa9b64", "other": "obrazek {{.Width}}x{{.Height}} w jakości {{.Quality}} koduje się do {{.Coded}} B, a plik zawsze niesie wolny blok, który kosztuje {{.Box}} B nawet pusty" }, + "checksum.AChecksumDigitsLongIsNot": { "hash": "sha256-0dcdc03bd2f7", "other": "suma kontrolna o długości {{.Digits}} cyfr nie jest taką, jaką to narzędzie wylicza" }, + "checksum.AChecksumFileIsNeverWritten": { "hash": "sha256-49681362425b", "other": "pliku sum kontrolnych nigdy się nie nadpisuje, bo może być jedynym zapisem tego, co było w folderze" }, + "checksum.AChecksumFilePromisesEveryFile": { "hash": "sha256-354be45714f3", "other": "plik sum kontrolnych obiecuje każdy plik w folderze, a taki z luką nie powiedziałby, gdzie ta luka jest" }, + "checksum.AChecksumFileWithoutALine": { "hash": "sha256-fcc3b0268478", "other": "plik sum kontrolnych bez żadnego wiersza sha256sum -c odrzuca, więc niczego by nie sprawdził" }, + "checksum.AChecksumLineIsAChecksum": { "hash": "sha256-acbe4fd99bc0", "other": "wiersz sumy kontrolnej to suma i ścieżka, zapisane jako hex ścieżka albo SHA256 (ścieżka) = hex, a bez niego nie ma czego sprawdzać" }, + "checksum.AChecksumOfAFileBeing": { "hash": "sha256-3992fc15f6b5", "other": "suma kontrolna pliku w trakcie zapisu nie jest sumą żadnej jego wersji" }, + "checksum.ADirectoryHasNoBytesOf": { "hash": "sha256-c5451d4ddaae", "other": "katalog nie ma własnych bajtów, z których można wyliczyć sumę kontrolną" }, + "checksum.AnAlgorithmIsNamedExactlyBecause": { "hash": "sha256-5c67b39f12b3", "other": "algorytm podaje się dokładną nazwą, bo dwa podobnie brzmiące dają różne sumy" }, + "checksum.ChangedWhileItWasBeingRead": { "hash": "sha256-7b323b86f3c7", "other": "{{.Path}} zmienił się w trakcie czytania" }, + "checksum.CheckTheFolderAgainstItWith": { "hash": "sha256-f2f1b7172ace", "other": "sprawdź folder względem niego przez tfg tool checksum-check albo odsuń go i uruchom to jeszcze raz" }, + "checksum.CheckTheFolderInPartsEach": { "hash": "sha256-e9771dc952f5", "other": "sprawdzaj folder po częściach, każdą z własnym plikiem sum kontrolnych" }, + "checksum.FolderUnreadable": { "hash": "sha256-07ba49413258", "other": "{{.Sentence}}:\n {{.Problems}}" }, + "checksum.FreeSomeSpaceOnThatDisk": { "hash": "sha256-0e09b5bc8590", "other": "zwolnij trochę miejsca na tym dysku i uruchom to jeszcze raz" }, + "checksum.HoldsNoChecksumLine": { "hash": "sha256-073a243f0ed8", "other": "{{.Path}} nie zawiera żadnego wiersza sumy kontrolnej" }, + "checksum.HoldsNoFileToList": { "hash": "sha256-8f2183b75bf4", "other": "{{.Folder}} nie zawiera żadnego pliku do wypisania" }, + "checksum.HoldsNoFileToListOnly": { "hash": "sha256-9f4a648b05cc", "other": "{{.Folder}} nie zawiera żadnego pliku do wypisania, tylko {{.Names}}, które nie są plikami" }, + "checksum.IsADirectoryAndThisReads": { "hash": "sha256-0af9eb952a74", "other": "{{.Path}} to katalog, a to narzędzie czyta jeden plik sum kontrolnych" }, + "checksum.IsADirectoryAndThisWorks": { "hash": "sha256-3cba141e419e", "other": "{{.Path}} to katalog, a to narzędzie wylicza sumę kontrolną jednego pliku" }, + "checksum.IsAFileAndThisLists": { "hash": "sha256-e43a949bd38b", "other": "{{.Path}} to plik, a to narzędzie wypisuje folder" }, + "checksum.IsAlreadyThereSoNothingWas": { "hash": "sha256-f3786e37e4f3", "other": "{{.Path}} już istnieje, więc nic nie zostało zapisane" }, + "checksum.IsLargerThanAChecksumFile": { "hash": "sha256-aead292e6e34", "other": "{{.Path}} ma {{.Bytes}}, więcej niż plik sum kontrolnych, który to narzędzie czyta" }, + "checksum.IsNotAChecksumWrittenIn": { "hash": "sha256-a48e593200f8", "other": "{{.Given}} nie jest sumą kontrolną zapisaną szesnastkowo" }, + "checksum.IsNotAFileButA": { "hash": "sha256-28957d41213f", "other": "{{.Path}} nie jest plikiem, tylko urządzeniem, potokiem albo gniazdem" }, + "checksum.ItTakesAndTheDiskHas": { "hash": "sha256-c053efc918f1", "other": "potrzeba {{.Need}}, a na dysku jest wolne {{.Have}}" }, + "checksum.MakeTheseReadableOrMoveThem": { "hash": "sha256-0d223d82147d", "other": "udostępnij je do odczytu albo wynieś z folderu i uruchom to jeszcze raz" }, + "checksum.NameAFolderWithAtLeast": { "hash": "sha256-5a322343ecd1", "other": "wskaż folder z co najmniej jednym plikiem" }, + "checksum.NameOneFile": { "hash": "sha256-74c2c0a6f263", "other": "wskaż jeden plik" }, + "checksum.NameTheChecksumFileItselfSuch": { "hash": "sha256-c75ab67b49df", "other": "wskaż sam plik sum kontrolnych, na przykład SHA256SUMS, a nie stronę albo podpis obok niego" }, + "checksum.NameTheFolderOrUseTfg": { "hash": "sha256-9457caf28208", "other": "wskaż folder albo użyj tfg tool checksum dla tego jednego pliku" }, + "checksum.Names": { "hash": "sha256-1b28298a4324", "one": "{{.Count}} nazwę", "few": "{{.Count}} nazwy", "many": "{{.Count}} nazw", "other": "{{.Count}} nazwy" }, + "checksum.NotEverythingUnderCouldBeRead": { "hash": "sha256-7d3694b98961", "other": "nie wszystko w {{.Folder}} dało się przeczytać, więc nic nie zostało zapisane" }, + "checksum.PasteAlone": { "hash": "sha256-c174b1551cbd", "other": "wklej samą sumę kontrolną, bez nazwy pliku. Długości to: {{.Lengths}}" }, + "checksum.PathProblem": { "hash": "sha256-8bcdf2cccf02", "other": "{{.Path}} - {{.Error}}" }, + "checksum.ReadingOneMayNeverEndSo": { "hash": "sha256-9055c322986c", "other": "jego czytanie może się nigdy nie skończyć, więc nie jest zaczynane" }, + "checksum.TheChecksumFileIsWrittenBeside": { "hash": "sha256-a62cd68c242f", "other": "plik sum kontrolnych jest zapisywany obok plików, które wypisuje, więc potrzebuje folderu, w którym leżą" }, + "checksum.TheLengthOfAChecksumSays": { "hash": "sha256-4add58be1e11", "other": "długość sumy kontrolnej mówi, który algorytm ją wyliczył, i to ten jest wyliczany do porównania" }, + "checksum.TheMostIsAboutHalfA": { "hash": "sha256-7cc63fb0d3f6", "other": "najwięcej to {{.SumsMostBytes}}, około pół miliona plików, a większy plik najpewniej w ogóle nie jest plikiem sum kontrolnych" }, + "checksum.ThereIsNoAlgorithmCalled": { "hash": "sha256-6de5decc9e2d", "other": "nie ma algorytmu o nazwie {{.Name}}" }, + "checksum.ThereIsNoRoomForSo": { "hash": "sha256-dfaa099cc3ba", "other": "nie ma miejsca na {{.Path}}, więc nic nie zostało zapisane" }, + "checksum.UnknownLine": { "hash": "sha256-a8662d5c6650", "other": "wiersz {{.Line}}: {{.Name}}" }, + "checksum.UseAlgorithms": { "hash": "sha256-34a4c307745f", "other": "użyj {{.Algorithms}}, kilku rozdzielonych przecinkami albo samego all" }, + "checksum.WaitUntilWhateverIsWritingIt": { "hash": "sha256-161d36039e9c", "other": "poczekaj, aż to, co go zapisuje, skończy, i uruchom to jeszcze raz" }, + "core.BoundaryTooLarge": { "hash": "sha256-7c6185922a32", "other": "zestaw graniczny potrzebuje jednego rozmiaru powyżej limitu, a nad tą liczbą nie ma już żadnej. Użyj limitu co najmniej o bajt mniejszego od największego albo sprawdź, czy to na pewno ta liczba" }, + "core.BoundaryTooSmall": { "hash": "sha256-a0fd66f30b3f", "other": "{{.Why}}. {{.Fix}}" }, + "core.BoundaryTooSmallFix": { "hash": "sha256-d3ccef8f1f9d", "other": "Użyj limitu co najmniej 1 B" }, + "core.BoundaryTooSmallWhy": { "hash": "sha256-54b8e801756c", "other": "zestaw potrzebuje rozmiaru o bajt mniejszego od limitu, a poniżej zera nic nie ma" }, + "core.Error": { "hash": "sha256-6dc38cd1cb85", "other": "{{.Error}}" }, + "core.PlanTooLargeFix": { "hash": "sha256-cb36e3de1bfb", "other": "Poproś o mniej plików albo o tańsze w planowaniu - mniej stron, mniej wpisów - albo podziel pracę na kilka przebiegów" }, + "core.PlanTooLargeWhy": { "hash": "sha256-52bd0c2594f6", "other": "cały plan jest trzymany w pamięci, zanim cokolwiek zostanie zapisane, a ta wersja ma na niego sufit {{.Ceiling}} - ile kosztuje zaplanowanie pliku, zależy od formatu, więc sam sufit liczby plików tego nie widzi" }, + "core.RangeBackwards": { "hash": "sha256-1d0ff9d1af62", "other": "zakres biegnie wstecz, od {{.From}} B w dół do {{.To}} B. Mniejszy koniec idzie pierwszy, a zakres biegnący wstecz nie zawiera żadnego rozmiaru. Zapisz go jako {{.Low}}-{{.High}}" }, + "core.RangeNotARange": { "hash": "sha256-ec7fa6194ef2", "other": "{{.Value}} nie jest zakresem. Zakres to dokładnie dwa rozmiary z jednym łącznikiem między nimi, na przykład 1kb-8kb" }, + "core.SizeBelowZero": { "hash": "sha256-a2509071b044", "other": "plik nie może być mniejszy niż zero bajtów" }, + "core.SizeEmpty": { "hash": "sha256-93de7d541324", "other": "pole {{.Setting}} jest puste: wpisz liczbę bajtów albo rozmiar, na przykład 10mb, 1.5gib albo 700kB" }, + "core.SizeNegative": { "hash": "sha256-d1fb1aecfb6b", "other": "{{.Setting}} {{.Value}} jest ujemny: plik nie może być mniejszy niż zero bajtów" }, + "core.SizeNoNumber": { "hash": "sha256-19e757667a89", "other": "{{.Setting}} {{.Value}} nie ma liczby: wpisz coś w rodzaju 10mb albo 1048576" }, + "core.SizeNotANumber": { "hash": "sha256-3a5e57f1b8ed", "other": "{{.Setting}} {{.Value}} nie jest liczbą: wpisz coś w rodzaju 10mb, 1.5gib albo 1048576" }, + "core.SizeNotPlain": { "hash": "sha256-0b892bed8bee", "other": "{{.Setting}} {{.Value}} nie jest zwykłą liczbą: wypisz cyfry, jak 10mb, 1.5gib albo 1048576. Zapisy takie jak 1e5, 0x10 czy 1_000 są odrzucane, a nie zgadywane, tak samo jak w przepisie" }, + "core.SizeNotWhole": { "hash": "sha256-e52c1a8c1d1f", "other": "{{.Setting}} {{.Value}} nie daje całej liczby bajtów: wychodzi {{.Exact}} bajtów. Poproś zamiast tego o {{.Below}} albo {{.Above}}" }, + "core.SizeTooLarge": { "hash": "sha256-dd73ed43bac3", "other": "{{.Setting}} {{.Value}} jest za duży, żeby wyrazić go w bajtach" }, + "core.SizeUnknownUnit": { "hash": "sha256-3cd2ef1da79b", "other": "{{.Setting}} {{.Value}} ma nieznaną jednostkę {{.Unit}}: użyj b, kb, mb, gb albo tb, wszystkie liczone po 1024, a kib, mib, gib albo tib to to samo zapisane w pełni" }, + "core.SizesOverflow": { "hash": "sha256-58d85993b3db", "other": "rozmiary w tym przebiegu sumują się do więcej, niż mieści liczba bajtów, więc sumy nie da się zmierzyć, a sprawdzeniu wolnego miejsca nie można ufać. Poproś o mniej plików albo mniejsze" }, + "core.TooManyFiles": { "hash": "sha256-a0fd66f30b3f", "other": "{{.Why}}. {{.Fix}}" }, + "core.TooManyFilesFix": { "hash": "sha256-f03079434408", "other": "Poproś o mniej plików albo podziel pracę na kilka przebiegów" }, + "core.TooManyFilesWhy": { "hash": "sha256-84bf666cf3a3", "other": "ta wersja planuje najwyżej {{.Most}} plików w jednym przebiegu, bo cały plan powstaje w pamięci, zanim cokolwiek zostanie zapisane - dzięki temu przebieg, który nie może się udać, jest odrzucany przed pierwszym bajtem" }, + "csv.HasToBeOneOf": { "hash": "sha256-150fed02f771", "other": "musi to być {{.Values}}" }, + "csv.MinimumHeader": { "hash": "sha256-002d78cd1305", "other": "tabela zawiera nagłówek i całe wiersze, a po jednym z nich potrzeba tyle" }, + "csv.MinimumRows": { "hash": "sha256-8feb9483f089", "other": "tabela zawiera całe wiersze, a jeden z nich potrzebuje tyle" }, + "damage.Conflict": { "hash": "sha256-6749ebcea05d", "other": "{{.What}}. {{.Instead}}" }, + "damage.ConflictInstead": { "hash": "sha256-e4ac08c1d195", "other": "pomiń oczekiwany wynik, a będzie nim odrzucenie, albo wybierz oczyszczenie, jeśli system pod testem ma naprawić plik, albo nieokreślony, jeśli właśnie o to pytasz" }, + "damage.ConflictWhat": { "hash": "sha256-a9099dc25467", "other": "ta partia uszkadza plik, a jako oczekiwany wynik ma {{.Outcome}}" }, + "damage.ConflictWhy": { "hash": "sha256-b90221bf868f", "other": "uszkodzony plik to taki, który sędzia odrzucał w pomiarze, więc oczekiwanie, że zostanie przyjęty, jest oczekiwaniem, którego nic nie spełni" }, + "damage.NoChange": { "hash": "sha256-14c3ed651919", "other": "{{.What}}. {{.Why}}" }, + "damage.NoChangeInstead": { "hash": "sha256-7b4599e7c06c", "other": "daj uszkodzeniu większy plik albo inne ustawienia, albo zdejmij je z tej partii" }, + "damage.NoChangeWhat": { "hash": "sha256-4f2698e2b793", "other": "{{.Damage}} nie zmieniło niczego w {{.File}}" }, + "damage.NoChangeWhy": { "hash": "sha256-c7feb2a26728", "other": "każdy czytnik przyjąłby ten plik, a manifest nazywałby go uszkodzonym, więc przebieg się zatrzymuje, zamiast go zapisać" }, + "damage.TooSmall": { "hash": "sha256-6749ebcea05d", "other": "{{.What}}. {{.Instead}}" }, + "damage.TooSmallInstead": { "hash": "sha256-cfc27ac4d05a", "other": "Poproś o {{.Floor}} albo więcej albo zdejmij uszkodzenie z tej partii." }, + "damage.TooSmallWhat": { "hash": "sha256-1b4679cf4415", "other": "{{.Damage}} potrzebuje co najmniej {{.Floor}}, a plik ma {{.Requested}}" }, + "damage.TooSmallWhy": { "hash": "sha256-17c864ed0120", "other": "plik mniejszy niż uszkodzenie wyszedłby niezmieniony, a niezmieniony plik opisany jako uszkodzony to jedyne, czego to narzędzie nie może zapisać" }, + "damage.Unknown": { "hash": "sha256-8ac340989b2f", "other": "{{.What}}. Ta wersja ma: {{.Known}}" }, + "damage.UnknownInstead": { "hash": "sha256-a53588601259", "other": "użyj jednego z: {{.Known}}" }, + "damage.UnknownInsteadRemove": { "hash": "sha256-0e2033823316", "other": "usuń linię z uszkodzeniem" }, + "damage.UnknownWhat": { "hash": "sha256-cfd2875327a9", "other": "nie ma uszkodzenia o nazwie {{.Damage}}" }, + "damage.UnknownWhy": { "hash": "sha256-f2720ea32a8a", "other": "uszkodzenie musi być znane tej wersji, a nieznane zostawiłoby plik nienaruszony, podczas gdy manifest nazywałby go uszkodzonym" }, + "docx.Paragraphs": { "hash": "sha256-45316182b77a", "one": "{{.Count}} akapitem", "few": "{{.Count}} akapitami", "many": "{{.Count}} akapitami", "other": "{{.Count}} akapitu" }, + "engine.CannotCreateOutDir": { "hash": "sha256-324037f71709", "other": "nie można utworzyć katalogu wyjściowego {{.Dir}}: {{.Cause}}" }, + "engine.CannotStart": { "hash": "sha256-114c2e153b9d", "other": "nie można zacząć przebiegu w {{.Dir}}: {{.Cause}}" }, + "engine.Character": { "hash": "sha256-add48a171334", "other": "znak {{.Character}}" }, + "engine.ChooseTheDirectory": { "hash": "sha256-111bd1b3e020", "other": "Zamiast tego wybierz katalog w ustawieniu wyjścia" }, + "engine.Collision": { "hash": "sha256-9b5b48b73ad2", "other": "{{.Path}} już istnieje i ten przebieg go nie nadpisze. Generuj do pustego katalogu albo najpierw usuń ten plik" }, + "engine.CollisionInstructions": { "hash": "sha256-f380a98d3a7f", "other": "{{.Path}} już istnieje i ten przebieg go nie nadpisze. Mówi, do czego służą pliki wcześniejszego przebiegu. Generuj do pustego katalogu albo nazwij manifest tego przebiegu inaczej" }, + "engine.CollisionManifest": { "hash": "sha256-cc110e79b974", "other": "{{.Path}} już istnieje i ten przebieg go nie nadpisze. To jedyny zapis tego, co napisał wcześniejszy przebieg, więc zastąpienie go zostawiłoby tamte pliki bez niczego, czym można je usunąć. Generuj do pustego katalogu albo odsuń stary manifest na bok" }, + "engine.ControlCharacter": { "hash": "sha256-519de8af1277", "other": "znak sterujący U+{{.Code}}" }, + "engine.CrashPlanning": { "hash": "sha256-90f7d5da615f", "other": "planowania pliku" }, + "engine.CrashWhile": { "hash": "sha256-071bf3d7823e", "other": "{{.While}}" }, + "engine.CrashWriting": { "hash": "sha256-1e90995301d1", "other": "zapisu {{.File}}" }, + "engine.Files": { "hash": "sha256-83589d643302", "one": "{{.Count}} plik", "few": "{{.Count}} pliki", "many": "{{.Count}} plików", "other": "{{.Count}} pliku" }, + "engine.FilesSoFar": { "hash": "sha256-83589d643302", "one": "{{.Count}} pliku", "few": "{{.Count}} plikach", "many": "{{.Count}} plikach", "other": "{{.Count}} plikach" }, + "engine.GeneratorCrash": { "hash": "sha256-82716d977373", "other": "generator {{.Format}} zatrzymał się na błędzie wewnętrznym w trakcie {{.While}}: {{.Value}}. To wada tego narzędzia, a nie coś, o co przepis może poprosić. Zgłoś ją razem z ustawieniami, które do niej doprowadziły" }, + "engine.IDTwice": { "hash": "sha256-724959681477", "other": "nazwa partii {{.ID}} występuje dwa razy" }, + "engine.IDTwiceWhy": { "hash": "sha256-ded29dfee98c", "other": "nazwy wskazują partie, więc powtórzenie jest błędem, a nie cichym nadpisaniem" }, + "engine.InstructionsNotWritten": { "hash": "sha256-7a1261279b19", "other": "nie można zapisać instrukcji do {{.Path}}: {{.Cause}}" }, + "engine.NameAbsolute": { "hash": "sha256-2dc5711eaa7d", "other": "{{.Where}} daje ścieżkę bezwzględną {{.Name}}" }, + "engine.NameAbsoluteWhy": { "hash": "sha256-61f6a0c76deb", "other": "przepis nie niesie ścieżek bezwzględnych, bo wtedy działa tylko na maszynie, na której go napisano" }, + "engine.NameColon": { "hash": "sha256-9346cfb2e4a8", "other": "{{.Where}} daje nazwę {{.Name}}, w której jest dwukropek" }, + "engine.NameColonFix": { "hash": "sha256-08dfaced15cc", "other": "Usuń dwukropek albo poproś o plik wewnątrz archiwum, gdzie nazwa przetrwa" }, + "engine.NameColonWhy": { "hash": "sha256-f033766b9cf1", "other": "Windows czyta go jako dysk albo alternatywny strumień danych, a nie jako część nazwy, więc plik trafia pod inną nazwą albo wcale. Jest odrzucany w każdym systemie, żeby przepis wszędzie znaczył to samo" }, + "engine.NameEmpty": { "hash": "sha256-a8beb5c74396", "other": "{{.Where}} daje plik bez nazwy" }, + "engine.NameForbidden": { "hash": "sha256-bf6388aa9fbb", "other": "{{.Where}} daje nazwę {{.Name}}, w której jest {{.Character}}" }, + "engine.NameForbiddenFix": { "hash": "sha256-1d1b81343f2c", "other": "Usuń ten znak albo poproś o plik wewnątrz archiwum, gdzie nazwa przetrwa" }, + "engine.NameForbiddenWhy": { "hash": "sha256-c7a749a53ea6", "other": "Windows nie przyjmuje tego znaku w nazwie pliku, więc plik w ogóle nie zostanie tam zapisany. Jest odrzucany w każdym systemie, żeby przepis wszędzie znaczył to samo" }, + "engine.NameIsADirectory": { "hash": "sha256-93b2fa7a0d98", "other": "{{.Where}} daje nazwę {{.Name}}, która oznacza katalog, a nie plik" }, + "engine.NameIsAPath": { "hash": "sha256-934e299eb38c", "other": "{{.Where}} daje nazwę {{.Name}}, która jest ścieżką, a nie nazwą pliku" }, + "engine.NameIsAPathWhy": { "hash": "sha256-8caafc0ee913", "other": "nazwy zostają wewnątrz katalogu wyjściowego, a separator jest odrzucany w każdym systemie, żeby przepis działał wszędzie" }, + "engine.NameIsTheInstructions": { "hash": "sha256-dfd7477f00a7", "other": "partia {{.Target}} daje plik o nazwie {{.Name}}, a tak ten przebieg nazywa instrukcje obok swojego manifestu" }, + "engine.NameIsTheInstructionsWhy": { "hash": "sha256-496cccf01d31", "other": "oba trafiają do katalogu wyjściowego, a instrukcje mówią, do czego służy każdy plik przebiegu, więc jeden z dwóch by przepadł" }, + "engine.NameIsTheManifest": { "hash": "sha256-7ee240c4a6a6", "other": "partia {{.Target}} daje plik o nazwie {{.Name}}, a tak ten przebieg nazywa swój manifest" }, + "engine.NameIsTheManifestWhy": { "hash": "sha256-19b2818db573", "other": "oba trafiają do katalogu wyjściowego, więc plik zająłby nazwę potrzebną manifestowi, a przebieg skończyłby się plikami i niczym, czym można je usunąć" }, + "engine.NameNul": { "hash": "sha256-5453f2b2ff0c", "other": "{{.Where}} daje nazwę {{.Name}}, która w Windows oznacza urządzenie puste, a nie plik" }, + "engine.NameNulFix": { "hash": "sha256-018e40bbb5cb", "other": "Daj jej rozszerzenie - nul.txt to zwykła nazwa - albo wybierz inną" }, + "engine.NameNulWhy": { "hash": "sha256-3c22a55bc686", "other": "zapis tam się udaje, a bajty idą donikąd, więc przebieg zapisałby plik, którego nie ma na dysku. Jest odrzucana w każdym systemie, żeby przepis wszędzie znaczył to samo" }, + "engine.NameTemplateFix": { "hash": "sha256-483ceb34910b", "other": "Jedyny znacznik to {{.Placeholder}}, więc nazwa wygląda jak invoice_{{.Example}}.pdf" }, + "engine.NameTemplateUnknown": { "hash": "sha256-2eaea446230c", "other": "partia {{.Target}} ma szablon nazw, którego ta wersja nie rozumie: {{.Template}}" }, + "engine.NameTheManifestElse": { "hash": "sha256-d5c9c9065282", "other": "Daj partii szablon nazw ze znacznikiem {{.Placeholder}} albo nazwij manifest inaczej" }, + "engine.NameTooLong": { "hash": "sha256-a45124f3b035", "other": "{{.Where}} daje nazwę {{.Name}}, która ma {{.Length}} bajtów" }, + "engine.NameTooLongFix": { "hash": "sha256-345b133ae94d", "other": "Skróć ją do {{.Most}} bajtów albo mniej albo poproś o plik wewnątrz archiwum, gdzie nazwa przetrwa" }, + "engine.NameTooLongWhy": { "hash": "sha256-08a626e33035", "other": "Linux mieści w nazwie pliku najwyżej {{.Most}} bajtów, a litera spoza ASCII zajmuje od dwóch do czterech z nich. Jest odrzucana w każdym systemie, żeby przepis wszędzie znaczył to samo" }, + "engine.NameTrailing": { "hash": "sha256-9e6def79fc88", "other": "{{.Where}} daje nazwę {{.Name}}, która kończy się kropką albo spacją" }, + "engine.NameTrailingFix": { "hash": "sha256-92a29aace20d", "other": "Usuń ostatni znak albo poproś o plik wewnątrz archiwum, gdzie nazwa przetrwa" }, + "engine.NameTrailingWhy": { "hash": "sha256-b080589d9f64", "other": "Windows zapisuje taką nazwę bez niego, więc plik na dysku nie byłby plikiem, który opisuje manifest, a verify zgłosiłby oba" }, + "engine.NamesClash": { "hash": "sha256-64fede2af0f4", "other": "partie {{.First}} i {{.Second}} dają plik o tej samej nazwie {{.Name}}" }, + "engine.NamesClashCase": { "hash": "sha256-54d889c75915", "other": "partie {{.First}} i {{.Second}} dają nazwy {{.FirstName}} i {{.SecondName}}, które różnią się tylko wielkością liter. Większość systemów plików traktuje je jako jeden plik, więc jeden nadpisałby drugi, a manifest opisywałby oba" }, + "engine.NamesClashFix": { "hash": "sha256-224675591d56", "other": "Daj jednej z nich szablon nazw ze znacznikiem {{.Placeholder}}" }, + "engine.NamesClashFolded": { "hash": "sha256-906d3beae424", "other": "partie {{.First}} i {{.Second}} dają nazwy {{.FirstName}} i {{.SecondName}}. To różne litery, które znaczą to samo - ostre s wobec ss, długie s wobec s, ligatura wobec liter, z których powstała. macOS zapisuje obie pod jedną nazwą, więc jeden plik nadpisałby drugi, a manifest opisywałby oba" }, + "engine.NamesClashSpelling": { "hash": "sha256-26f82646a0e5", "other": "partie {{.First}} i {{.Second}} dają nazwy {{.FirstName}} i {{.SecondName}}. Wyglądają tak samo, bo to jedna nazwa zapisana na dwa sposoby - litera z akcentem wobec zwykłej litery z akcentem jako osobnym znakiem. macOS zapisuje obie pod jedną nazwą, więc jeden plik nadpisałby drugi, a manifest opisywałby oba" }, + "engine.NoFiles": { "hash": "sha256-8adff4655ee0", "other": "partia {{.Target}} prosi o 0 plików" }, + "engine.NoFilesFix": { "hash": "sha256-b43efdfdc1cf", "other": "Poproś o co najmniej jeden" }, + "engine.NoID": { "hash": "sha256-9788e1abb850", "other": "partia nie ma nazwy" }, + "engine.NoIDWhy": { "hash": "sha256-363605924eb4", "other": "każda partia potrzebuje stałej nazwy, bo od niej zależy ziarno i to ona łączy partię z manifestem" }, + "engine.NoOutDir": { "hash": "sha256-9ff19de0e58b", "other": "katalog wyjściowy jest pusty" }, + "engine.NoOutDirFix": { "hash": "sha256-c14426ffc389", "other": "Podaj katalog, na przykład ./fixtures" }, + "engine.NoSpace": { "hash": "sha256-0e6457cf7a3b", "other": "ten przebieg potrzebuje {{.Needed}} B, a w {{.Dir}} jest wolne {{.Free}} B - nic nie zostało zapisane. Poproś o mniej plików albo mniejszy rozmiar albo zapisz na inny dysk, zmieniając katalog wyjściowy" }, + "engine.NotProduced": { "hash": "sha256-46fb82f6985b", "other": "Ten plik nie powstał. Przebieg poszedł dalej i skończył się kodem wyjścia oznaczającym częściowy wynik." }, + "engine.OutDirIsAFile": { "hash": "sha256-55a340f0b67d", "other": "katalog wyjściowy {{.Dir}} jest plikiem, a nie katalogiem" }, + "engine.OutDirIsAFileFix": { "hash": "sha256-f9e5f71f3822", "other": "Wskaż jako katalog wyjściowy katalog albo taki, którego jeszcze nie ma - zostanie utworzony" }, + "engine.PlanTooLarge": { "hash": "sha256-bc308275bb7d", "other": "plan tego przebiegu doszedł do {{.Used}} po {{.Files}}, a sufit to {{.Ceiling}}" }, + "engine.RefusalFix": { "hash": "sha256-fbb793af1f23", "other": "{{.What}}. {{.Fix}}" }, + "engine.RefusalWhy": { "hash": "sha256-657cfcd8b903", "other": "{{.What}} - {{.Why}}" }, + "engine.RefusalWhyFix": { "hash": "sha256-ba47f922ee57", "other": "{{.What}} - {{.Why}}. {{.Fix}}" }, + "engine.RunInProgress": { "hash": "sha256-790ae36784b4", "other": "inny przebieg już zapisuje do {{.Dir}}, więc ten się nie zacznie. Dwa przebiegi zapisujące do jednego katalogu mogą nadpisać sobie pliki tak, że żaden o tym nie powie. Poczekaj, aż tamten skończy, albo generuj do innego katalogu. Jeśli nic nie działa, tamten przebieg został zabity, zanim zdążył po sobie posprzątać - usuń {{.Lock}} i spróbuj jeszcze raz" }, + "engine.RunTooLarge": { "hash": "sha256-aff817760566", "other": "partia {{.Target}} doprowadza przebieg do rozmiaru zbyt dużego, żeby go zmierzyć" }, + "engine.SizeMoved": { "hash": "sha256-e0c05c6f958b", "other": "Rozmiar wylosowany z zakresu nie jest taki, jaki ten format potrafi zapisać, więc użyto najbliższego, który potrafi. Plik dalej mieści się w zakresie, o który proszono." }, + "engine.Target": { "hash": "sha256-f4062d7dbd6c", "other": "partia {{.Target}}" }, + "engine.Targets": { "hash": "sha256-7f9636c1f771", "one": "{{.Count}} partii", "few": "{{.Count}} partiach", "many": "{{.Count}} partiach", "other": "{{.Count}} partiach" }, + "engine.TheInstructions": { "hash": "sha256-074853502278", "other": "instrukcje obok manifestu" }, + "engine.TheManifest": { "hash": "sha256-b802ab9f53ca", "other": "manifest" }, + "engine.TooManyFiles": { "hash": "sha256-a18427eed894", "other": "ten przebieg prosi o {{.Files}} w {{.Targets}}" }, + "format.AAlreadyNeedsThatMuchBefore": { "hash": "sha256-c48d23d9a134", "other": "{{.Opts}}{{.Carrying}} już potrzebuje tyle, zanim dojdzie jakiekolwiek dopełnienie" }, + "format.ABMPStatesItsOwnSize": { "hash": "sha256-dbd63989b4da", "other": "BMP zapisuje swój rozmiar w polu czterobajtowym, więc ten format nie opisze tak dużego pliku" }, + "format.AByteOrderMarkIsB": { "hash": "sha256-4efd8df69acd", "other": "znacznik kolejności bajtów ma {{.Mark}} B, a plik musi go pomieścić" }, + "format.ADeckOfAlreadyPackagesTo": { "hash": "sha256-34617932d22a", "other": "prezentacja z {{.Count}} już po spakowaniu zajmuje tyle - prezentacja potrzebuje wzorca, układu i motywu, czy je pokazuje, czy nie" }, + "format.ADocumentHoldsADeclarationA": { "hash": "sha256-7f2c76645ed2", "other": "dokument zawiera deklarację, element główny i całe rekordy, a po jednym z nich potrzeba tyle" }, + "format.ADocumentHoldsARootKey": { "hash": "sha256-e288d9c236e7", "other": "dokument zawiera klucz główny i całe rekordy, a po jednym z nich potrzeba tyle" }, + "format.ADocumentHoldsWholeRecordsAnd": { "hash": "sha256-c002a9e365fb", "other": "dokument zawiera całe rekordy, a jeden rekord {{.Name}} ze wszystkimi typami wartości potrzebuje tyle" }, + "format.ADocumentHoldsWholeRecordsAnd2": { "hash": "sha256-651b6768a03d", "other": "dokument zawiera całe rekordy, a jedna ich tabela ze wszystkimi typami wartości potrzebuje tyle" }, + "format.ADocumentOfAlreadyPackagesTo": { "hash": "sha256-9f7b489fb2f9", "other": "dokument z {{.Count}} już po spakowaniu zajmuje tyle, a plik Worda nie może pominąć typów zawartości ani relacji" }, + "format.ADrawingHoldsADeclarationA": { "hash": "sha256-bd5f623311d9", "other": "rysunek zawiera deklarację, element główny i całe kształty, a po jednym z nich potrzeba tyle" }, + "format.AFileCannotHoldFewerThan": { "hash": "sha256-bb90f7e86bf0", "other": "plik nie może zawierać mniej niż zero bajtów" }, + "format.ALogHoldsWholeEntriesAnd": { "hash": "sha256-6e80433af493", "other": "log zawiera całe wpisy, a jeden wpis w kształcie {{.ID}} potrzebuje tyle" }, + "format.APasswordWasGivenAndThis": { "hash": "sha256-9dead9140bc0", "other": "podano hasło, a to ustawienie mówi, że archiwum nie jest zamknięte, więc jedno z dwóch nie jest tym, co miałeś na myśli" }, + "format.ARIFFFileStatesItsOwn": { "hash": "sha256-beaa6ef535a9", "other": "plik RIFF zapisuje swoją długość w polu czterobajtowym, więc ten format nie opisze tak dużego pliku" }, + "format.ARateSaysHowFastEntries": { "hash": "sha256-d38f8ddf9901", "other": "tempo mówi, jak szybko przychodzą wpisy, a timestamps=fixed stawia je wszystkie w jednej chwili" }, + "format.ASheetOfByAlreadyPackages": { "hash": "sha256-4246af222774", "other": "arkusz {{.Rows}} na {{.Columns}} już po spakowaniu zajmuje tyle, a skoroszyt nie może pominąć typów zawartości ani relacji" }, + "format.ATIFFLocatesItsPixelsWith": { "hash": "sha256-0e6ed3d21e3b", "other": "TIFF wskazuje swoje piksele czterobajtowym przesunięciem, więc ten format nie opisze tak dużego pliku" }, + "format.AWAVNeedsItsRIFFHeader": { "hash": "sha256-bca949f9e7fd", "other": "WAV potrzebuje nagłówka RIFF, fragmentu formatu i nagłówka fragmentu danych, zanim pojawi się choćby jedna próbka" }, + "format.AWebPDeclaresItsLengthIn": { "hash": "sha256-4beea662d79f", "other": "WebP zapisuje swoją długość w polu czterobajtowym, więc ten format nie opisze tak dużego pliku" }, + "format.AXIconHoldingAIs": { "hash": "sha256-9aaa563130b9", "other": "ikona {{.W}}x{{.H}} z obrazem {{.Embed}} to {{.Picture}} B obrazu, a przed nim stoi katalog o {{.Preamble}} B" }, + "format.AXPictureAlreadyEncodesTo": { "hash": "sha256-1ce454591837", "other": "obrazek {{.Width}}x{{.Height}} już po zakodowaniu zajmuje tyle, zanim dojdzie jakiekolwiek dopełnienie" }, + "format.AXPictureAtQualityEncodes": { "hash": "sha256-69d0bf02a8f6", "other": "obrazek {{.W}}x{{.H}} w jakości {{.Quality}} koduje się do {{.Bare}} B i zawsze niesie segment komentarza, który kosztuje {{.ComOverhead}} B nawet pusty" }, + "format.AXPictureEncodesToExactly": { "hash": "sha256-a13cd5c964ec", "other": "obrazek {{.Width}}x{{.Height}} koduje się do dokładnie {{.Bare}} B, pusty komentarz dodaje {{.EmptyComment}} B, a najmniejszy komentarz niosący jakiekolwiek dopełnienie dodaje {{.SmallestCarryingComment}} B, więc nic pomiędzy nie jest osiągalne" }, + "format.AXPictureEncodesToExactly2": { "hash": "sha256-f4e7a12edc2d", "other": "obrazek {{.W}}x{{.H}} koduje się do dokładnie {{.Bare}} B, a fragment dopełnienia, który wyrównuje różnicę, sam kosztuje {{.ChunkOverhead}} B, więc nic pomiędzy tymi dwiema wartościami nie jest osiągalne" }, + "format.AXPictureIsBOf": { "hash": "sha256-17ec22f5de5d", "other": "obrazek {{.W}}x{{.H}} to {{.PixelBytes}} B pikseli po zaokrągleniu każdego wiersza w górę do czterech bajtów, a przed nimi stoi nagłówek o {{.Headers}} B" }, + "format.AXPictureIsBOf2": { "hash": "sha256-e4b53453234d", "other": "obrazek {{.W}}x{{.H}} to {{.PixelBytes}} B pikseli po trzy bajty na piksel, a nagłówek, katalog i jego wartości zajmują kolejne {{.Overhead}} B" }, + "format.AXPictureIsBOf3": { "hash": "sha256-eb9dad08cc09", "other": "obrazek {{.W}}x{{.H}} to {{.W2}} B pikseli po trzy bajty na piksel, a kontener i tablice kodowania zajmują kolejne {{.Bare}} B" }, + "format.AboveMaximum": { "hash": "sha256-595465db6fd3", "other": "{{.Format}} nie może mieć więcej niż {{.Maximum}} B - {{.Why}}. Żądano: {{.Requested}} B. {{.Fix}}" }, + "format.AboveMaximumWhat": { "hash": "sha256-543087dbdc70", "other": "{{.Format}} nie może mieć więcej niż {{.Maximum}} B. Żądano: {{.Requested}} B" }, + "format.AnArchiveHoldingAlreadyNeedsThat": { "hash": "sha256-944272a047fc", "other": "archiwum zawierające {{.Groups}} już potrzebuje tyle, a nic nie jest kompresowane" }, + "format.AnArchiveHoldingAlreadyNeedsThat2": { "hash": "sha256-8ae8f9d3962c", "other": "archiwum zawierające {{.Groups}} już potrzebuje tyle" }, + "format.AnArchiveHoldingNeedsThatMuch": { "hash": "sha256-9d357c4329f8", "other": "archiwum zawierające {{.Groups}} potrzebuje tyle, zanim cokolwiek zostanie ściśnięte, a o ile się ściśnie, wiadomo dopiero po ściśnięciu" }, + "format.AnArchiveThisLargeWouldNeed": { "hash": "sha256-64b57daffe59", "other": "archiwum tak duże potrzebowałoby rekordów zip64, a ta wersja wylicza rozmiar archiwum przed zapisem w sposób, który ich nie uwzględnia" }, + "format.AnIconStatesWhereItsPicture": { "hash": "sha256-89eb5442cbef", "other": "ikona zapisuje, gdzie zaczyna się jej obraz, w polu czterobajtowym, więc ten format nie opisze tak dużego pliku" }, + "format.AnOfficeFileIsAZIP": { "hash": "sha256-7b6cc559cff5", "other": "plik Office to archiwum ZIP, a ta wersja wylicza jego rozmiar przed zapisem w sposób, który nie uwzględnia rekordów zip64 potrzebnych większemu plikowi" }, + "format.AskFor0BOrMore": { "hash": "sha256-d83d8503ba9c", "other": "Poproś o 0 B albo więcej." }, + "format.AskFor4GiBOrLess": { "hash": "sha256-00fdb9c1486c", "other": "Poproś o 4 GiB albo mniej albo wybierz format bez własnego pola rozmiaru, na przykład gif." }, + "format.AskFor4GiBOrLess2": { "hash": "sha256-1e9c7b671e63", "other": "Poproś o 4 GiB albo mniej albo wybierz format bez własnego pola przesunięcia, na przykład gif." }, + "format.AskFor4GiBOrLess3": { "hash": "sha256-8853202486d3", "other": "Poproś o 4 GiB albo mniej albo wybierz format bez własnego przesunięcia, na przykład gif." }, + "format.AskFor4GiBOrLess4": { "hash": "sha256-312924ab27ec", "other": "Poproś o 4 GiB albo mniej albo wybierz format bez własnego pola długości, na przykład txt." }, + "format.AskFor4GiBOrLess5": { "hash": "sha256-0ecad3b0b8a9", "other": "Poproś o 4 GiB albo mniej albo wybierz format bez własnego pola długości, na przykład gif." }, + "format.AskForASmallerSizeOr": { "hash": "sha256-424ebdc15921", "other": "Poproś o mniejszy rozmiar albo ustaw większe wymiary, żeby sam obrazek niósł więcej z niego." }, + "format.AskForBOrB": { "hash": "sha256-04c46025145c", "other": "Poproś o {{.Requested}} B albo {{.Next}} B." }, + "format.AskForBOrLessOr": { "hash": "sha256-d86fed5097fb", "other": "Poproś o {{.Below}} B albo mniej albo o {{.Above}} B albo więcej." }, + "format.AskForBOrMore": { "hash": "sha256-b8c0c503ff88", "other": "Poproś o {{.Min}} B albo więcej." }, + "format.AskForBOrMore2": { "hash": "sha256-af279473fd39", "other": "Poproś o {{.Floor}} B albo więcej{{.CleanHint}}." }, + "format.AskForBOrMoreOr": { "hash": "sha256-208b2345e8bb", "other": "Poproś o {{.Floor}} B albo więcej albo ustaw mniejszą szerokość i wysokość albo niższą jakość" }, + "format.AskForBOrMoreOr10": { "hash": "sha256-b691ac49b317", "other": "Poproś o {{.Target}} B albo więcej albo ustaw Kompresję na brak." }, + "format.AskForBOrMoreOr11": { "hash": "sha256-f426b70414a5", "other": "Poproś o {{.Floor}} B albo więcej albo włóż mniej plików albo mniejsze." }, + "format.AskForBOrMoreOr2": { "hash": "sha256-3f92483ea6fc", "other": "Poproś o {{.Bare}} B albo więcej albo ustaw mniejszą szerokość i wysokość" }, + "format.AskForBOrMoreOr3": { "hash": "sha256-c6080ef6701f", "other": "Poproś o {{.Bare}} B albo więcej albo ustaw mniej akapitów" }, + "format.AskForBOrMoreOr4": { "hash": "sha256-647ee0bda35c", "other": "Poproś o {{.Min}} B albo więcej albo wybierz krótszy kształt wpisu." }, + "format.AskForBOrMoreOr5": { "hash": "sha256-09e6c08df925", "other": "Poproś o {{.Bare}} B albo więcej albo ustaw mniej slajdów" }, + "format.AskForBOrMoreOr6": { "hash": "sha256-cf805d0df5e1", "other": "Poproś o {{.Bare}} B albo więcej albo włóż mniej plików albo mniejsze." }, + "format.AskForBOrMoreOr7": { "hash": "sha256-b2a756076baf", "other": "Poproś o {{.Bare}} B albo więcej albo zostaw etykietę włączoną - wtedy działa każdy rozmiar od {{.Bare2}} B." }, + "format.AskForBOrMoreOr8": { "hash": "sha256-8e38c6016502", "other": "Poproś o {{.Mark}} B albo więcej albo wyłącz ustawienie {{.SettingBOM}}." }, + "format.AskForBOrMoreOr9": { "hash": "sha256-a629bffc5f72", "other": "Poproś o {{.Bare}} B albo więcej albo ustaw mniej wierszy" }, + "format.AskForBOrMoreSet": { "hash": "sha256-77d3cf72434d", "other": "Poproś o {{.Bare}} B albo więcej, ustaw mniejszą szerokość i wysokość albo ustaw embed={{.EmbedPNG}}" }, + "format.AskForEntriesOrFewer": { "hash": "sha256-dba2cac4aaed", "other": "Poproś o najwyżej {{.MaxEntries}} wpisów." }, + "format.AskForExactlyBExactlyB": { "hash": "sha256-2e1e3ea291d3", "other": "Poproś o dokładnie {{.Bare}} B, dokładnie {{.Bare2}} B albo o {{.Bare3}} B albo więcej." }, + "format.AskForExactlyBOrFor": { "hash": "sha256-b926717011c3", "other": "Poproś o dokładnie {{.Bare}} B albo o {{.Bare2}} B albo więcej." }, + "format.AskForLessThan4GiB": { "hash": "sha256-1f62e6b78697", "other": "Poproś o mniej niż 4 GiB albo rozłóż treść na kilka plików." }, + "format.AskForLessThan4GiB2": { "hash": "sha256-a6bbed8e80cd", "other": "Poproś o mniej niż 4 GiB albo rozłóż zawartość na kilka archiwów." }, + "format.AtBThisFileHasRoom": { "hash": "sha256-672437efe0de", "other": "Przy {{.Bytes}} B ten plik ma miejsce na nagłówki, ale nie na choćby jedną ramkę dźwięku, więc nie zawiera dźwięku. Nadal jest poprawnym plikiem WAV." }, + "format.BelowMinimum": { "hash": "sha256-5f4163ec5ef3", "other": "{{.Format}} nie może mieć mniej niż {{.Minimum}} B - {{.Why}}. Żądano: {{.Requested}} B. {{.Fix}}" }, + "format.BelowMinimumWhat": { "hash": "sha256-951d66fcbb5f", "other": "{{.Format}} nie może mieć mniej niż {{.Minimum}} B. Żądano: {{.Requested}} B" }, + "format.BothReadersOnThisMachineRefuse": { "hash": "sha256-0c8d02726af7", "other": "oba czytniki na tej maszynie odrzucają plik TOML, który zaczyna się znacznikiem kolejności bajtów, nawet w UTF-8" }, + "format.CannotBe": { "hash": "sha256-4c716fd14bea", "other": "{{.Format}}: {{.Key}} nie może mieć wartości {{.Value}}" }, + "format.CannotTake": { "hash": "sha256-f4cf771e6ee1", "other": "{{.Format}} nie przyjmuje {{.Key}}" }, + "format.CannotTakeWhy": { "hash": "sha256-657cfcd8b903", "other": "{{.What}} - {{.Why}}" }, + "format.CannotTakeWhyFix": { "hash": "sha256-ba47f922ee57", "other": "{{.What}} - {{.Why}}. {{.Fix}}" }, + "format.ContentsConflict": { "hash": "sha256-c02ff58536fe", "other": "{{.Format}}: contains i właściwości {{.Keys}} mówią obie, co zawiera archiwum. Zostaw contains i usuń właściwości albo odwrotnie" }, + "format.ContentsConflictOne": { "hash": "sha256-274eb866ce96", "other": "{{.Format}}: contains i właściwość {{.Key}} mówią obie, co zawiera archiwum. Zostaw contains i usuń właściwość albo odwrotnie" }, + "format.CountOf": { "hash": "sha256-8efd2508206c", "other": "{{.Number}} {{.Unit}}" }, + "format.ExactlyOf": { "hash": "sha256-8efd2508206c", "other": "{{.Number}} {{.Unit}}" }, + "format.GroupOf": { "hash": "sha256-72d3bb0ede42", "one": "{{.Count}} plik {{.Kind}} po {{.Bytes}} B", "few": "{{.Count}} pliki {{.Kind}} po {{.Bytes}} B", "many": "{{.Count}} plików {{.Kind}} po {{.Bytes}} B", "other": "{{.Count}} pliku {{.Kind}} po {{.Bytes}} B" }, + "format.Groups": { "hash": "sha256-211cf5f97e38", "other": "{{.Groups}}" }, + "format.HasToBeWholeNumberOf": { "hash": "sha256-5755e0fe2273", "other": "musi to być liczba całkowita {{.Unit}}" }, + "format.ItHasToBeAWhole": { "hash": "sha256-4012124b4c7c", "other": "musi to być liczba całkowita" }, + "format.ItHasToBeAdvancingOr": { "hash": "sha256-47f684fea840", "other": "musi to być rosnące albo stałe" }, + "format.ItHasToBeBetweenAnd": { "hash": "sha256-baa0b5548801", "other": "musi to być liczba od {{.MinColumns}} do {{.MaxColumns}}" }, + "format.ItHasToBeGetRead": { "hash": "sha256-82e0c4023c3b", "other": "musi to być get, odczyt albo mieszane" }, + "format.ItHasToBeLfOr": { "hash": "sha256-3c7dd4cf3524", "other": "musi to być lf albo crlf" }, + "format.ItHasToBeOr": { "hash": "sha256-fd792a800341", "other": "musi to być {{.EmbedBMP}} albo {{.EmbedPNG}}" }, + "format.ItHasToBeRealisticQuiet": { "hash": "sha256-4333eb6e7404", "other": "musi to być realistycznie, cicho, błędy albo szczegółowo" }, + "format.ItHasToBeRealisticSuccess": { "hash": "sha256-3aa1143e7c48", "other": "musi to być realistycznie, sukcesy, błędy klienta albo błędy serwera" }, + "format.ItHasToBeTrueOr": { "hash": "sha256-71042abeb07a", "other": "musi to być true albo false" }, + "format.ItHasToBeV4V6": { "hash": "sha256-55bcc5393f4f", "other": "musi to być v4, v6 albo mieszane" }, + "format.ItIsNotAPermissionWritten": { "hash": "sha256-8a4d58bcc9cf", "other": "to nie są uprawnienia zapisane cyframi ósemkowymi" }, + "format.ItIsNotOneOfThe": { "hash": "sha256-4478409a7903", "other": "to nie jest żaden z kształtów, które ten format zapisuje" }, + "format.ItTakesADateWrittenAs": { "hash": "sha256-26cf0d660406", "other": "przyjmuje datę zapisaną jako 2024-02-29, 2024-02-29T13:45:00 albo 2024-02-29T13:45:00+02:00, albo none" }, + "format.ItTakesAWholeNumberFrom": { "hash": "sha256-3333fcc150b7", "other": "przyjmuje liczbę całkowitą od 0 do {{.MaxEntries}}" }, + "format.ItTakesTextAndThisValue": { "hash": "sha256-fd5ffed3d609", "other": "przyjmuje tekst, a ta wartość nie jest poprawnym UTF-8" }, + "format.ItTakesTrueOrFalse": { "hash": "sha256-20eb24e18b8f", "other": "przyjmuje true albo false" }, + "format.JointEachSide": { "hash": "sha256-c105de306e1f", "other": "{{.Why}}. Każdy bok może mieć do {{.Most}}, ale nie oba naraz - poproś o mniejszą parę" }, + "format.JointSmallerPair": { "hash": "sha256-a05cb7a754cc", "other": "{{.Why}}. Poproś o mniejszą parę" }, + "format.JointTooMuch": { "hash": "sha256-8c4dbfc1e55c", "other": "razem dają {{.Asked}}, a limit to {{.Limit}}, bo {{.Why}}" }, + "format.MixedPagesNeedAtLeastTwo": { "hash": "sha256-338f9f2bd35e", "other": "mieszane strony wymagają co najmniej dwóch stron, a ten dokument ma jedną" }, + "format.NeitherTarNorGzipHasAny": { "hash": "sha256-b3831ecb57f7", "other": "ani tar, ani gzip nie mają żadnego szyfrowania, więc w żadnym z nich nie ma pola na hasło" }, + "format.NeitherTarNorGzipHasAny2": { "hash": "sha256-92f4ef797f00", "other": "ani tar, ani gzip nie mają żadnego szyfrowania, więc nie ma między czym wybierać" }, + "format.NestingUnsupported": { "hash": "sha256-f2cd29c6111d", "other": "{{.Format}} nie może jeszcze zawierać {{.Inner}} - archiwum w archiwum wymaga najpierw limitu głębokości. Włóż inny format albo zbuduj wewnętrzne archiwum jako osobną partię" }, + "format.NoCalendarHasThatDayTime": { "hash": "sha256-4771df073166", "other": "żaden kalendarz nie ma takiego dnia, godziny ani strefy" }, + "format.NoSuchProperty": { "hash": "sha256-d6b4359c3811", "other": "{{.Format}} nie ma właściwości o nazwie {{.Key}}" }, + "format.NoSuchPropertyKnown": { "hash": "sha256-129d78741cc6", "other": "{{.What}}. Przyjmuje: {{.Known}}" }, + "format.NotAContainer": { "hash": "sha256-4bbda5317b35", "other": "{{.Format}} nie zawiera innych plików, więc nie przyjmuje contains" }, + "format.NotAContainerFix": { "hash": "sha256-2d1a3a9567c1", "other": "Usuń contains albo zmień format" }, + "format.NotAContainerWhole": { "hash": "sha256-ba47f922ee57", "other": "{{.What}} - {{.Why}}. {{.Fix}}" }, + "format.NotAContainerWhy": { "hash": "sha256-ac844612fe99", "other": "formaty, które to potrafią, to {{.Containers}}" }, + "format.RemoveTheLine": { "hash": "sha256-14c26fa261f4", "other": "usuń ten wiersz" }, + "format.SetPagesTo2OrMore": { "hash": "sha256-92ea12d95952", "other": "Ustaw pole Strony na 2 albo więcej albo {{.Other}}." }, + "format.Setting": { "hash": "sha256-1a0545a47535", "other": "{{.Name}}" }, + "format.StoresTwoBytesForEveryCharacter": { "hash": "sha256-0e9ed2b9d1c1", "other": "{{.Name}} zapisuje dwa bajty na każdy znak, więc cały plik ma zawsze parzystą ich liczbę" }, + "format.TOMLIsUTF8ByIts": { "hash": "sha256-ece7fa6fa508", "other": "TOML z definicji jest w UTF-8, więc dokument nie może mieć innego kodowania" }, + "format.TakesNoProperties": { "hash": "sha256-68129a25e9b5", "other": "{{.Format}} nie przyjmuje żadnych właściwości, więc {{.Key}} nie jest jedną z nich" }, + "format.TakesOneOf": { "hash": "sha256-bc4247426c03", "other": "przyjmuje jedną z wartości: {{.Values}}" }, + "format.TakesRange": { "hash": "sha256-46bf5c66d883", "other": "przyjmuje liczbę całkowitą od {{.Least}} do {{.Most}}" }, + "format.TakesRangeOf": { "hash": "sha256-a381ff9c7edc", "other": "przyjmuje liczbę całkowitą {{.Unit}} od {{.Least}} do {{.Most}}" }, + "format.TakesSize": { "hash": "sha256-df427136497a", "other": "przyjmuje rozmiar taki jak 2mb albo zwykłą liczbę bajtów" }, + "format.TakesTrueOrFalse": { "hash": "sha256-20eb24e18b8f", "other": "przyjmuje true albo false" }, + "format.TakesWholeNumber": { "hash": "sha256-b075aeaef437", "other": "przyjmuje liczbę całkowitą" }, + "format.TakesWholeNumberOf": { "hash": "sha256-1978d684bebc", "other": "przyjmuje liczbę całkowitą {{.Unit}}" }, + "format.TargzThePaddingOfThisCompressed": { "hash": "sha256-aacb4fd044e0", "other": "targz: dopełnienie tego skompresowanego archiwum nie ustala się po {{.SolveRounds}} rundach. Poproś o inny rozmiar albo ustaw Kompresję na brak" }, + "format.ThatIsWhatTheContentsCome": { "hash": "sha256-5a2c6dc3bbe0", "other": "tyle zajmuje zawartość po skompresowaniu, więc nic nie da się już odjąć" }, + "format.TheArchiveCommentCarriesAtMost": { "hash": "sha256-2b5084f09b68", "other": "komentarz archiwum mieści najwyżej {{.CommentLimit}} B, a najmniejsza dodatkowa część kosztuje {{.FillerOverhead}} B, więc nic pomiędzy tymi dwiema wartościami nie jest osiągalne" }, + "format.TheDrawingIsPxTallAnd": { "hash": "sha256-39369dd5d5ed", "other": "Rysunek ma {{.H}} px wysokości, a etykieta potrzebuje paska {{.TextBand}} px na dole, więc ten plik nie niesie widocznej etykiety. Wciąż wskazują go jego nazwa i manifest." }, + "format.TheIconIsPxWideAnd": { "hash": "sha256-aa4c94da4914", "other": "Ikona ma {{.W}} px szerokości, a etykieta potrzebuje więcej miejsca, więc ten plik nie niesie widocznej etykiety. Wciąż wskazują go jego nazwa i manifest." }, + "format.TheLabelCommentNeedsBAnd": { "hash": "sha256-e2a47de40153", "other": "Komentarz z etykietą potrzebuje {{.Length}} B, a ten plik nie ma na niego miejsca obok całego rekordu. Wciąż wskazują go jego nazwa i manifest." }, + "format.TheLabelLineNeedsBAnd": { "hash": "sha256-2ad4fd6ac4b9", "other": "Wiersz z etykietą potrzebuje {{.Length}} B, a ten plik nie ma na niego miejsca obok całego wpisu. Wciąż wskazują go jego nazwa i manifest." }, + "format.TheLabelNeedsBAndThe": { "hash": "sha256-12f3f0aa811c", "other": "Etykieta potrzebuje {{.Length}} B, a plik ma {{.Bytes}} B, więc ten plik nie niesie etykiety. Wciąż wskazują go jego nazwa i manifest." }, + "format.TheLabelNeedsBAndThis": { "hash": "sha256-009ae9e3d815", "other": "Etykieta potrzebuje {{.Length}} B, a ten plik nie ma na nią miejsca obok całego bloku. Wciąż wskazują go jego nazwa i manifest." }, + "format.TheLabelNeedsBAndThis2": { "hash": "sha256-734d54b4fe08", "other": "Etykieta potrzebuje {{.Length}} B, a ten plik nie ma na nią miejsca obok całego kształtu. Wciąż wskazują go jego nazwa i manifest." }, + "format.ThePaddingEntryTheArchiveNeeds": { "hash": "sha256-307f2bf5a35d", "other": "wpis dopełnienia, którego archiwum potrzebuje przy tym rozmiarze, się nie mieści" }, + "format.ThePaddingNeededIsLargerThan": { "hash": "sha256-cf2e87fbb851", "other": "potrzebne dopełnienie jest większe, niż zmieści jeden fragment, a ten generator zapisuje tylko jeden" }, + "format.ThePaddingThatWouldMakeUp": { "hash": "sha256-303f9f27491e", "other": "dopełnienie, które wyrównałoby różnicę, nie może mieć jednego bajta, więc ten rozmiar leży w luce tuż nad najmniejszym archiwum" }, + "format.ThePictureIsPxWideAnd": { "hash": "sha256-5eaf84a1feb2", "other": "Obrazek ma {{.Width}} px szerokości, a etykieta potrzebuje więcej miejsca, więc ten plik nie niesie widocznej etykiety. Wciąż wskazują go jego nazwa i manifest." }, + "format.TheSizeIsBeingLeftTo": { "hash": "sha256-e8e5faeb4de3", "other": "rozmiar zależy od zawartości, a o ile się ona skompresuje, wiadomo dopiero po kompresji" }, + "format.TheseContentsComeOutOfThe": { "hash": "sha256-10c68148e9e7", "other": "ta zawartość wychodzi z kompresora większa, niż do niego weszła, więc po ściśnięciu archiwum nie może być aż tak małe" }, + "format.ThisDocumentIsExactlyBAnd": { "hash": "sha256-c09bf7afd65a", "other": "ten dokument ma dokładnie {{.Bare}} B, a najkrótszy komentarz, który mógłby go dopełnić, ma {{.MinComment}} B, więc o bajt więcej niż dokument to jedyny rozmiar pomiędzy, którego nie da się osiągnąć" }, + "format.ThisSizeNeedsAPaddingChunk": { "hash": "sha256-d2ded7f9dbf9", "other": "ten rozmiar wymaga fragmentu dopełnienia, a najmniejszy kosztuje {{.ChunkHeader}} B, więc nic między {{.Fixed}} a {{.Fixed2}} B nie jest osiągalne" }, + "format.TwoSettings": { "hash": "sha256-a61ae8d11eb8", "other": "{{.Of}} i {{.By}}" }, + "format.Unknown": { "hash": "sha256-11137b9e59cb", "other": "nieznany format {{.Format}}. Znane formaty: {{.Known}}" }, + "format.UnknownPropertyWhy": { "hash": "sha256-90dde87dda4b", "other": "format przyjmuje tylko ustawienia, które deklaruje, a nieznane przepadłoby po drodze" }, + "format.UseOneOf": { "hash": "sha256-a53588601259", "other": "użyj jednego z: {{.Known}}" }, + "format.UseTxtOrMdForA": { "hash": "sha256-9b7a68f87337", "other": "Użyj txt albo md dla pliku, który zaczyna się znacznikiem kolejności bajtów." }, + "format.UseYamlXmlTxtOrMd": { "hash": "sha256-4172b81dc9c5", "other": "Użyj yaml, xml, txt albo md dla pliku w innym kodowaniu." }, + "format.UseZipForAnArchiveWith": { "hash": "sha256-f8842d3575f8", "other": "Użyj zip dla archiwum z hasłem albo zostaw to archiwum otwarte." }, + "format.ValueRefused": { "hash": "sha256-3fba4a321577", "other": "{{.Format}}: {{.Setting}} nie może mieć wartości {{.Value}} - {{.Why}}" }, + "format.ValueRefusedBare": { "hash": "sha256-9b1b8007064f", "other": "{{.Subject}} nie może mieć wartości {{.Value}} - {{.Why}}" }, + "format.WriteADateThatExistsSuch": { "hash": "sha256-324ab7f056b1", "other": "Wpisz datę, która istnieje, na przykład 2024-02-29T13:45:00+02:00." }, + "format.WriteAValue": { "hash": "sha256-134396e545bd", "other": "wpisz wartość, którą przyjmuje, albo pomiń ten wiersz, a format sam ją wyliczy" }, + "format.WriteAValueSuchAs": { "hash": "sha256-b91dd569f6d3", "other": "wpisz wartość, którą przyjmuje, na przykład {{.Default}}, albo pomiń ten wiersz" }, + "format.WriteItTheWayChmodTakes": { "hash": "sha256-233e48fc5f2d", "other": "Zapisz to tak, jak przyjmuje chmod, czyli 644 albo 755." }, + "format.WriteItTheWayTheSetting": { "hash": "sha256-59a86a136c8b", "other": "Zapisz to tak, jak deklaruje ustawienie, czyli root albo użytkownik, albo pomiń ten wiersz, a pliki w środku nie będą miały właściciela." }, + "format.WriteTheDateInOneOf": { "hash": "sha256-95e8e2f5f105", "other": "Zapisz datę na jeden z tych trzech sposobów, bez ułamka sekundy." }, + "format.WriteTheValueInUTF8": { "hash": "sha256-28353b79a463", "other": "Zapisz wartość w UTF-8." }, + "format.ZipTheDirectoryCouldNotBe": { "hash": "sha256-7eb26b5ea426", "other": "zip: nie udało się nazwać katalogu {{.Name}}: {{.Err}}" }, + "format.ZipTheFileInsideCannotBe": { "hash": "sha256-8f5ef3b82380", "other": "zip: plik {{.Format}} w środku nie może powstać: {{.Err}}" }, + "html.HasToBeStructure": { "hash": "sha256-1a83745fa4b2", "other": "musi to być {{.Document}} albo {{.Fragment}}" }, + "html.MinimumFragment": { "hash": "sha256-e716d4f229d7", "other": "fragment zawiera całe bloki, a jeden z nich potrzebuje tyle" }, + "html.MinimumPage": { "hash": "sha256-3745440d090a", "other": "strona zawiera nagłówek, treść i całe bloki, a po jednym z nich potrzeba tyle" }, + "json.HasToBeStyle": { "hash": "sha256-c1343585d922", "other": "musi to być {{.Indented}}, {{.Minified}} albo {{.RecordPerLine}}" }, + "jxl.MinimumReason": { "hash": "sha256-8031f7cf3cc6", "other": "najmniejszy obrazek, jaki rysuje ten format, koduje się w najgorszym razie do {{.Coded}} B, kontener wokół niego kosztuje {{.Container}} B, a plik zawsze niesie wolny blok, który kosztuje {{.Box}} B nawet pusty" }, + "jxl.MinimumReasonOwn": { "hash": "sha256-08ce5283cdaa", "other": "obrazek {{.Width}}x{{.Height}} w jakości {{.Quality}} koduje się do {{.Coded}} B, kontener wokół niego kosztuje {{.Container}} B, a plik zawsze niesie wolny blok, który kosztuje {{.Box}} B nawet pusty" }, + "log.ClientAddress": { "hash": "sha256-df4616586839", "other": "adres klienta" }, + "log.ConflictDrop": { "hash": "sha256-9d99490328fe", "other": "{{.Why}}. Usuń jedno z dwóch albo zmień drugie" }, + "log.RequestMethod": { "hash": "sha256-eaa67a9c3b30", "other": "metoda żądania" }, + "log.ShapeCarriesNo": { "hash": "sha256-70d92750429d", "other": "kształt {{.Shape}} nie niesie pola: {{.What}}" }, + "log.ShapeNoResponseCode": { "hash": "sha256-33e6f87fa6e4", "other": "kształt {{.Shape}} nie niesie kodu odpowiedzi" }, + "log.ShapeNoSeverity": { "hash": "sha256-24f6ca4ac678", "other": "kształt {{.Shape}} nie niesie poziomu ważności" }, + "manifest.EntryHasThePathWhichLands": { "hash": "sha256-79cbc1e59f3f", "other": "wpis {{.I}} ma ścieżkę {{.Path}}, która wychodzi poza katalog opisany przez manifest - {{.Problem}}. To narzędzie nigdy nie czyta ani nie usuwa niczego poza tym katalogiem, więc nie wykona manifestu, który o to prosi. Użyj manifestu, który przebieg naprawdę zapisał, albo popraw ścieżkę na leżącą wewnątrz katalogu" }, + "manifest.Files": { "hash": "sha256-83589d643302", "one": "{{.Count}} plik", "few": "{{.Count}} pliki", "many": "{{.Count}} plików", "other": "{{.Count}} pliku" }, + "manifest.ItCarriesNoManifestVersionSo": { "hash": "sha256-00841a615039", "other": "nie ma manifest_version, więc to nie jest manifest zapisany przez to narzędzie" }, + "manifest.ItIsSchemaVersionAndThis": { "hash": "sha256-57828974a0e1", "other": "ma wersję schematu {{.ManifestVersion}}, a ta wersja czyta {{.Version}}. Użyj wersji tfg, która go zapisała" }, + "manifest.NoteMany": { "hash": "sha256-c4c8340dd2e0", "other": "{{.Files}}: {{.Note}} Wymienione: {{.Named}}. Pozostałe, których tu nie wymieniono: {{.Hidden}}." }, + "manifest.NoteOne": { "hash": "sha256-9d548db5c58a", "other": "{{.File}}: {{.Note}}" }, + "manifest.NoteSeveral": { "hash": "sha256-d0803401c505", "other": "{{.Files}}: {{.Note}} Wymienione: {{.Named}}." }, + "manifest.NoteText": { "hash": "sha256-e75b463d20f0", "other": "{{.Text}}" }, + "manifest.RunInstructionsIsAndItCan": { "hash": "sha256-1c92a916a2de", "other": "run.instructions to {{.Name}}, a może to być tylko nazwa pliku obok manifestu kończąca się na {{.InstructionsSuffix}}. To narzędzie usuwa ten plik razem z manifestem, więc nie wykona takiego, który wskazuje cokolwiek innego. Użyj manifestu, który przebieg naprawdę zapisał, albo usuń ten klucz" }, + "manifest.TooLarge": { "hash": "sha256-e12fe7ac5071", "other": "{{.Path}} ma {{.Bytes}} B, a limit to {{.Limit}} B. Manifest jest wczytywany do pamięci, żeby porównać go z katalogiem, więc nieograniczony byłby sposobem na jej wyczerpanie. Sprawdź, czy to manifest zapisany przez to narzędzie, albo podziel przebieg, który opisuje" }, + "manifest.Unreadable": { "hash": "sha256-bb90fafec850", "other": "{{.Path}} nie daje się odczytać jako manifest: {{.Detail}}" }, + "pdf.Carrying": { "hash": "sha256-83e9af0cb805", "other": " z {{.What}}" }, + "pdf.ChooseOneSize": { "hash": "sha256-61da6826cc02", "other": "wybierz jeden rozmiar, na przykład a4" }, + "pdf.ChooseOneWay": { "hash": "sha256-5f59b0be4c6d", "other": "wybierz {{.Portrait}} albo {{.Landscape}}" }, + "pdf.Document": { "hash": "sha256-17da9045f3c4", "other": "{{.Pages}}-stronicowy dokument {{.Size}}" }, + "pdf.DocumentLandscape": { "hash": "sha256-46b233374a70", "other": "{{.Pages}}-stronicowy dokument {{.Size}} poziomo" }, + "pdf.DocumentMixed": { "hash": "sha256-5f511b4e2ed1", "other": "{{.Pages}}-stronicowy dokument {{.Size}} pionowo i poziomo" }, + "pdf.HintAll": { "hash": "sha256-447a5ca57f7b", "other": ", poproś o mniej stron, ustawiając pole Strony na 1, skróć właściwości dokumentu albo usuń etykietę" }, + "pdf.HintLabel": { "hash": "sha256-142534ffd01d", "other": ", poproś o mniej stron, ustawiając pole Strony na 1, albo usuń etykietę" }, + "pdf.HintPages": { "hash": "sha256-6f80cbb77708", "other": " albo poproś o mniej stron, ustawiając pole Strony na 1" }, + "pdf.HintProperties": { "hash": "sha256-4ac9a00fbf06", "other": ", poproś o mniej stron, ustawiając pole Strony na 1, albo skróć właściwości dokumentu" }, + "pdf.MixedSize": { "hash": "sha256-20964137735f", "other": "w mieszanych rozmiarach" }, + "pdf.Size": { "hash": "sha256-a7df568dcb25", "other": "{{.Size}}" }, + "pdf.TheLabel": { "hash": "sha256-a51c21f8bbe2", "other": "samoopisującą etykietą" }, + "pdf.TheProperties": { "hash": "sha256-a566d1539507", "other": "ustawionymi właściwościami dokumentu" }, + "pptx.Slides": { "hash": "sha256-05da85d44c19", "one": "{{.Count}} slajdem", "few": "{{.Count}} slajdami", "many": "{{.Count}} slajdami", "other": "{{.Count}} slajdu" }, + "preset.ADistanceFromTheLimitHas": { "hash": "sha256-3fad7b38f5ae", "other": "odległość od limitu musi być większa od zera" }, + "preset.ARecipeCanBuildOnlyOn": { "hash": "sha256-b96ec4594ce9", "other": "przepis może budować tylko na presecie, który zna narzędzie, na którym działa" }, + "preset.AlreadyMeansEveryFormatSoIt": { "hash": "sha256-af17cbc9bca4", "other": "{{.EveryFormat}} już oznacza każdy format, więc nie może stać obok nazwanego. Wpisz je samo albo wymień tylko formaty, których chcesz" }, + "preset.AnExtensionIsWrittenWithoutIts": { "hash": "sha256-098698484a13", "other": "rozszerzenie zapisuje się bez kropki, więc {{.TrimPrefix}}, a nie {{.Item}}" }, + "preset.BulkNotWhole": { "hash": "sha256-8dd30727905b", "other": "{{.Param}}: {{.Value}} nie jest całkowitą liczbą plików" }, + "preset.BulkNought": { "hash": "sha256-9f12af8e80c4", "other": "masowe przesyłanie ma wartość zero, więc ten zestaw nie zawiera masowego uploadu." }, + "preset.CannotBuild": { "hash": "sha256-6ae81854228f", "other": "preset {{.Preset}} nie może zbudować tego zestawu" }, + "preset.Combinations": { "hash": "sha256-89e8d79ad737", "one": "{{.Count}} kombinację", "few": "{{.Count}} kombinacje", "many": "{{.Count}} kombinacji", "other": "{{.Count}} kombinacji" }, + "preset.Defaulted": { "hash": "sha256-e75b463d20f0", "other": "{{.Text}}" }, + "preset.EncodingLeftOut": { "hash": "sha256-bdf71c709021", "other": "ta wersja odrzuca {{.Refused}} zestawu kodowań, więc zawiera on {{.Holds}} zamiast {{.Described}}. Pominięte: {{.Left}}." }, + "preset.EncodingNoGrid": { "hash": "sha256-587807703cd8", "other": "żaden format w tej wersji nie niesie naraz kodowania i końca linii, więc dwie połowy tego zestawu to osobne pliki, a nie jedna siatka. Kodowania: {{.Encodings}}. Końce linii: {{.LineEndings}}." }, + "preset.ExtendsNamesPresetWhichThisBuild": { "hash": "sha256-be91635f052f", "other": "extends wskazuje preset:{{.ID}}, którego ta wersja nie ma" }, + "preset.FarOverOff": { "hash": "sha256-767f6cd55319", "other": "plik daleko ponad limitem jest wyłączony, więc nic w tym zestawie nie jest daleko ponad limitem. Największy plik jest o bajt ponad nim." }, + "preset.Files": { "hash": "sha256-83589d643302", "one": "{{.Count}} plik", "few": "{{.Count}} pliki", "many": "{{.Count}} plików", "other": "{{.Count}} pliku" }, + "preset.ImpossibleWhy": { "hash": "sha256-1bf156d5da79", "other": "preset {{.Preset}} nie może zbudować tego zestawu - {{.Why}}." }, + "preset.ImpossibleWhyFix": { "hash": "sha256-568852ea20d3", "other": "preset {{.Preset}} nie może zbudować tego zestawu - {{.Why}}. {{.Fix}}" }, + "preset.IsAllowedAndIsDeniedAnd": { "hash": "sha256-ea49ead8ea31", "other": "{{.ID}} jest dozwolony, a {{.Written}} odrzucany, i oba oznaczają plik kończący się na {{.Extension}} - więc zestaw zawierałby jeden do przyjęcia i jeden do odrzucenia" }, + "preset.ItHasToBeANumber": { "hash": "sha256-d6de05796906", "other": "musi to być krotność limitu zapisana z x - 2x albo 10x - albo wyłączone" }, + "preset.ItHoldsAndADistanceIs": { "hash": "sha256-e721e19decc5", "other": "zawiera {{.Bad}}, a odległość zapisuje się cyframi, literami i kropką - na przykład 1kb, 512 albo 1.5mb. Jej tekst staje się nazwą pliku" }, + "preset.ItHoldsAndAnExtensionIs": { "hash": "sha256-15909d4361fa", "other": "zawiera {{.Bad}}, a rozszerzenie zapisuje się literami i cyframi - na przykład exe, sh albo svg. Jego tekst staje się końcem nazwy pliku" }, + "preset.ItIsCharactersLongAndAn": { "hash": "sha256-78d81ba1a16a", "other": "ma {{.Length}} znaków, a rozszerzenie ma tu najwyżej {{.LongestExtension}}" }, + "preset.ItIsTheSameDistanceAs": { "hash": "sha256-4ad253058b9f", "other": "to ta sama odległość co {{.First}} i zestaw zawierałby ten krok dwa razy. Każda odległość musi być inna, bo każda wyznacza po jednym pliku z obu stron limitu" }, + "preset.ItIsTheSameExtensionAs": { "hash": "sha256-7fa2a1b46c69", "other": "to to samo rozszerzenie co {{.First}} i zestaw zawierałby ten plik dwa razy. Każde odrzucane rozszerzenie występuje raz, bo każde odpowiada jednej regule Twojego formularza" }, + "preset.ItIsTheSameFormatAs": { "hash": "sha256-4504e5d7a37e", "other": "to ten sam format co {{.First}} i zestaw zawierałby ten plik dwa razy. Każdy format występuje raz, bo każdy odpowiada jednej ścieżce przez Twój czytnik" }, + "preset.ItIsTheSameTypeAs": { "hash": "sha256-e66f4431f6ca", "other": "to ten sam typ co {{.First}} i zestaw zawierałby ten plik dwa razy. Każdy dozwolony typ występuje raz, bo każdy odpowiada jednej ścieżce przez Twój formularz" }, + "preset.ItsExtensionIsTooLongFor": { "hash": "sha256-f5d7c69e3a4a", "other": "jego rozszerzenie {{.Ext}} jest za długie na nazwę o {{.Bytes}} bajtach, której dotyczy plik {{.ID}}. Wybierz format o krótszym rozszerzeniu" }, + "preset.LeftOutAs": { "hash": "sha256-f3020fe59d49", "other": "{{.Format}} jako {{.As}} ({{.Why}})" }, + "preset.LimitUnusable": { "hash": "sha256-5cf12375b81f", "other": "{{.Param}}: zawiera {{.Bad}}, a limit zapisuje się cyframi, literami i kropką - na przykład 10mb, 512 albo 1.5gb. Jego tekst staje się częścią każdej nazwy pliku" }, + "preset.NoDistancesWereGivenSoThere": { "hash": "sha256-5493a98292c6", "other": "nie podano żadnych odległości, więc z obu stron limitu nic nie ma" }, + "preset.NoEmptyForm": { "hash": "sha256-75baf9544d6e", "other": "żaden format w tym zestawie nie ma poprawnej pustej postaci, więc zestaw nie zawiera pustych plików - tylko najmniejszy poprawny z każdego formatu. Formaty, które w tej wersji schodzą do zera bajtów: {{.Formats}}." }, + "preset.NoExtensionsWereDeniedSoThere": { "hash": "sha256-732512c09653", "other": "nie odrzucono żadnych rozszerzeń, więc formularz nie ma czego odrzucać" }, + "preset.NoFormatInThisBuildCarries": { "hash": "sha256-4a6750d6f63d", "other": "żaden format w tej wersji nie niesie kodowania ani końca linii, więc nie ma zestawu do zbudowania" }, + "preset.NoFormatsWereGivenSoThere": { "hash": "sha256-4cdc44101684", "other": "nie podano żadnych formatów, więc nie ma z czego zbudować zestawu" }, + "preset.NoSuchParameter": { "hash": "sha256-929a6264f65a", "other": "preset {{.Preset}} nie ma parametru o nazwie {{.Name}}. Przyjmuje: {{.Known}}" }, + "preset.NoTypesWereAllowedSoThe": { "hash": "sha256-bb028b47b8fc", "other": "nie dopuszczono żadnych typów, więc zestaw nie ma kontroli pozytywnej ani pliku do błędnego nazwania" }, + "preset.OneAllowed": { "hash": "sha256-bbfaa4682425", "other": "dozwolony jest tylko jeden typ, więc zestaw nie zawiera pliku nazwanego jak jeden dozwolony typ, a wypełnionego innym - do tego potrzeba dwóch." }, + "preset.ParamCause": { "hash": "sha256-db5c6acdbb8d", "other": "{{.Param}}: {{.Cause}}" }, + "preset.RaiseTheSettingAboveBNarrow": { "hash": "sha256-1e2c92f16629", "other": "Podnieś pole {{.Setting}} powyżej {{.Floor}} B, zawęź rozrzut albo wybierz format o mniejszym minimum. W polu {{.Setting}} podano {{.Limit}} B." }, + "preset.RaiseTheSettingToBOr": { "hash": "sha256-588c9bcb6cc3", "other": "Podnieś pole {{.Setting}} do {{.Need}} B albo więcej albo usuń {{.ID}} z dozwolonych typów. W polu {{.Setting}} podano {{.Limit}} B." }, + "preset.Refused": { "hash": "sha256-e75b463d20f0", "other": "{{.Text}}" }, + "preset.RemoveTheLine": { "hash": "sha256-14c26fa261f4", "other": "usuń ten wiersz" }, + "preset.RunPresetShow": { "hash": "sha256-fe7d5528fb3b", "other": "uruchom \"tfg preset show {{.Preset}}\", żeby zobaczyć, co przyjmuje" }, + "preset.RunTfgFormatsForTheList": { "hash": "sha256-923b9bd13d26", "other": "uruchom \"tfg formats\", żeby zobaczyć listę" }, + "preset.RunTfgFormatsToSeeWhat": { "hash": "sha256-f047d0599c10", "other": "Uruchom \"tfg formats\", żeby zobaczyć, co ma ta wersja." }, + "preset.RunTfgPresetListForThe": { "hash": "sha256-25a1a032c139", "other": "uruchom \"tfg preset list\", żeby zobaczyć identyfikatory tej wersji" }, + "preset.SetTheSettingToBOr": { "hash": "sha256-5a47f6cb7b98", "other": "Ustaw pole {{.Setting}} na {{.Size}} B albo {{.Minimum}} B." }, + "preset.SetTheSettingToBOr2": { "hash": "sha256-c3ba9e8a8eb4", "other": "Ustaw pole {{.Setting}} na {{.Floor}} B albo więcej." }, + "preset.StandIns": { "hash": "sha256-a86a61e3c5aa", "one": "ta wersja nie ma formatu o nazwie {{.Formats}}, więc {{.Names}} zawiera zwykły tekst pod tą nazwą. To sprawdza formularz, który czyta koniec nazwy, a nie zawartość.", "few": "ta wersja nie ma formatu o nazwie {{.Formats}}, więc {{.Names}} zawierają zwykły tekst pod tymi nazwami. To sprawdza formularz, który czyta koniec nazwy, a nie zawartość.", "many": "ta wersja nie ma formatu o nazwie {{.Formats}}, więc {{.Names}} zawierają zwykły tekst pod tymi nazwami. To sprawdza formularz, który czyta koniec nazwy, a nie zawartość.", "other": "ta wersja nie ma formatu o nazwie {{.Formats}}, więc {{.Names}} zawierają zwykły tekst pod tymi nazwami. To sprawdza formularz, który czyta koniec nazwy, a nie zawartość." }, + "preset.StepBelowFloor": { "hash": "sha256-170136bfee7f", "other": "{{.Step}} miałby {{.Size}} B, a najmniejszy plik {{.Format}}, jaki tworzy ta wersja, ma {{.Floor}} B" }, + "preset.StepBelowZero": { "hash": "sha256-4a4ed15f64f3", "other": "{{.Step}} miałby {{.Size}} B, a plik nie może być mniejszy niż nic" }, + "preset.TakeOutOfTheListOr": { "hash": "sha256-3a45bd78cd55", "other": "Usuń {{.ID}} z listy {{.AllowParam}} albo {{.Written}} z listy {{.DenyParam}}." }, + "preset.TakesNoParameters": { "hash": "sha256-10127e9faa16", "other": "preset {{.Preset}} nie przyjmuje parametrów, więc {{.Name}} nie jest jednym z nich" }, + "preset.ThePresetGivesEveryFileIt": { "hash": "sha256-0bd3908db204", "other": "preset daje każdemu budowanemu plikowi ten format, więc musi to być taki, który ta wersja umie zapisać" }, + "preset.ThePresetRefusedWhatTheRecipe": { "hash": "sha256-46f82ba266de", "other": "preset odrzucił to, co dał mu przepis" }, + "preset.ThePresetTakes": { "hash": "sha256-078daea8c1a6", "other": "preset przyjmuje: {{.Join}}" }, + "preset.TheSetHoldsFilesInAnd": { "hash": "sha256-770eaf5a913a", "other": "zestaw zawiera pliki w {{.Name}}, a {{.Reason}}" }, + "preset.ThisBuildHas": { "hash": "sha256-3787cbba4f98", "other": "ta wersja ma: {{.Join}}" }, + "preset.ThisBuildHasNoFormatCalled": { "hash": "sha256-758e0bae8496", "other": "ta wersja nie ma formatu o takiej nazwie. Ma: {{.Join}}" }, + "preset.Unknown": { "hash": "sha256-535352e45ee2", "other": "nieznany preset {{.Preset}}. Ta wersja ma: {{.Known}}" }, + "preset.UnknownNone": { "hash": "sha256-1ad71c52426d", "other": "nieznany preset {{.Preset}}, a ta wersja nie rejestruje żadnego" }, + "preset.WithCannotBe": { "hash": "sha256-0e6c86d85911", "other": "with.{{.Key}} nie może mieć wartości {{.Value}}" }, + "preset.WithFillsTheParametersThePreset": { "hash": "sha256-647f0f97fb96", "other": "with wypełnia parametry, które preset deklaruje, a to nie jest żaden z nich" }, + "preset.WithFormatNamesWhichThisBuild": { "hash": "sha256-d497cb42a8d9", "other": "with.format wskazuje {{.ID}}, którego ta wersja nie ma" }, + "preset.WithNamesWhichThePresetDoes": { "hash": "sha256-751e28920004", "other": "with wskazuje {{.Name}}, którego preset {{.Preset}} nie przyjmuje" }, + "preset.WouldBeBAndTheSmallest": { "hash": "sha256-1879f6cbbc78", "other": "{{.ID}} miałby {{.Bytes}} B, a najmniejszy plik {{.ID2}}, jaki tworzy ta wersja, ma {{.Floor}} B" }, + "preset.WrittenAsCannotBeSmallerThan": { "hash": "sha256-7691ec217cc8", "other": "{{.ID}} zapisany jako {{.Label}} nie może mieć mniej niż {{.Floor}} B, a każdy plik tego zestawu ma ten sam rozmiar" }, + "recipe.ABoundaryAlreadyDecidesTheSizes": { "hash": "sha256-b6aeb28edc93", "other": "limit testowany już wyznacza rozmiary, więc pole {{.Setting}} obok niego znaczy naraz dwie różne rzeczy" }, + "recipe.ABoundarySetIsExactlyThree": { "hash": "sha256-562f950f5d71", "other": "zestaw graniczny to dokładnie trzy pliki, jeden poniżej limitu, jeden na limicie i jeden powyżej" }, + "recipe.AContainerCannotHoldFewerThan": { "hash": "sha256-e6eeafdabc57", "other": "kontener nie może zawierać mniej niż zero plików" }, + "recipe.AContainerHoldsRealFilesSo": { "hash": "sha256-134f67142edb", "other": "kontener zawiera prawdziwe pliki, więc każda grupa mówi, w jakim formacie są jej pliki" }, + "recipe.AContainsEntryDescribesFilesWith": { "hash": "sha256-0c0612b3ccd3", "other": "wpis contains opisuje pliki formatem, liczbą i rozmiarem, a wszystko inne przepadłoby po drodze" }, + "recipe.ADamageIsWrittenAsIts": { "hash": "sha256-10250751f233", "other": "uszkodzenie zapisuje się jego nazwą albo jako mapę z type i ustawieniami, które przyjmuje" }, + "recipe.ADamageSettingTakesOneValue": { "hash": "sha256-c3de566b6c59", "other": "ustawienie uszkodzenia przyjmuje jedną wartość, tak jak właściwość formatu" }, + "recipe.ADamageWrittenAsAMapping": { "hash": "sha256-8f9dc4da5556", "other": "uszkodzenie zapisane jako mapa nazywa się przez type" }, + "recipe.AFormatIsTheIdOf": { "hash": "sha256-c6b847dd03df", "other": "format to identyfikator formatu, który ta wersja obsługuje" }, + "recipe.AFormatPropertyIsASingle": { "hash": "sha256-9a608b215b35", "other": "właściwość formatu to jedna wartość" }, + "recipe.APresetContributesTargetsAndNothing": { "hash": "sha256-91787ca07023", "other": "preset wnosi partie i nic więcej, żeby ziarno, wartości domyślne i sekcja wyjścia zawsze należały do samego pliku" }, + "recipe.APresetSTargetsWereSupplied": { "hash": "sha256-c52ddca5c58e", "other": "partie presetu zostały podane czytnikowi, a przepis nie wskazuje żadnego" }, + "recipe.ARangeIsTwoSizesWith": { "hash": "sha256-5a18946381db", "other": "zakres to dwa rozmiary z łącznikiem między nimi" }, + "recipe.AReasonIsOneValueFrom": { "hash": "sha256-0111a952089c", "other": "testowana reguła to jedna wartość z zamkniętej listy" }, + "recipe.ARecipeCanBuildOnA": { "hash": "sha256-0f3adf9d29dc", "other": "przepis może budować na presecie i na niczym innym w tej wersji - nie na innym pliku" }, + "recipe.ARecipeIsATextDocument": { "hash": "sha256-8361f48aa1aa", "other": "przepis to dokument tekstowy, a znak sterujący albo go psuje, albo przepada bez słowa" }, + "recipe.ARecipeIsOneDocumentAnd": { "hash": "sha256-01123bae35c4", "other": "przepis to jeden dokument, a wszystko po pierwszym separatorze zostałoby pominięte bez słowa" }, + "recipe.ARecipeThatBuildsOnA": { "hash": "sha256-e559b6752421", "other": "przepis budujący na presecie czyta czytnik, który najpierw rozwija preset, a ten tego nie robi" }, + "recipe.ARecipeWithoutTargetsHasNothing": { "hash": "sha256-f69e05f3bab1", "other": "przepis bez partii nie ma czego wytworzyć" }, + "recipe.ATargetHasToSayWhat": { "hash": "sha256-21889853a052", "other": "partia musi powiedzieć, jaki rodzaj pliku wytwarza" }, + "recipe.ATargetThatDamagesNothingProduces": { "hash": "sha256-e3ae55c0f4ef", "other": "partia, która niczego nie uszkadza, daje te same pliki co partia, która o uszkodzeniu nie wspomina" }, + "recipe.ATargetThatProducesNothingIs": { "hash": "sha256-3a4f444ceac5", "other": "partia, która nic nie wytwarza, to prawie zawsze pomyłka, a nie zamiar" }, + "recipe.ATypoAcceptedInSilenceBecomes": { "hash": "sha256-0c20fa7ae876", "other": "literówka przyjęta po cichu staje się oczekiwaniem, którego żaden test nigdy nie sprawdzi" }, + "recipe.AddAtLeastOneEntryUnder": { "hash": "sha256-577a79df5625", "other": "dodaj co najmniej jeden wpis pod targets:" }, + "recipe.AddExtendsPresetIdAboveIt": { "hash": "sha256-c0747ed47cd7", "other": "dodaj nad nim extends: preset: albo usuń with" }, + "recipe.AddFormatPdf": { "hash": "sha256-514ee52e95db", "other": "dodaj format: pdf" }, + "recipe.AddFormatTxtOrRunTfg": { "hash": "sha256-1fc7c58ded33", "other": "dodaj format: txt albo uruchom \"tfg formats\", żeby zobaczyć całą listę" }, + "recipe.AddOutcomeAcceptRejectSanitizeOr": { "hash": "sha256-906f83c6e332", "other": "dodaj outcome: accept, reject, sanitize albo unspecified" }, + "recipe.AddSize2mb": { "hash": "sha256-aaa48a1f0dc8", "other": "dodaj size: 2mb" }, + "recipe.AddSize2mbSizeRange1kb": { "hash": "sha256-75d03ddd7410", "other": "dodaj size: 2mb, size-range: 1kb-8kb, boundary, contains albo zwykłą liczbę bajtów" }, + "recipe.AddTypeWithTheNameOf": { "hash": "sha256-a4232a7b5f56", "other": "dodaj type z nazwą uszkodzenia" }, + "recipe.AddVersionAsTheFirstLine": { "hash": "sha256-ffcea33474a0", "other": "dodaj version: {{.SchemaVersion}} jako pierwszy wiersz" }, + "recipe.AllowNondeterministicTrueHasNothingTo": { "hash": "sha256-2ef05077fa28", "other": "allow_nondeterministic: true nie ma w tej wersji na co pozwalać" }, + "recipe.AnExpectationCarriesAnOutcomeAnd": { "hash": "sha256-c3cdf61efb0d", "other": "oczekiwanie niesie wynik i testowaną regułę, a wszystko inne przepadłoby bez słowa" }, + "recipe.AnExpectationIsEitherOneWord": { "hash": "sha256-5ba3e0655b36", "other": "oczekiwanie to albo jedno słowo, albo blok z outcome" }, + "recipe.AnExpectationSaysWhatTheSystem": { "hash": "sha256-4f1054b7ef90", "other": "oczekiwanie mówi, co system pod testem powinien zrobić z plikiem" }, + "recipe.AnOutcomeIsOneOfFour": { "hash": "sha256-936413f8f699", "other": "wynik to jedno z czterech słów" }, + "recipe.AskForAtLeastOneOr": { "hash": "sha256-6af43d0b9605", "other": "poproś o co najmniej jeden albo usuń partię" }, + "recipe.AsksForFiles": { "hash": "sha256-41167ad22021", "other": "{{.At}} prosi o {{.Value}} plików" }, + "recipe.BothEndsOfARangeAre": { "hash": "sha256-a9a3149f123a", "other": "oba końce zakresu to rozmiary, a jednostki liczą się po 1024" }, + "recipe.CannotBe": { "hash": "sha256-f03796441e95", "other": "{{.Where}}: {{.Key}} nie może mieć wartości {{.Value}}" }, + "recipe.DeclaresAReasonThatIsNot": { "hash": "sha256-5b2a54855ceb", "other": "{{.Where}} podaje testowaną regułę, która nie jest słowem" }, + "recipe.DeclaresAnExpectationThisBuildCannot": { "hash": "sha256-eb121a54cc49", "other": "{{.Where}} podaje oczekiwanie, którego ta wersja nie umie odczytać" }, + "recipe.DeclaresAnExpectationWithNoOutcome": { "hash": "sha256-364dd9dae02a", "other": "{{.Where}} podaje oczekiwanie bez wyniku" }, + "recipe.DeclaresAnOutcomeThatIsNot": { "hash": "sha256-1bdfdc699e0a", "other": "{{.Where}} podaje wynik, który nie jest słowem" }, + "recipe.DeclaresInsideItsExpectation": { "hash": "sha256-83c4a173ba32", "other": "{{.Where}} podaje {{.K}} wewnątrz swojego oczekiwania" }, + "recipe.DoesNotSayWhichDamageIt": { "hash": "sha256-aaa7993ea8ad", "other": "{{.At}} nie mówi, które to uszkodzenie" }, + "recipe.Entry": { "hash": "sha256-3d172c936981", "other": "{{.Where}}: wpis {{.Number}} listy {{.List}}" }, + "recipe.EveryFormatHereRepeatsToThe": { "hash": "sha256-a1c5006fdb6d", "other": "każdy format tutaj powtarza się co do bajtu, więc żadna zgoda nie jest potrzebna" }, + "recipe.EveryRecipeDeclaresTheSchemaVersion": { "hash": "sha256-48bf6f9999fc", "other": "każdy przepis podaje wersję schematu, według której go napisano" }, + "recipe.EveryTargetDeclaresItsSettingWhich": { "hash": "sha256-df7af7d5d62b", "other": "każda partia podaje pole {{.Setting}} i to dzięki temu przebieg próbny podaje dokładne liczby, zanim cokolwiek trafi na dysk" }, + "recipe.ExpectsWhichIsNotAKnown": { "hash": "sha256-056d10d1cdc7", "other": "{{.Where}} oczekuje wyniku {{.Outcome}}, a takiego wyniku nie ma" }, + "recipe.ExtendsDoesNotNameAPreset": { "hash": "sha256-96b167b16bf9", "other": "extends {{.Value}} nie wskazuje presetu" }, + "recipe.GeneratedContentIsEnglishOnlySo": { "hash": "sha256-730196b0eddb", "other": "generowana treść jest na razie tylko po angielsku" }, + "recipe.GiveItADifferentSettingOr": { "hash": "sha256-8dd1a4a5c174", "other": "zmień pole {{.Setting}} albo pomiń partię presetu, wyjmując preset do pliku i edytując przepis" }, + "recipe.GiveItOneValueForExample": { "hash": "sha256-f43e69de623b", "other": "daj mu jedną wartość, na przykład bytes: 16" }, + "recipe.GiveItOneValueForExample2": { "hash": "sha256-1865e1d0d4fc", "other": "daj jej jedną wartość, na przykład pages: 3" }, + "recipe.GiveItSettingForExampleId": { "hash": "sha256-9066c56454f2", "other": "uzupełnij pole {{.Setting}}, na przykład id: invoices" }, + "recipe.GiveOneOfThemADifferent": { "hash": "sha256-f06830c13eb6", "other": "zmień pole {{.Setting}} w jednej z nich" }, + "recipe.GivesTheReasonWhichIsNot": { "hash": "sha256-8f677f25d2b4", "other": "{{.Where}} podaje testowaną regułę {{.Value}}, której nie ma na liście" }, + "recipe.HasAFormatThatIsNot": { "hash": "sha256-e06c14c235fd", "other": "{{.At}} ma format, który nie jest nazwą" }, + "recipe.HasANegativeSetting": { "hash": "sha256-608858bc635d", "other": "{{.At}} ma ujemne pole {{.Setting}}" }, + "recipe.HasNoFormat": { "hash": "sha256-54e9815fc753", "other": "{{.At}} nie ma formatu" }, + "recipe.HasNoSetting": { "hash": "sha256-bbb5a764f659", "other": "{{.At}} nie ma pola {{.Setting}}" }, + "recipe.HasSettingOfB": { "hash": "sha256-9f292ccc35c0", "other": "{{.Where}} ma w polu {{.Setting}} wartość {{.Limit}} B" }, + "recipe.HasSettingOfWhichIsNot": { "hash": "sha256-121c812b222f", "other": "{{.Where}} ma w polu {{.Setting}} wartość {{.Text}}, która nie jest liczbą całkowitą" }, + "recipe.HasSettingThatIsNeitherText": { "hash": "sha256-ffa08f43b328", "other": "{{.At}} ma w polu {{.Setting}} coś, co nie jest ani tekstem, ani liczbą" }, + "recipe.HasSettingThatIsNotA": { "hash": "sha256-2a5268bfb9cb", "other": "{{.At}} ma w polu {{.Setting}} coś, co nie jest liczbą całkowitą" }, + "recipe.HasSettingThatIsNotText": { "hash": "sha256-2b479eb7c452", "other": "{{.Where}} ma w polu {{.Setting}} coś, co nie jest tekstem" }, + "recipe.HasTheKey": { "hash": "sha256-1cdc9dd0b5e2", "other": "{{.At}} ma klucz {{.Key}}" }, + "recipe.HasTheSettingOfATarget": { "hash": "sha256-ff414a2e347a", "other": "{{.Where}} ma w polu {{.Setting}} to samo co partia, którą preset już buduje" }, + "recipe.HoldsTheCharacterWhichCannotBe": { "hash": "sha256-e6aafb7f9e39", "other": "{{.At}} zawiera znak {{.Bad}}, którego nie da się zapisać w przepisie" }, + "recipe.IsAListOrABlock": { "hash": "sha256-d3df7f29aa77", "other": "{{.At}}: {{.Key}} jest listą albo blokiem" }, + "recipe.IsNeitherANameNorA": { "hash": "sha256-ff6baa7396bc", "other": "{{.At}} nie jest ani nazwą, ani zestawem ustawień" }, + "recipe.IsWhichIsNeitherTrueNor": { "hash": "sha256-731e62d7639d", "other": "{{.Subject}} to {{.Value}}, czyli ani true, ani false" }, + "recipe.IsWrittenAsAListOr": { "hash": "sha256-311df656fc11", "other": "{{.Subject}} jest zapisane jako lista albo blok" }, + "recipe.KeepBoundaryForTheThreeSizes": { "hash": "sha256-4e72c705dde7", "other": "zostaw boundary dla trzech rozmiarów wokół limitu albo pole {{.Setting}} dla jednego dokładnego rozmiaru ({{.Setting}})" }, + "recipe.KeepSettingForIdenticalFilesOr": { "hash": "sha256-46527448d710", "other": "zostaw pole {{.Setting}} dla identycznych plików albo size-range, żeby każdy dostał inne pole {{.Setting}}" }, + "recipe.KeepSettingToTestALimit": { "hash": "sha256-789756ad13c4", "other": "zostaw pole {{.Setting}}, żeby testować limit, albo size-range dla plików różnej wielkości" }, + "recipe.LeaveTheFileAsItIs": { "hash": "sha256-6b6af820f0ca", "other": "zostaw plik, jaki jest, i sprawdź, czy nie ma w nim kotwicy albo aliasu użytego tam, gdzie należy się klucz" }, + "recipe.LeaveTheFileAsItIs2": { "hash": "sha256-1a69ba8162a8", "other": "zostaw plik, jaki jest, i uprość to, co nie ma jednej zapisanej postaci - zwykle chodzi o znacznik kolejności bajtów, który nie stoi na samym początku" }, + "recipe.LocaleIsNotAvailableInThis": { "hash": "sha256-74efd135bbd0", "other": "locale {{.Locale}} nie jest dostępne w tej wersji" }, + "recipe.NameADamageOrRemoveThe": { "hash": "sha256-3ab62a82689b", "other": "podaj uszkodzenie albo usuń ten wiersz" }, + "recipe.NamesADamageThatIsNot": { "hash": "sha256-72cc91016a21", "other": "{{.At}} podaje uszkodzenie, które nie jest słowem" }, + "recipe.NotYAML": { "hash": "sha256-4f98e4e1591a", "other": "tego pliku nie da się odczytać jako YAML. Poszukaj znacznika tagu albo kotwicy, takiego jak ! albo &, po którym nic nie ma" }, + "recipe.NotYet": { "hash": "sha256-40d28c193ca6", "other": "{{.Key}} nie jest jeszcze w tej wersji" }, + "recipe.NotYetIn": { "hash": "sha256-0092c2fe4e03", "other": "{{.Where}}: {{.Key}} nie jest jeszcze w tej wersji" }, + "recipe.OneNamesAnExactSettingAnd": { "hash": "sha256-47db418b9d1b", "other": "jedno podaje dokładne pole {{.Setting}}, a drugie je losuje, więc razem mówią dwie różne rzeczy" }, + "recipe.PresetCarries": { "hash": "sha256-b5289d7e61ab", "other": "przepis presetu niesie {{.Keys}}, a przepis budujący na nim nie może tego przejąć" }, + "recipe.PresetTarget": { "hash": "sha256-a13be873f25d", "other": "partia presetu {{.Target}}" }, + "recipe.PresetTargetNumber": { "hash": "sha256-6fdb0658c67b", "other": "partia presetu nr {{.Number}}" }, + "recipe.Problem": { "hash": "sha256-f2a3b0708fd9", "other": "{{.What}} - {{.Why}}.\n {{.Fix}}." }, + "recipe.PropertyIsAListOrA": { "hash": "sha256-81837b3cdae2", "other": "{{.Where}}: właściwość {{.K}} jest listą albo blokiem" }, + "recipe.ReadTheFileWithParseOr": { "hash": "sha256-ebbbbc57d3a1", "other": "czytaj plik przez Parse albo dodaj extends: preset:" }, + "recipe.ReadTheFileWithTfgGenerate": { "hash": "sha256-0855d32f4635", "other": "czytaj plik przez \"tfg generate\" albo \"tfg validate\", które to robią" }, + "recipe.Refused": { "hash": "sha256-338516f4d59b", "one": "{{.Name}} ma {{.Count}} problem i nic nie zostało zapisane:", "few": "{{.Name}} ma {{.Count}} problemy i nic nie zostało zapisane:", "many": "{{.Name}} ma {{.Count}} problemów i nic nie zostało zapisane:", "other": "{{.Name}} ma {{.Count}} problemu i nic nie zostało zapisane:" }, + "recipe.RemoveItItIsUsuallyA": { "hash": "sha256-9d3d6a72b52c", "other": "usuń go - to zwykle zabłąkany tabulator albo koniec wiersza z wklejania" }, + "recipe.RemoveSettingOrUseSizeWith": { "hash": "sha256-2834c992f758", "other": "usuń pole {{.Setting}} albo użyj size z polem {{.Setting}}, żeby prosić o identyczne pliki" }, + "recipe.RemoveTheKeyOrMoveIt": { "hash": "sha256-549770eb0c2f", "other": "usuń klucz albo przenieś go do samej partii" }, + "recipe.RemoveTheLine": { "hash": "sha256-14c26fa261f4", "other": "usuń ten wiersz" }, + "recipe.RemoveTheLineContentIsGenerated": { "hash": "sha256-0e80dd6c948d", "other": "usuń ten wiersz - treść powstaje z ziarna" }, + "recipe.RemoveTheLineItWillBe": { "hash": "sha256-d58a87dfe991", "other": "usuń ten wiersz - będzie potrzebny formatom, które używają kodera systemu" }, + "recipe.RemoveTheLineOrPutThe": { "hash": "sha256-339469eb5320", "other": "usuń ten wiersz albo wpisz wyjaśnienie w reason" }, + "recipe.RemoveTheLineTheManifestRecords": { "hash": "sha256-348821455596", "other": "usuń ten wiersz - manifest zapisuje wersję, która działała" }, + "recipe.RemoveTheSectionTheExpectedField": { "hash": "sha256-d2455a9faadf", "other": "usuń tę sekcję - pole expected w partii już działa" }, + "recipe.RemoveTheSeparatorsOrSplitThe": { "hash": "sha256-15c435179b86", "other": "usuń separatory --- albo podziel plik na jeden przepis na plik" }, + "recipe.SaysDamageAndNamesNone": { "hash": "sha256-5f72b5dd57ef", "other": "{{.Where}} mówi damage i nie podaje żadnego" }, + "recipe.SeedIsNotAWholeNumber": { "hash": "sha256-789fd24618af", "other": "ziarno {{.Text}} nie jest liczbą całkowitą" }, + "recipe.SettingAnchorsTheSeedOfA": { "hash": "sha256-77b6c3d3c3ba", "other": "pole {{.Setting}} wyznacza ziarno partii, więc zmiana jednej partii nigdy nie przesuwa bajtów innej" }, + "recipe.SettingIdentifiesATargetAnchorsIts": { "hash": "sha256-3777811417a4", "other": "pole {{.Setting}} wskazuje partię, wyznacza jej ziarno i łączy ją z manifestem" }, + "recipe.SettingIsHowManyFilesOf": { "hash": "sha256-4817aba23867", "other": "pole {{.Setting}} mówi, ile plików tej grupy zawiera kontener" }, + "recipe.SettingIsHowManyFilesThis": { "hash": "sha256-fc479ec62722", "other": "pole {{.Setting}} mówi, ile plików wytwarza ta partia, więc jest czytane dokładnie tak, jak je zapisano" }, + "recipe.SettingIsOneSizeAndThe": { "hash": "sha256-93e665020c2d", "other": "pole {{.Setting}} to jeden rozmiar, a zestaw powstaje po obu jego stronach" }, + "recipe.SettingIsThreeChosenSizesAround": { "hash": "sha256-f4373c72f697", "other": "pole {{.Setting}} to trzy wybrane rozmiary wokół limitu, więc losowanie rozmiarów dodatkowo przekreśliłoby powód ich wyboru" }, + "recipe.SettingIsWrittenAs2mbOr": { "hash": "sha256-232713d53bb0", "other": "pole {{.Setting}} zapisuje się jako 2mb albo jako zwykłą liczbę bajtów" }, + "recipe.SettlingItTwiceGivesTwoDifferent": { "hash": "sha256-eff74d7341c6", "other": "dwukrotne uporządkowanie daje dwa różne pliki, więc sprawdzenie nigdy by nie przeszło, a skrót przepisu w manifeście by go nie opisywał" }, + "recipe.StatesBothSettingAndABoundary": { "hash": "sha256-e26d703b93e3", "other": "{{.Where}} podaje naraz pole {{.Setting}} i limit testowany" }, + "recipe.StatesBothSettingAndASize": { "hash": "sha256-fe85fe68a072", "other": "{{.Where}} podaje naraz pole {{.Setting}} i size-range" }, + "recipe.Target": { "hash": "sha256-f4062d7dbd6c", "other": "partia {{.Target}}" }, + "recipe.TargetNumber": { "hash": "sha256-7292920a57c5", "other": "partia nr {{.Number}}" }, + "recipe.TargetSettingIsUsedTwice": { "hash": "sha256-85e32b5347f6", "other": "pole {{.Setting}} partii o wartości {{.ID}} występuje dwa razy" }, + "recipe.TheFileHoldsYAMLDocuments": { "hash": "sha256-180cc02ceaf6", "other": "plik zawiera dokumenty YAML w liczbie {{.Value}}" }, + "recipe.TheFillModeIsNotSettable": { "hash": "sha256-baa1d43a605b", "other": "trybu wypełniania nie da się jeszcze ustawić" }, + "recipe.TheLayoutThisFileNeedsIs": { "hash": "sha256-3821ee0220a7", "other": "układu, którego ten plik potrzebuje, formater nie umie zapisać, więc uporządkowanie zastąpiłoby go czymś, czego już nie da się odczytać" }, + "recipe.TheListIsClosedSoThat": { "hash": "sha256-bd547801019b", "other": "lista jest zamknięta, żeby raport mógł grupować po regule, a literówka tworzyłaby kategorię jednoelementową" }, + "recipe.TheManifestIsAlwaysWrittenAs": { "hash": "sha256-c91903773952", "other": "manifest jest na razie zawsze zapisywany jako jeden plik" }, + "recipe.ThePresetSTargetsComeFirst": { "hash": "sha256-38dc9b08929a", "other": "partie presetu idą pierwsze, a każde pole {{.Setting}} wyznacza ziarno, więc druga taka partia byłaby nie do odróżnienia od pierwszej" }, + "recipe.TheRecipeAsksForNoFiles": { "hash": "sha256-dfe478303a1a", "other": "przepis nie prosi o żadne pliki" }, + "recipe.TheRecipeHasNoVersion": { "hash": "sha256-328db8bc7fcb", "other": "przepis nie ma wersji" }, + "recipe.TheSeedDecidesEveryByteOf": { "hash": "sha256-d273c2d6fe96", "other": "ziarno decyduje o każdym bajcie przebiegu, więc jest czytane dokładnie tak, jak je zapisano, i nigdy nie jest zgadywane" }, + "recipe.TheSettingOfTheContainerFollows": { "hash": "sha256-6a5eac5ec73d", "other": "pole {{.Setting}} kontenera wynika z pola {{.Setting}} tego, co zawiera, więc każda grupa je podaje" }, + "recipe.TheSettledShapeOfThisFile": { "hash": "sha256-20b623f6d3da", "other": "uporządkowanej postaci tego pliku nie da się odczytać z powrotem" }, + "recipe.TheSettledShapeOfThisFile2": { "hash": "sha256-23ef3499f4b2", "other": "uporządkowana postać tego pliku nie zostaje uporządkowana" }, + "recipe.TheToolVersionThisRecipeRequires": { "hash": "sha256-aad8ed8feddf", "other": "wersja narzędzia, której ten przepis wymaga, nie jest jeszcze sprawdzana" }, + "recipe.TheTypeOfADamageIs": { "hash": "sha256-c7a22bfecd16", "other": "type uszkodzenia to nazwa, pod którą zna je ta wersja" }, + "recipe.TheVersionDecidesHowTheRest": { "hash": "sha256-e7a6e86b83ab", "other": "wersja decyduje, jak czytana jest reszta pliku, więc nigdy nie jest zgadywana" }, + "recipe.ThisBuildUnderstandsVersion": { "hash": "sha256-09815b5705a3", "other": "ta wersja rozumie wersję schematu {{.SchemaVersion}}" }, + "recipe.ThisIsAMistakeInThe": { "hash": "sha256-7fa411c5c61a", "other": "to błąd w presecie, a nie w przepisie - uruchom \"tfg preset eject\" i edytuj wynik" }, + "recipe.ThisRecipeBuildsOnPresetAnd": { "hash": "sha256-0cdff56485bc", "other": "ten przepis buduje na preset:{{.Preset}}, a partie presetu nie zostały podane" }, + "recipe.ThisRecipeDoesNotBuildOn": { "hash": "sha256-008e7bc28deb", "other": "ten przepis nie buduje na presecie" }, + "recipe.ThisSettingIsOneOfTwo": { "hash": "sha256-3ad6ce9edcc3", "other": "to ustawienie jest jednym z dwóch słów, a wszystko inne trzeba by zgadywać" }, + "recipe.ThisSettingIsOneValueWritten": { "hash": "sha256-342b49446a29", "other": "to ustawienie to jedna wartość, zapisana obok nazwy, a nie wcięta pod nią" }, + "recipe.TooDeep": { "hash": "sha256-0d47280182a1", "other": "{{.Name}} zagnieżdża listy i mapy na głębokość {{.Depth}}, a limit to {{.Limit}}. Czytanie głęboko zagnieżdżonego dokumentu kosztuje pamięć, która rośnie dużo szybciej niż sam dokument, więc mały plik może wyczerpać tę maszynę, zanim cokolwiek zostanie zapisane. Zapisz partie jako zwykłą listę" }, + "recipe.TooLarge": { "hash": "sha256-c0e7fdfe5e2a", "other": "{{.Name}} ma {{.Bytes}} B, a limit to {{.Limit}} B. Przepis to dokument, który ktoś pisze, a czas czytania rośnie z jego rozmiarem, więc nieograniczony byłby sposobem na zawieszenie budowania. Podziel go na kilka przepisów albo generuj partie pętlą we własnym skrypcie" }, + "recipe.UnitsCountIn1024sSo10mb": { "hash": "sha256-6583bf42dc24", "other": "jednostki liczą się po 1024, więc 10mb to 10485760 bajtów" }, + "recipe.Unreadable": { "hash": "sha256-6db3546dfc2c", "other": "{{.Name}} nie daje się odczytać jako przepis:\n{{.Detail}}" }, + "recipe.UnspecifiedExpectationsAreLeftInThe": { "hash": "sha256-94e9e4b033f1", "other": "nieokreślone oczekiwania zostają w manifeście do rozstrzygnięcia przez odbiorcę" }, + "recipe.UpgradeTheToolOrWriteThe": { "hash": "sha256-191faed33769", "other": "zaktualizuj narzędzie albo napisz przepis według wersji podanej wyżej" }, + "recipe.UseAcceptRejectSanitizeOrUnspecified": { "hash": "sha256-85d365e7de8c", "other": "użyj accept, reject, sanitize albo unspecified" }, + "recipe.UseBoundary10mbOrAPlain": { "hash": "sha256-82fc57a9cff3", "other": "użyj boundary: 10mb albo zwykłej liczby bajtów" }, + "recipe.UseCount0ForAGroup": { "hash": "sha256-de61b29c39d0", "other": "użyj count: 0 dla grupy, która nic nie wnosi, albo usuń wpis" }, + "recipe.UseCount50": { "hash": "sha256-ec6634fce2c2", "other": "użyj count: 50" }, + "recipe.UseExpectedAcceptOrABlock": { "hash": "sha256-d7d3c6dee42f", "other": "użyj expected: accept albo bloku z outcome:" }, + "recipe.UseFormatPdfOrRunTfg": { "hash": "sha256-39cbda389c37", "other": "użyj format: pdf albo uruchom tfg formats, żeby zobaczyć listę" }, + "recipe.UseLocaleEnOrLeaveThe": { "hash": "sha256-0a19d43dd48f", "other": "użyj locale: en albo pomiń ten wiersz" }, + "recipe.UseOneOfReasons": { "hash": "sha256-4e2410ba8441", "other": "użyj jednej z: {{.Reasons}}" }, + "recipe.UseSettingAtLeastOneByte": { "hash": "sha256-810411e520cb", "other": "wpisz w polu {{.Setting}} wartość co najmniej o bajt mniejszą od największej liczby" }, + "recipe.UseSettingOfOrLessOr": { "hash": "sha256-a950b91ebbd9", "other": "wpisz w polu {{.Setting}} najwyżej {{.MaxFilesPerRun}} albo rozłóż kontener na kilka wpisów" }, + "recipe.UseSettingOfOrLessOr2": { "hash": "sha256-e940b6c564de", "other": "wpisz w polu {{.Setting}} najwyżej {{.MaxFilesPerRun}} albo rozłóż partię na kilka przepisów" }, + "recipe.UseSettingSuchAs10mb512kb": { "hash": "sha256-bead317186c2", "other": "wpisz w polu {{.Setting}} na przykład 10mb, 512kb albo zwykłą liczbę bajtów" }, + "recipe.UseSettingSuchAs2mb512kb": { "hash": "sha256-cd8679e4c31b", "other": "wpisz w polu {{.Setting}} na przykład 2mb, 512kb albo zwykłą liczbę bajtów" }, + "recipe.UseSize2mbOrSize2097152": { "hash": "sha256-e3c50d33a6be", "other": "użyj size: 2mb albo size: 2097152" }, + "recipe.UseSizeRange1kb8kb": { "hash": "sha256-ae8d05cf1f68", "other": "użyj size-range: 1kb-8kb" }, + "recipe.UseSizeRange1kb8kbOr": { "hash": "sha256-f1d9e9c21ff4", "other": "użyj size-range: 1kb-8kb albo pary zwykłych liczb bajtów" }, + "recipe.UseTrueOrFalse": { "hash": "sha256-27db5e3986d1", "other": "użyj {{.Subject}}: true albo {{.Subject2}}: false" }, + "recipe.VersionIsNotASchemaThis": { "hash": "sha256-3fe3d2cfbd35", "other": "wersja {{.Version}} to schemat, którego ta wersja nie zna" }, + "recipe.VersionIsNotAWholeNumber": { "hash": "sha256-82edb8fdc772", "other": "wersja {{.Text}} nie jest liczbą całkowitą" }, + "recipe.WhereError": { "hash": "sha256-806cf9b89163", "other": "{{.Where}}: {{.Error}}" }, + "recipe.WhereWhat": { "hash": "sha256-d0a8cf451027", "other": "{{.Where}}: {{.What}}" }, + "recipe.WithBelongsBesideExtendsItSays": { "hash": "sha256-f7313a2dbc3d", "other": "with należy obok extends: mówi, co wypełnić, a extends mówi, w co" }, + "recipe.WithFillsTheParametersOfA": { "hash": "sha256-91f25e74d772", "other": "with wypełnia parametry presetu, a żaden preset nie jest wskazany" }, + "recipe.WriteADecimalNumberSuchAs": { "hash": "sha256-fd6a6e21bf53", "other": "wpisz liczbę dziesiętną, na przykład seed: 20260802" }, + "recipe.WriteADecimalNumberSuchAs2": { "hash": "sha256-69ec31d15356", "other": "wpisz liczbę dziesiętną, na przykład count: 10" }, + "recipe.WriteExtendsPresetIdAndRun": { "hash": "sha256-eec676d41814", "other": "wpisz extends: preset: i uruchom \"tfg preset list\", żeby zobaczyć identyfikatory tej wersji" }, + "recipe.WriteItOnOneLineSuch": { "hash": "sha256-b00307984821", "other": "zapisz to w jednym wierszu, na przykład {{.Example}}" }, + "recipe.WriteTheNameAsAWord": { "hash": "sha256-effad2028c85", "other": "zapisz nazwę jako słowo" }, + "recipe.WriteTheNameOnItsOwn": { "hash": "sha256-da431c8ff106", "other": "zapisz samą nazwę albo mapę zaczynającą się od type" }, + "recipe.WriteVersion": { "hash": "sha256-6b426dee7162", "other": "wpisz version: {{.SchemaVersion}}" }, + "textenc.HasToBeEncoding": { "hash": "sha256-cf8214cf1fa6", "other": "musi to być {{.UTF8}}, {{.UTF16LE}} albo {{.UTF16BE}}" }, + "tool.AToolIsNamedExactlyAnd": { "hash": "sha256-756a78c6f204", "other": "narzędzie podaje się dokładną nazwą, a najbliższa nazwa do pomylonej może robić coś innego" }, + "tool.NameTheToWorkOn": { "hash": "sha256-3dac06c6319b", "other": "wskaż, na czym narzędzie ma pracować: {{.Kind}}" }, + "tool.NoWasGiven": { "hash": "sha256-c984aa47e200", "other": "nie podano wejścia: {{.Kind}}" }, + "tool.Sentence": { "hash": "sha256-d9a59b987ed3", "other": "{{.What}} - {{.Why}}. {{.Instead}}" }, + "tool.TheToolHasNothingToRead": { "hash": "sha256-8b5043abfb15", "other": "bez tego narzędzie nie ma czego czytać" }, + "tool.ThereIsNoToolCalled": { "hash": "sha256-151e818f434e", "other": "nie ma narzędzia o nazwie {{.ID}}" }, + "tool.ThisBuildHasNoTools": { "hash": "sha256-42b26a93974c", "other": "ta wersja nie ma narzędzi" }, + "tool.UseOneOf": { "hash": "sha256-a53588601259", "other": "użyj jednego z: {{.Known}}" }, + "wav.DropTheLabel": { "hash": "sha256-b8bfddcdab7d", "other": " albo usuń etykietę" }, + "xlsx.Columns": { "hash": "sha256-5a53b3c96cb2", "one": "{{.Count}} kolumnę", "few": "{{.Count}} kolumny", "many": "{{.Count}} kolumn", "other": "{{.Count}} kolumny" }, + "xlsx.JointFewer": { "hash": "sha256-15c3bae806db", "other": "{{.Why}}. Poproś o mniej wierszy albo mniej kolumn" }, + "xlsx.Rows": { "hash": "sha256-244cc1fe9311", "one": "{{.Count}} wiersz", "few": "{{.Count}} wiersze", "many": "{{.Count}} wierszy", "other": "{{.Count}} wiersza" }, + "xml.NeedsBOM": { "hash": "sha256-74660e3e12d1", "other": "XML w {{.Encoding}} musi zaczynać się znacznikiem kolejności bajtów, więc to wymaga bom=true albo encoding={{.UTF8}}" } +} diff --git a/internal/gui/text/locale/toolkit/en.json b/internal/gui/text/locale/toolkit/en.json new file mode 100644 index 00000000..1cd58598 --- /dev/null +++ b/internal/gui/text/locale/toolkit/en.json @@ -0,0 +1,42 @@ +{ + "(Parent)": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-c81daefccf15", "other": "(Parent)" }, + "Advanced": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-9f088dbebd6c", "other": "Advanced" }, + "Cancel": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-19766ed6ccb2", "other": "Cancel" }, + "Confirm": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-eebdd24a77d9", "other": "Confirm" }, + "Copy": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-e21f935f11d7", "other": "Copy" }, + "Create Folder": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-8ce1226161bc", "other": "Create Folder" }, + "Cut": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-1f45f02561f4", "other": "Cut" }, + "Enter filename": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-f06a0eeedcba", "other": "Enter filename" }, + "Error": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-54a0e8c17ebb", "other": "Error" }, + "Favourites": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-d97d51d37092", "other": "Favourites" }, + "File": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-50009ce1da4d", "other": "File" }, + "Folder": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-74ccd4330384", "other": "Folder" }, + "New Folder": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-c75ba8074400", "other": "New Folder" }, + "No": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-1ea442a134b2", "other": "No" }, + "OK": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-565339bc4d33", "other": "OK" }, + "Open": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-ed077f3d8125", "other": "Open" }, + "Paste": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-f3380f7b44bd", "other": "Paste" }, + "Quit": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-b3eb37acb66b", "other": "Quit" }, + "Redo": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-74273989b096", "other": "Redo" }, + "Save": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-1509f561f241", "other": "Save" }, + "Select all": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-1fc9a387654d", "other": "Select all" }, + "Show Hidden Files": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-415951a5dbe9", "other": "Show Hidden Files" }, + "Undo": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-a8283ade3185", "other": "Undo" }, + "Yes": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-85a39ab345d6", "other": "Yes" }, + "file.name": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-dcd1d5223f73", "other": "Name" }, + "file.parent": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-5f7953f7c9b6", "other": "Parent" }, + "friday": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-e21f3f372a46", "other": "Friday" }, + "friday.short": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-66dab40cea1d", "other": "Fri" }, + "monday": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-6a00dfc1dc86", "other": "Monday" }, + "monday.short": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-f40d7f51f69e", "other": "Mon" }, + "saturday": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-dbe35c7363c2", "other": "Saturday" }, + "saturday.short": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-fdeb71b569e0", "other": "Sat" }, + "sunday": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-873fef760e5d", "other": "Sunday" }, + "sunday.short": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-db18f17fe532", "other": "Sun" }, + "thursday": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-fc2662062ffd", "other": "Thursday" }, + "thursday.short": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-7da11212ed34", "other": "Thu" }, + "tuesday": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-7d8af1de1262", "other": "Tuesday" }, + "tuesday.short": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-d1eb39b09bf5", "other": "Tue" }, + "wednesday": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-c0a6cc829cd0", "other": "Wednesday" }, + "wednesday.short": { "description": "A word of the graphics toolkit the window is built with - in its menus and dialogs. The window gives the toolkit these in the language it speaks.", "hash": "sha256-58339f45df96", "other": "Wed" } +} diff --git a/internal/gui/text/locale/toolkit/pl.json b/internal/gui/text/locale/toolkit/pl.json new file mode 100644 index 00000000..aa35af62 --- /dev/null +++ b/internal/gui/text/locale/toolkit/pl.json @@ -0,0 +1,42 @@ +{ + "(Parent)": { "hash": "sha256-c81daefccf15", "other": "(wyżej)" }, + "Advanced": { "hash": "sha256-9f088dbebd6c", "other": "Zaawansowane" }, + "Cancel": { "hash": "sha256-19766ed6ccb2", "other": "Anuluj" }, + "Confirm": { "hash": "sha256-eebdd24a77d9", "other": "Potwierdź" }, + "Copy": { "hash": "sha256-e21f935f11d7", "other": "Kopiuj" }, + "Create Folder": { "hash": "sha256-8ce1226161bc", "other": "Utwórz folder" }, + "Cut": { "hash": "sha256-1f45f02561f4", "other": "Wytnij" }, + "Enter filename": { "hash": "sha256-f06a0eeedcba", "other": "Wpisz nazwę pliku" }, + "Error": { "hash": "sha256-54a0e8c17ebb", "other": "Błąd" }, + "Favourites": { "hash": "sha256-d97d51d37092", "other": "Ulubione" }, + "File": { "hash": "sha256-50009ce1da4d", "other": "Plik" }, + "Folder": { "hash": "sha256-74ccd4330384", "other": "Folder" }, + "New Folder": { "hash": "sha256-c75ba8074400", "other": "Nowy folder" }, + "No": { "hash": "sha256-1ea442a134b2", "other": "Nie" }, + "OK": { "hash": "sha256-565339bc4d33", "other": "OK" }, + "Open": { "hash": "sha256-ed077f3d8125", "other": "Otwórz" }, + "Paste": { "hash": "sha256-f3380f7b44bd", "other": "Wklej" }, + "Quit": { "hash": "sha256-b3eb37acb66b", "other": "Zakończ" }, + "Redo": { "hash": "sha256-74273989b096", "other": "Ponów" }, + "Save": { "hash": "sha256-1509f561f241", "other": "Zapisz" }, + "Select all": { "hash": "sha256-1fc9a387654d", "other": "Zaznacz wszystko" }, + "Show Hidden Files": { "hash": "sha256-415951a5dbe9", "other": "Pokaż ukryte pliki" }, + "Undo": { "hash": "sha256-a8283ade3185", "other": "Cofnij" }, + "Yes": { "hash": "sha256-85a39ab345d6", "other": "Tak" }, + "file.name": { "hash": "sha256-dcd1d5223f73", "other": "Nazwa" }, + "file.parent": { "hash": "sha256-5f7953f7c9b6", "other": "Folder nadrzędny" }, + "friday": { "hash": "sha256-e21f3f372a46", "other": "piątek" }, + "friday.short": { "hash": "sha256-66dab40cea1d", "other": "pt" }, + "monday": { "hash": "sha256-6a00dfc1dc86", "other": "poniedziałek" }, + "monday.short": { "hash": "sha256-f40d7f51f69e", "other": "pon" }, + "saturday": { "hash": "sha256-dbe35c7363c2", "other": "sobota" }, + "saturday.short": { "hash": "sha256-fdeb71b569e0", "other": "sob" }, + "sunday": { "hash": "sha256-873fef760e5d", "other": "niedziela" }, + "sunday.short": { "hash": "sha256-db18f17fe532", "other": "niedz" }, + "thursday": { "hash": "sha256-fc2662062ffd", "other": "czwartek" }, + "thursday.short": { "hash": "sha256-7da11212ed34", "other": "czw" }, + "tuesday": { "hash": "sha256-7d8af1de1262", "other": "wtorek" }, + "tuesday.short": { "hash": "sha256-d1eb39b09bf5", "other": "wt" }, + "wednesday": { "hash": "sha256-c0a6cc829cd0", "other": "środa" }, + "wednesday.short": { "hash": "sha256-58339f45df96", "other": "śr" } +} diff --git a/internal/gui/text/registry.go b/internal/gui/text/registry.go index 7bf28c90..02825fe6 100644 --- a/internal/gui/text/registry.go +++ b/internal/gui/text/registry.go @@ -46,7 +46,7 @@ type Owner string // preset's here, because every screen draws them in one list with its // parameters. The same English under two presets is one Polish sentence twice, // and a guard holds the two to agreeing. -func FormatOwner(id string) Owner { return Owner("format/" + id) } +func FormatOwner(id string) Owner { return Owner(core.FormatOwner(id)) } func PresetOwner(id string) Owner { return Owner("preset/" + id) } func DamageOwner(id string) Owner { return Owner("damage/" + id) } @@ -70,16 +70,21 @@ func WrittenAs(o Owner, key string) string { // The keys, built here and nowhere else - exported for the guard that writes // registry/en.json out of the registries, so the window and the guard cannot // come to ask for two different things. -func LabelKey(name string) string { return "Label." + name } +func LabelKey(name string) string { return core.LabelKey(name) } func DetailKey(o Owner, name string) string { return "Detail." + string(o) + "." + name } func GroupKey(o Owner, group string) string { return "Group." + string(o) + "." + group } -func UnitKey(unit string) string { return "Unit." + unit } +func UnitKey(unit string) string { return core.UnitKey(unit) } func ShapeKey(shape string) string { return "Shape." + shape } -func JointKey(o Owner, of, by string) string { return "Joint." + string(o) + "." + of + "." + by } +func JointKey(o Owner, of, by string) string { return core.JointKey(string(o), of, by) } func QuestionKey(preset string) string { return "Question." + preset } func CatchKey(preset string, n int) string { return "Catch." + preset + "." + strconv.Itoa(n) } func NoteKey(preset, about string) string { return "Note." + preset + "." + about } +// ChoiceKey is the key of one value of a closed list, by the recipe key of the +// setting the list belongs to - the same value of two settings may need two +// words, and the same setting declared by two formats means one thing. +func ChoiceKey(of, value string) string { return core.ChoiceKey(of, value) } + // The words of a tool, under keys of their own rather than beside a preset's: // a tool and a preset may one day share a name, and "Question." would then // be one key for two sentences. @@ -143,6 +148,47 @@ func PresetNote(preset, about, english string) string { return lookup(NoteKey(preset, about), english) } +// FormatNameKey, TitleKey and LicenceKey are the keys of a format's name, a +// preset's title and one paragraph of the licence notice. +func FormatNameKey(id string) string { return "Format." + id } +func TitleKey(preset string) string { return "Title." + preset } +func LicenceKey(n int) string { return "Licence." + strconv.Itoa(n) } + +// FormatName is what a format is called, beside its id in a list of formats. +// Most are the names of standards and stay as they are in every language. +func FormatName(id, english string) string { return lookup(FormatNameKey(id), english) } + +// PresetTitle is what a preset is called, beside its id in a list of presets. +func PresetTitle(id, english string) string { return lookup(TitleKey(id), english) } + +// LicenceParagraphs is the licence notice split into the paragraphs a +// translation keys, in the order they stand. The first is the name and the +// copyright line, which no language translates. +func LicenceParagraphs(notice string) []string { + return strings.Split(strings.TrimSpace(notice), "\n\n") +} + +// Licence is the licence notice in the window's language, paragraph by +// paragraph, and a sentence after it saying that the English text binds where +// the window speaks another language. The sentence is empty in English, so an +// English window shows the notice as it always has. +func Licence(notice string) string { + paras := LicenceParagraphs(notice) + out := make([]string, len(paras)) + for i, p := range paras { + out[i] = lookup(LicenceKey(i+1), p) + } + if binds := LicenceBinds(); binds != "" { + out = append(out, binds) + } + return strings.Join(out, "\n\n") +} + +// ChoiceName is what a list shows for one of its values. The value itself is +// what a recipe, the command line and the manifest write, and what the list +// hands back - only the words on the screen are the window's. +func ChoiceName(of, value string) string { return lookup(ChoiceKey(of, value), value) } + // ToolQuestion is the question a tool answers, which is its title. func ToolQuestion(id, english string) string { return lookup(ToolQuestionKey(id), english) } diff --git a/internal/gui/text/registrywords.go b/internal/gui/text/registrywords.go index 81c8cae6..c3504630 100644 --- a/internal/gui/text/registrywords.go +++ b/internal/gui/text/registrywords.go @@ -3,10 +3,13 @@ package text import ( "sort" + "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/damage" "github.com/donislawdev/TestingFilesGenerator/internal/format" "github.com/donislawdev/TestingFilesGenerator/internal/preset" + "github.com/donislawdev/TestingFilesGenerator/internal/recipe" "github.com/donislawdev/TestingFilesGenerator/internal/tool" + "github.com/donislawdev/TestingFilesGenerator/internal/version" ) // RegistryWord is one sentence of the registries the window shows, under the @@ -41,9 +44,24 @@ func RegistryWords() []RegistryWord { for _, d := range tool.All() { w.tool(d) } + w.lists() + for i, p := range LicenceParagraphs(version.LicenceNotice) { + w.add(LicenceKey(i+1), p, "One paragraph of the licence notice on the About screen. The first is the name and the copyright line and stays as it is. The program's name and the licence's name stay as they are.") + } return w.sorted() } +// lists is the values of the two closed lists a window offers under "notes for +// the manifest": what the system under test should do, and the rule tested. +func (w registryWords) lists() { + for _, o := range recipe.Outcomes() { + w.add(ChoiceKey(recipe.KeyExpected, o), o, "One entry of the list of what the system under test should do with a file. The manifest writes the value itself.") + } + for _, r := range recipe.Reasons() { + w.add(ChoiceKey(recipe.KeyExpectedReason, r), r, "One entry of the list of the rule a file tests. The manifest writes the value itself.") + } +} + // registryWords collects by key. A key reached twice - a width declared by // four formats has one label - holds the sentence it was first reached with. type registryWords map[string]RegistryWord @@ -57,6 +75,7 @@ func (w registryWords) add(key, english, where string) { func (w registryWords) format(d format.Descriptor) { owner := FormatOwner(d.ID) + w.add(FormatNameKey(d.ID), d.Name, "What the "+d.ID+" format is called, beside its id in a list of formats. The name of a standard stays as it is - only a name that describes the file is translated.") w.settings(owner, "the "+d.ID+" format", d.Properties) for _, j := range d.JointLimits { w.add(JointKey(owner, j.Of, j.By), j.Why, "Why "+j.Of+" times "+j.By+" of the "+d.ID+ @@ -67,6 +86,7 @@ func (w registryWords) format(d format.Descriptor) { func (w registryWords) preset(p preset.Preset) { whose := "the " + p.ID + " preset" + w.add(TitleKey(p.ID), p.Title, "What "+whose+" is called, beside its id in the list of presets.") declared := append(append([]format.Property{}, p.Parameters...), p.Globals()...) w.settings(PresetOwner(p.ID), whose, declared) w.add(QuestionKey(p.ID), p.Question, "The question "+whose+" answers, at the top of the preset screen.") @@ -85,6 +105,7 @@ func (w registryWords) tool(d tool.Descriptor) { for _, in := range d.Inputs { w.add(LabelKey(in.Name), EnglishLabel(in.Name), "The name beside the box of what "+whose+" works on.") w.add(InputKey(d.ID, in.Name), in.Detail, "The sentence under the "+in.Name+" box of "+whose+".") + w.add(core.KindKey(string(in.Kind)), string(in.Kind), "What a tool works on, as a word, in a refusal that one was not given: no file was given.") } for _, n := range d.Notes { w.add(ToolNoteKey(d.ID, n.ID), n.Says, "A line of the result of "+whose+", ending in a colon - what follows it is a list of names or one number.") @@ -98,6 +119,9 @@ func (w registryWords) settings(owner Owner, whose string, declared []format.Pro w.add(DetailKey(owner, p.Name), p.Detail, "The sentence under the "+p.Name+" setting of "+whose+", after what it takes.") w.add(GroupKey(owner, p.Group), p.Group, "The heading over a block of settings of "+whose+".") w.add(ShapeKey(p.Shape), p.Shape, "What the text in a box has to look like, in the sentence under it.") + for _, c := range p.Choices { + w.add(ChoiceKey(p.Name, c), c, "One entry of the "+p.Name+" list, as the list shows it. A recipe and the command line write the value itself, so a token such as utf-8, a4, sha256 or 644 stays as it is.") + } w.unit(p.Unit) } } diff --git a/internal/gui/text/said.go b/internal/gui/text/said.go new file mode 100644 index 00000000..2b8b91af --- /dev/null +++ b/internal/gui/text/said.go @@ -0,0 +1,221 @@ +package text + +import ( + "errors" + "fmt" + "io/fs" + "strings" + "syscall" + + "github.com/nicksnyder/go-i18n/v2/i18n" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" +) + +// The engine's sentences - refusals and notes - in the language the window +// speaks. docs/OKNO-PO-POLSKU-2026-10-05.md section 3. +// +// The engine keeps each sentence as core.Said: an id, the English layout the +// command line prints, and named values. A window looks the id up in the +// folder below and fills the fields with the values, each written the way the +// English layout writes it - quoted where it quotes, a number where it prints +// a number - so a translation cannot change what a value looks like, only the +// words around it. +// +// No entry, or a window in English, is the sentence exactly as the window +// showed it before any of this existed. That is not a courtesy: the stored +// pictures of every screen hold the English window to its old self, which is +// the proof this layer changed nothing there. + +// SaidFolder is where the engine's sentences are kept, inside the folder of +// the window's catalogue. Its en.json is written for a translator by the guard +// that reads the sentences out of the code, and is never loaded. +const SaidFolder = "said" + +// Refusal is an error in the window's language, naming the setting it is about +// by the label above that setting's box. An empty label names it by its recipe +// key, which is what a refusal at the foot of the form has always done. +func Refusal(err error, label string) string { + if err == nil { + return "" + } + var said interface{ Said() core.Said } + if errors.As(err, &said) { + if out, ok := translated(said.Said(), label); ok { + return out + } + return inEnglish(err, label) + } + if core.IsDefect(err) { + return Defect(err.Error()) + } + return systemIn(err) +} + +// inEnglish is a refusal the way the window has always shown it. +func inEnglish(err error, label string) string { + var reworded interface{ InTheWordsOf(string) string } + if label != "" && errors.As(err, &reworded) { + return reworded.InTheWordsOf(label) + } + return err.Error() +} + +// Sentence is one of the engine's sentences that is not a refusal - a note a +// run left - in the window's language. +func Sentence(s core.Said) string { + if out, ok := translated(s, ""); ok { + return out + } + return s.String() +} + +// translated is the sentence in the window's language, and false when this +// window says it in English: the language is English, nothing is loaded, or +// the language has no entry for it. +func translated(s core.Said, label string) (string, bool) { + if pseudo { + return fill(pseudoOf(core.NamedLayout(s.Layout(), s.Names())), valuesOf(s, label)), true + } + if localiser == nil || Speaking() == English || s.ID() == "" { + return "", false + } + asked := &i18n.LocalizeConfig{MessageID: s.ID(), TemplateData: valuesOf(s, label)} + if s.Plural() { + asked.PluralCount = countOf(s) + } + out, err := localiser.Localize(asked) + if err != nil || out == "" { + return "", false + } + return s.Shaped(out), true +} + +// valuesOf is every value of a sentence as the text a translation puts in its +// field, with the setting under its reserved name. +func valuesOf(s core.Said, label string) map[string]any { + values := map[string]any{core.SettingField: label} + directives := s.Directives() + for i, a := range s.Args() { + directive := "%v" + if i < len(directives) { + directive = directives[i] + } + values[a.Name] = valueIn(a.Value, directive, label) + } + return values +} + +// valueIn is one value as a field shows it. +// +// A value somebody typed goes through core.ShownText, because it is put on a +// screen and may carry a character that turns the text around it - the foot +// of the form did that before, and the line under a box did not. +func valueIn(v any, directive, label string) string { + if out, several := severalIn(v, label); several { + return out + } + switch v := v.(type) { + case core.Said: + if out, ok := translated(v, label); ok { + return out + } + return core.InTheWordsOf(v.String(), label) + case core.Bytes: + return HumanBytes(int64(v)) + case core.Choice: + return ChoiceName(v.Of, v.Value) + case core.Term: + return lookup(v.Key, v.Text) + case error: + return Refusal(v, label) + case interface{ Said() core.Said }: + return valueIn(v.Said(), directive, label) + } + return core.ShownText(fmt.Sprintf(strings.Replace(directive, "w", "v", 1), v)) +} + +// severalIn is a value holding several things, joined the way this language +// joins them, and false for a value holding one. +func severalIn(v any, label string) (string, bool) { + switch v := v.(type) { + case core.Joined: + return Joined(v.Items, v.And), true + case core.Sentences: + return Formats(eachIn(v, label)), true + case core.Conjoined: + return Joined(eachIn(v, label), true), true + case core.Lines: + return strings.Join(eachIn(v, label), "\n "), true + case core.Choices: + names := make([]string, len(v.Values)) + for i, value := range v.Values { + names[i] = ChoiceName(v.Of, value) + } + return Formats(names), true + } + return "", false +} + +// eachIn is every sentence of a list as a field shows it. +func eachIn(list []core.Said, label string) []string { + out := make([]string, len(list)) + for i, s := range list { + out[i] = valueIn(s, "%s", label) + } + return out +} + +// countOf is the number that chooses the form of a sentence. +func countOf(s core.Said) int64 { + for _, a := range s.Args() { + if n, whole := wholeCount(a.Value); whole && a.Name == core.CountArg { + return n + } + } + return 0 +} + +// wholeCount is a count given as either width of whole number. +func wholeCount(v any) (int64, bool) { + switch n := v.(type) { + case int: + return int64(n), true + case int64: + return n, true + } + return 0, false +} + +// systemIn is an error from the system or from a library, with the system's +// own words about the cause replaced by ours - which is what the command line +// has done since 2026-09-25 (internal/cli/errors.go, systemReason) and the +// window did not (O251). The system answers in its own language, which may be +// neither the window's nor English. +func systemIn(err error) string { + full := err.Error() + var errno syscall.Errno + if !errors.As(err, &errno) { + return core.ShownText(full) + } + var path *fs.PathError + if errors.As(err, &path) { + return core.ShownText(SystemFailure(core.Shown(path.Path), systemReason(errno))) + } + return core.ShownText(strings.ReplaceAll(full, errno.Error(), systemReason(errno))) +} + +// systemReason is our sentence for what the system refused, by its kind - the +// kinds the command line tells apart, from the same place. +func systemReason(errno syscall.Errno) string { + switch core.SystemKindOf(errno) { + case core.SystemNothingThere: + return SystemNothingThere(uint64(errno)) + case core.SystemNoPermission: + return SystemNoPermission(uint64(errno)) + case core.SystemAlreadyThere: + return SystemAlreadyThere(uint64(errno)) + default: + return SystemRefused(uint64(errno)) + } +} diff --git a/internal/gui/text/screens.go b/internal/gui/text/screens.go index 5f69c872..4278ce0f 100644 --- a/internal/gui/text/screens.go +++ b/internal/gui/text/screens.go @@ -257,6 +257,10 @@ func PlaceholderWorkedOut() string { return say("PlaceholderWorkedOut", "worked // means for the file is said in the sentence beside the box. func PlaceholderNotSet() string { return say("PlaceholderNotSet", "not set") } +// PlaceholderChooseOne stands in a menu nothing has been chosen in yet. The +// words are the toolkit's own, which it puts in every empty menu in English. +func PlaceholderChooseOne() string { return say("PlaceholderChooseOne", "(Select one)") } + // PlaceholderFilter stands in the box at the top of an open list of formats, // where typing narrows the list to the formats whose identifier holds what was // typed, or whose name or kind has a word starting with it. @@ -454,10 +458,14 @@ func SettingKey(key string) string { return sayf("SettingKey", "Written as {{.Key}} in a recipe.", map[string]any{"Key": key}) } +// LicenceBinds follows a translated licence notice. Empty in English on +// purpose - there is nothing to say where the notice is the one that binds. +func LicenceBinds() string { return say("AboutLicenceBinds", "") } + // TooManyFiles refuses a run before the list is built, because building it is // the failure. The reason comes from core, so the window and the command line // do not hold two opinions about how many files is too many. -func TooManyFiles(count int64, reason error) string { +func TooManyFiles(count int64, reason string) string { return sayf("TooManyFiles", "this run asks for {{.Count}} files - {{.Reason}}", map[string]any{"Count": count, "Reason": reason}) } diff --git a/internal/gui/text/text.go b/internal/gui/text/text.go index 3ee311df..be4ff714 100644 --- a/internal/gui/text/text.go +++ b/internal/gui/text/text.go @@ -40,6 +40,9 @@ package text import ( "strconv" "strings" + "time" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // Buttons on the run controls, in the order G6 puts them: preview before the @@ -165,6 +168,22 @@ func AndNothingWrittenYet() string { // person reads, and one language's comma is another's ideograph. func Formats(kinds []string) string { return strings.Join(kinds, listSeparator) } +// Joined is names joined the way the window's language joins the last two of +// a list - "a, b and c", "a or b". +func Joined(items []string, and bool) string { + switch len(items) { + case 0: + return "" + case 1: + return items[0] + } + data := map[string]any{"Head": strings.Join(items[:len(items)-1], listSeparator), "Last": items[len(items)-1]} + if and { + return sayf("ListJoinedAnd", "{{.Head}} and {{.Last}}", data) + } + return sayf("ListJoinedOr", "{{.Head}} or {{.Last}}", data) +} + // listSeparator divides the items of a list said on one line. const listSeparator = ", " @@ -238,6 +257,20 @@ func Progress(filesDone, filesTotal int, bytesDone, bytesTotal string, percent i "BytesDone": bytesDone, "BytesTotal": bytesTotal, "Percent": percent}) } +// Roughly is core.Roughly with the units of the window's language - "3m" is +// three metres to a Polish reader. The rounding is core's, so the two +// surfaces round alike. +func Roughly(d time.Duration) string { + r := core.Rough(d) + switch r.Unit { + case 's': + return sayf("TimeSeconds", "{{.Seconds}}s", map[string]any{"Seconds": r.Seconds}) + case 'm': + return sayf("TimeMinutes", "{{.Minutes}}m", map[string]any{"Minutes": r.Minutes}) + } + return sayf("TimeHours", "{{.Hours}}h{{.Minutes}}m", map[string]any{"Hours": r.Hours, "Minutes": r.Minutes}) +} + // TimeLeft is appended to Progress once the estimate is worth showing. func TimeLeft(roughly string) string { return " " + sayf("TimeLeft", "{{.Roughly}} left", map[string]any{"Roughly": roughly}) @@ -369,8 +402,11 @@ func RendererNotLoaded(err error) string { // its own with, for a sentence that supplies its own. Measured on // 2026-09-17: "The specified module could not be found." arrived with one // and the sentence around it ended with two. +// +// The words are the system's own, in the system's language, so they go through +// Refusal, which puts ours in their place with the number beside them (O251). func withoutFullStop(err error) string { - return strings.TrimSuffix(strings.TrimSpace(err.Error()), ".") + return strings.TrimSuffix(strings.TrimSpace(Refusal(err, "")), ".") } // RendererDidNotHelp is what the refusal adds when the renderer was loaded @@ -468,3 +504,38 @@ func SaidWithManifest(said, name string) string { } return said + " " + ManifestNamed(name) } + +// What the system refused, in our words rather than the system's, with the +// number the system gave - the same four kinds and the same sentences the +// command line prints (internal/cli/errors.go, systemReason), told apart in +// one place (core.SystemKindOf). See Refusal in said.go. +func SystemNothingThere(number uint64) string { + return sayf("SystemNothingThere", "there is nothing at that path (system error {{.Number}})", map[string]any{"Number": number}) +} + +func SystemNoPermission(number uint64) string { + return sayf("SystemNoPermission", "the system refused permission (system error {{.Number}})", map[string]any{"Number": number}) +} + +func SystemAlreadyThere(number uint64) string { + return sayf("SystemAlreadyThere", "something is already there (system error {{.Number}})", map[string]any{"Number": number}) +} + +func SystemRefused(number uint64) string { + return sayf("SystemRefused", "the system refused it (system error {{.Number}})", map[string]any{"Number": number}) +} + +// SystemFailure is a path and what the system refused about it. The name of +// the operation the system was asked for - open, mkdir - is left out: it is +// the vocabulary of the program, not of the person reading. +func SystemFailure(path, reason string) string { + return sayf("SystemFailure", "{{.Path}}: {{.Reason}}", map[string]any{"Path": path, "Reason": reason}) +} + +// Defect is what the window says about an error only a fault in the program +// can produce, with the error's own words after it for whoever reports it. +// The owner's decision of 2026-10-05: the sentence in the window's language, +// the detail as it came, because the detail is for the report. +func Defect(detail string) string { + return sayf("Defect", "Something inside the program went wrong, and it should not have. Report it with this description: {{.Detail}}", map[string]any{"Detail": detail}) +} diff --git a/internal/gui/window/about.go b/internal/gui/window/about.go index 7904b76d..df795971 100644 --- a/internal/gui/window/about.go +++ b/internal/gui/window/about.go @@ -62,7 +62,7 @@ func About(h Host) fyne.CanvasObject { // In a card like every other block on every other screen, so this reads // as a page of the application rather than as the one screen that was // left as it was. - parts.Section(text.SectionLicence(), parts.Prose(paragraphs(version.LicenceNotice))), + parts.Section(text.SectionLicence(), parts.Prose(paragraphs(text.Licence(version.LicenceNotice)))), // The support address, in words, on the one screen somebody reads when // they are deciding what this program costs them. // diff --git a/internal/gui/window/generate.go b/internal/gui/window/generate.go index 9e7dadaa..dad014ec 100644 --- a/internal/gui/window/generate.go +++ b/internal/gui/window/generate.go @@ -677,7 +677,7 @@ func (g *Generate) settle() ([]engine.Target, engine.Options, error) { // defect as reading a number out of a box and losing the subject. case count > core.MaxFilesPerRun: bad = append(bad, saying(engine.SettingCount, - errors.New(text.TooManyFiles(count, core.ErrTooManyFiles)))) + errors.New(text.TooManyFiles(count, text.Refusal(core.ErrTooManyFiles, ""))))) default: files = int(count) } diff --git a/internal/gui/window/preset.go b/internal/gui/window/preset.go index 35e534c1..71118121 100644 --- a/internal/gui/window/preset.go +++ b/internal/gui/window/preset.go @@ -91,6 +91,7 @@ func NewPreset(host Host, links ...fyne.CanvasObject) *Preset { ids := preset.IDs() p.pick = parts.NewChooser(ids, p.onPresetChosen) + p.pick.NameOf = presetTitle p.outDir = entry(startingDirectory(), "") p.seed = entry("0", "") @@ -390,11 +391,27 @@ func notesIn(e *preset.Expansion) []string { spoken := e.Spoken() out := make([]string, 0, len(spoken)) for _, n := range spoken { - out = append(out, text.PresetNote(e.Preset.ID, n.About, n.Said)) + if n.About == "" { + out = append(out, text.Sentence(n.Said)) + continue + } + out = append(out, text.PresetNote(e.Preset.ID, n.About, n.Said.String())) } return out } +// presetTitle is what a preset is called, beside its id in a list of presets - +// the way a format's name stands beside its id (the owner's decision of +// 2026-10-05). The id stays what the box shows, because it is what a recipe +// and the command line write. +func presetTitle(id string) string { + p, err := preset.Get(id) + if err != nil { + return "" + } + return text.PresetTitle(id, p.Title) +} + // engineTarget turns one recipe target into one engine target. // // The command line has the same conversion and the two cannot be shared - they diff --git a/internal/gui/window/recipebase.go b/internal/gui/window/recipebase.go index 8fa15748..c5db5678 100644 --- a/internal/gui/window/recipebase.go +++ b/internal/gui/window/recipebase.go @@ -74,6 +74,7 @@ func newBase(r *Recipe) *base { r.rebuild() } }) + b.pick.NameOf = presetTitle // Chosen here rather than left empty, so a switch turned on shows a // preset with its parameters at once rather than a menu asking to be // opened first. diff --git a/internal/gui/window/recipefolds.go b/internal/gui/window/recipefolds.go index eda89d54..e0136170 100644 --- a/internal/gui/window/recipefolds.go +++ b/internal/gui/window/recipefolds.go @@ -138,8 +138,10 @@ func newBatchNotes() batchNotes { // has to stay unstated - manifest rule MF5, because an invented expectation // produces false failures in somebody else's test run. n.expected = parts.NewChooser(recipe.Outcomes(), nil) + n.expected.ShownAs = outcomeName n.expected.PlaceHolder = text.PlaceholderNotStated() n.reason = parts.NewChooser(recipe.Reasons(), nil) + n.reason.ShownAs = reasonName n.reason.PlaceHolder = text.PlaceholderNotStated() return n } @@ -198,7 +200,23 @@ func (b *batch) settingsSaid() string { // said is what the manifest notes say while they are away. func (n *batchNotes) said() string { - return text.FoldedSummary(n.group.Text, n.purpose.Text, n.expected.Selected, n.reason.Selected) + return text.FoldedSummary(n.group.Text, n.purpose.Text, outcomeName(n.expected.Selected), reasonName(n.reason.Selected)) +} + +// outcomeName and reasonName are a value of the two lists as the window says +// it. The manifest writes the value. +func outcomeName(v string) string { + if v == "" { + return "" + } + return text.ChoiceName(recipe.KeyExpected, v) +} + +func reasonName(v string) string { + if v == "" { + return "" + } + return text.ChoiceName(recipe.KeyExpectedReason, v) } // openFoldHolding opens everything a box has been put away inside. diff --git a/internal/gui/window/run.go b/internal/gui/window/run.go index 03bb5228..9eb785aa 100644 --- a/internal/gui/window/run.go +++ b/internal/gui/window/run.go @@ -723,7 +723,7 @@ func (r *runner) runFinished(res *engine.Result, runErr, saveErr error, room dis if saved.Missed != nil { // Escaped as the command line escapes it, the path and the system's // sentence both - the second carries the path again (review on #140). - said = append(said, text.InstructionsNotSaved(core.Shown(saved.Missed.Path), core.ShownText(saved.Missed.Err.Error()))) + said = append(said, text.InstructionsNotSaved(core.Shown(saved.Missed.Path), core.ShownText(text.Refusal(saved.Missed.Err, "")))) } r.say(append(said, notesOf(res)...)...) r.toneOfOutcome(res, runErr) @@ -828,7 +828,7 @@ func (r *runner) Settled() { func wholeNumber(setting, field, value string) (int64, error) { n, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64) if err != nil { - return 0, &aboutField{setting: setting, detail: text.NotAWholeNumber(field, value)} + return 0, &aboutField{setting: setting, err: errors.New(text.NotAWholeNumber(field, value))} } return n, nil } @@ -842,12 +842,16 @@ func wholeNumber(setting, field, value string) (int64, error) { // 2026-08-12 - the size field could be marked when a format refused the number // and not when the number was not a number, which is the more common mistake of // the two. +// +// It keeps the refusal it wraps rather than its text, so a window speaking +// another language still finds the sentence inside - see text.Refusal. type aboutField struct { setting string - detail string + err error } -func (e *aboutField) Error() string { return e.detail } +func (e *aboutField) Error() string { return e.err.Error() } +func (e *aboutField) Unwrap() error { return e.err } func (e *aboutField) AboutSetting() string { return e.setting } // saying wraps a refusal from somewhere that does not know which box was read. @@ -860,5 +864,5 @@ func saying(setting string, err error) error { if errors.As(err, &already) && already.AboutSetting() != "" { return err } - return &aboutField{setting: setting, detail: err.Error()} + return &aboutField{setting: setting, err: err} } diff --git a/internal/gui/window/runoutcome.go b/internal/gui/window/runoutcome.go index e195efbb..259367c9 100644 --- a/internal/gui/window/runoutcome.go +++ b/internal/gui/window/runoutcome.go @@ -24,9 +24,21 @@ func outcomeText(res *engine.Result, runErr error) string { return text.Written(written) } +// notesOf is what a run says about its files, in the window's language. A +// note the run did not remember as a sentence - there is none today - is said +// as the manifest holds it. func notesOf(res *engine.Result) []string { if res == nil || res.Manifest == nil { return nil } - return res.Manifest.Notes() + groups := res.Manifest.NoteGroups() + out := make([]string, 0, len(groups)) + for _, g := range groups { + note, known := res.NotesSaid[g.Detail] + if !known { + note = g.AsHeld() + } + out = append(out, text.Sentence(g.Line(note))) + } + return out } diff --git a/internal/gui/window/runrefuse.go b/internal/gui/window/runrefuse.go index 6c5db171..18bdb59c 100644 --- a/internal/gui/window/runrefuse.go +++ b/internal/gui/window/runrefuse.go @@ -49,7 +49,7 @@ func (r *runner) refuse(err error) { } // About the run rather than about one box, or about a setting this // screen does not draw. The foot of the form is where those belong. - loose = append(loose, core.ShownText(one.Error())) + loose = append(loose, core.ShownText(text.Refusal(one, ""))) } if len(loose) > 0 { r.problem.Say(strings.Join(loose, "\n\n")) diff --git a/internal/gui/window/runreport.go b/internal/gui/window/runreport.go index bc76cc07..a04c8456 100644 --- a/internal/gui/window/runreport.go +++ b/internal/gui/window/runreport.go @@ -63,7 +63,7 @@ func progressText(p engine.Progress, elapsed time.Duration) string { } left := time.Duration(float64(elapsed) * float64(p.BytesTotal-p.BytesDone) / float64(p.BytesDone)) - return line + text.TimeLeft(core.Roughly(left)) + return line + text.TimeLeft(text.Roughly(left)) } // saveRecord writes the record of what the run did, and hands back where it diff --git a/internal/gui/window/tools.go b/internal/gui/window/tools.go index 438b3d90..807a6293 100644 --- a/internal/gui/window/tools.go +++ b/internal/gui/window/tools.go @@ -220,7 +220,7 @@ func (t *Tools) Settled() { func (t *Tools) onChosen(choice string) { d, err := tool.Get(t.menu.titles[choice]) if err != nil { - t.fault.Say(core.ShownText(err.Error())) + t.fault.Say(core.ShownText(text.Refusal(err, ""))) return } t.chosen = d @@ -303,7 +303,7 @@ func (t *Tools) refuse(err error) { t.relay() return } - t.fault.Say(core.ShownText(err.Error())) + t.fault.Say(core.ShownText(text.Refusal(err, ""))) } // toolJob is one run of a tool beside the window. diff --git a/internal/gui/window/toolstext.go b/internal/gui/window/toolstext.go index 39f5d0eb..85778d7f 100644 --- a/internal/gui/window/toolstext.go +++ b/internal/gui/window/toolstext.go @@ -24,19 +24,28 @@ import ( func noteLines(d tool.Descriptor, n tool.Noted, most int) []string { says := text.ToolNote(d.ID, n.ID, d.NoteSays(n.ID)) if len(n.Items) == 1 { - return []string{says + " " + core.Shown(n.Items[0])} + return []string{says + " " + itemOf(n, 0)} } out := []string{says} - for i, item := range n.Items { + for i := range n.Items { if most > 0 && i == most { out = append(out, text.ToolMoreItems(len(n.Items)-i, d.ID)) break } - out = append(out, core.Shown(item)) + out = append(out, itemOf(n, i)) } return out } +// itemOf is one item of a note: in the window's words where the tool gave +// some, and as the data it is otherwise. +func itemOf(n tool.Noted, i int) string { + if len(n.Words) == len(n.Items) { + return core.ShownText(text.Sentence(n.Words[i])) + } + return core.Shown(n.Items[i]) +} + // verdictSaid is tool.Verdict.Said in the window's language. func verdictSaid(v tool.Verdict) string { switch { diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go index 40265d65..d8c98047 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -2,7 +2,6 @@ package manifest import ( "encoding/json" - "fmt" "io" "os" "runtime" @@ -426,6 +425,25 @@ const noteExamples = 3 // Names are not sorted here and the count is kept rather than the names, so a // million entry run no longer sorts a million strings to print sixteen lines. func (m *Manifest) Notes() []string { + groups := m.NoteGroups() + out := make([]string, 0, len(groups)) + for _, g := range groups { + out = append(out, g.Line(core.Says("manifest.NoteText", "%s", core.A("Text", g.Detail))).String()) + } + return out +} + +// NoteGroup is one sentence of the notes and the files that carry it, for a +// reader that words the line in its own language - see Line. +type NoteGroup struct { + Detail string + Count int + First []string +} + +// NoteGroups is the notes grouped by their sentence, in the order Notes prints +// them. +func (m *Manifest) NoteGroups() []NoteGroup { groups := noteGroups{byDetail: map[string]*noteGroup{}} for _, f := range m.Files { for _, n := range f.Notes { @@ -434,9 +452,10 @@ func (m *Manifest) Notes() []string { } sort.Strings(groups.order) - out := make([]string, 0, len(groups.order)) + out := make([]NoteGroup, 0, len(groups.order)) for _, detail := range groups.order { - out = append(out, groups.byDetail[detail].line(detail)) + g := groups.byDetail[detail] + out = append(out, NoteGroup{Detail: detail, Count: g.count, First: g.first}) } return out } @@ -475,23 +494,33 @@ func (n *noteGroups) add(detail, name string) { } } +// AsHeld is the group's note said as the manifest holds it, for a window +// given a note the run did not keep as a sentence. There is none today, and +// one would reach a window in English rather than not at all. +func (g NoteGroup) AsHeld() core.Said { + return core.Says("manifest.NoteText", "%s", core.A("Text", g.Detail)) +} + // line renders one group for a person to read. // // A group of one keeps the shape it always had - the file name in front - so // the common case of one file with something to say about it does not get worse // to make the large case better. -func (g *noteGroup) line(detail string) string { - if g.count == 1 { - return fmt.Sprintf("%s: %s", core.Shown(g.first[0]), detail) +// +// The note is handed in as a sentence, so the command line gives the English +// the manifest holds and a window gives the same note in its own language. +func (g NoteGroup) Line(note core.Said) core.Said { + if g.Count == 1 { + return core.Says("manifest.NoteOne", "%s: %s", core.A("File", core.Shown(g.First[0])), core.A("Note", note)) } - named := strings.Join(core.ShownEach(g.first), ", ") - if hidden := g.count - len(g.first); hidden > 0 { - return fmt.Sprintf("%s: %s Named: %s. %s not named here.", - core.Count(g.count, "file", "files"), detail, named, - core.Count(hidden, "file", "files")) + named := strings.Join(core.ShownEach(g.First), ", ") + files := core.SaysN("manifest.Files", "%d file", "%d files", core.A("Count", g.Count)) + if hidden := g.Count - len(g.First); hidden > 0 { + return core.Says("manifest.NoteMany", "%s: %s Named: %s. %s not named here.", + core.A("Files", files), core.A("Note", note), core.A("Named", named), + core.A("Hidden", core.SaysN("manifest.Files", "%d file", "%d files", core.A("Count", hidden)))) } - return fmt.Sprintf("%s: %s Named: %s.", - core.Count(g.count, "file", "files"), detail, named) + return core.Says("manifest.NoteSeveral", "%s: %s Named: %s.", core.A("Files", files), core.A("Note", note), core.A("Named", named)) } // Encode renders the manifest as JSON. @@ -504,11 +533,14 @@ func (m *Manifest) Encode(w io.Writer) error { // SchemaError is a manifest this build cannot read. type SchemaError struct { Path string - Detail string + Detail core.Said } -func (e *SchemaError) Error() string { - return fmt.Sprintf("%s cannot be read as a manifest: %s", e.Path, e.Detail) +func (e *SchemaError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. +func (e *SchemaError) Said() core.Said { + return core.Says("manifest.Unreadable", "%s cannot be read as a manifest: %s", core.A("Path", e.Path), core.A("Detail", e.Detail)) } // MaxBytes is the largest manifest this build will read. @@ -596,10 +628,13 @@ type TooLargeError struct { Bytes int64 } -func (e *TooLargeError) Error() string { - return fmt.Sprintf( +func (e *TooLargeError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. +func (e *TooLargeError) Said() core.Said { + return core.Says("manifest.TooLarge", "%s is %d B and the limit is %d B. A manifest is read into memory to be compared against a directory, so an unbounded one is a way to exhaust it. Check that this is a manifest this tool wrote, or split the run it describes", - e.Path, e.Bytes, MaxBytes) + core.A("Path", e.Path), core.A("Bytes", e.Bytes), core.A("Limit", int64(MaxBytes))) } // Load reads a manifest written by an earlier run. @@ -619,16 +654,14 @@ func Load(path string) (*Manifest, error) { } var m Manifest if err := json.Unmarshal(raw, &m); err != nil { - return nil, &SchemaError{Path: path, Detail: err.Error()} + return nil, &SchemaError{Path: path, Detail: core.SaidOf(err)} } if m.ManifestVersion == "" { - return nil, &SchemaError{Path: path, Detail: "it carries no manifest_version, so it is not a manifest this tool wrote"} + return nil, &SchemaError{Path: path, Detail: core.Says("manifest.ItCarriesNoManifestVersionSo", "it carries no manifest_version, so it is not a manifest this tool wrote")} } got, want := major(m.ManifestVersion), major(Version) if got != want { - return nil, &SchemaError{Path: path, Detail: fmt.Sprintf( - "it is schema version %s and this build reads %s. Use the version of tfg that wrote it", - m.ManifestVersion, Version)} + return nil, &SchemaError{Path: path, Detail: core.Says("manifest.ItIsSchemaVersionAndThis", "it is schema version %s and this build reads %s. Use the version of tfg that wrote it", core.A("ManifestVersion", m.ManifestVersion), core.A("Version", Version))} } if err := checkPaths(path, &m); err != nil { return nil, err @@ -664,22 +697,18 @@ func checkPaths(path string, m *Manifest) error { if problem == "" { continue } - return &SchemaError{Path: path, Detail: fmt.Sprintf( - "entry %d has the path %q, which lands outside the directory the manifest describes - %s. "+ - "This tool never reads or removes anything outside that directory, so a manifest that asks it to is one it will not act on. "+ - "Use the manifest the run actually wrote, or correct the path to one inside the directory", - i+1, f.Path, problem)} + return &SchemaError{Path: path, Detail: core.Says("manifest.EntryHasThePathWhichLands", "entry %d has the path %q, which lands outside the directory the manifest describes - %s. "+ + "This tool never reads or removes anything outside that directory, so a manifest that asks it to is one it will not act on. "+ + "Use the manifest the run actually wrote, or correct the path to one inside the directory", core.A("I", i+1), core.A("Path", f.Path), core.A("Problem", problem))} } // The instructions are a name beside the manifest and nothing else. // cleanup --with-manifest removes the file this names, so a manifest // carrying a path here would be a manifest that removes something the run // never wrote. if name := m.Run.Instructions; name != "" && !isInstructionsName(name) { - return &SchemaError{Path: path, Detail: fmt.Sprintf( - "run.instructions is %q, and it can only be the name of a file beside the manifest ending in %s. "+ - "This tool removes that file with the manifest, so it will not act on one that names anything else. "+ - "Use the manifest the run actually wrote, or remove the key", - name, instructionsSuffix)} + return &SchemaError{Path: path, Detail: core.Says("manifest.RunInstructionsIsAndItCan", "run.instructions is %q, and it can only be the name of a file beside the manifest ending in %s. "+ + "This tool removes that file with the manifest, so it will not act on one that names anything else. "+ + "Use the manifest the run actually wrote, or remove the key", core.A("Name", name), core.A("InstructionsSuffix", instructionsSuffix))} } return nil } diff --git a/internal/manifest/reservation.go b/internal/manifest/reservation.go index e548ce0c..cc4369f7 100644 --- a/internal/manifest/reservation.go +++ b/internal/manifest/reservation.go @@ -107,7 +107,7 @@ func Claim(path string) (*Reservation, error) { // holds the file this run created. A save can be asked once. func (r *Reservation) Save(m *Manifest) error { if r.used { - return fmt.Errorf("the reservation of %s was already used or given back", core.Shown(r.final)) + return core.Defect(fmt.Errorf("the reservation of %s was already used or given back", core.Shown(r.final))) } r.used = true f, err := core.OpenOwn(r.tmp, r.own) diff --git a/internal/preset/build.go b/internal/preset/build.go index 6b448ecd..dc2e327a 100644 --- a/internal/preset/build.go +++ b/internal/preset/build.go @@ -7,6 +7,8 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/format" "github.com/donislawdev/TestingFilesGenerator/internal/recipe" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // The machinery every preset shares, so that a preset file holds its question @@ -33,10 +35,10 @@ type commaList struct { // label on the field. preset, param string // empty is the reason for a list that named nothing at all. - empty string - // check judges one item and answers with why it is not allowed, or "" when - // it is. It receives the item trimmed, never blank. - check func(item string) string + empty core.Said + // check judges one item and answers with why it is not allowed, or nothing + // when it is. It receives the item trimmed, never blank. + check func(item string) core.Said // same is what makes two items one, for the duplicate that would otherwise // reach the recipe as two targets of one id. Nil compares the items // themselves. size-boundaries compares byte counts instead, so that 1024 @@ -57,7 +59,21 @@ type commaList struct { // flattens both into "the same item" makes the message vaguer for everyone // in order to save a line. The text rules in CLAUDE.md ask for the name the // reader sees, not the name the code uses. - duplicate func(first string) string + duplicate func(first string) core.Said +} + +// paramCause is a refusal of one parameter's value from below, with the +// parameter named in front of it. +func paramCause(param string, err error) error { + return core.Refuse(core.Says("preset.ParamCause", "%s: %w", core.A("Param", core.LabelTerm(param)), core.A("Cause", err))) +} + +// limitUnusable is a limit whose text cannot become part of a file name. +func limitUnusable(param, bad string) error { + return core.Refuse(core.Says("preset.LimitUnusable", + "%s: it holds %s, and a limit is written with digits, letters and a dot - "+ + "such as 10mb, 512 or 1.5gb. Its text becomes part of every file name", + core.A("Param", core.LabelTerm(param)), core.A("Bad", bad))) } // refuse is the shape a bad value in a preset parameter takes. @@ -67,7 +83,7 @@ type commaList struct { // plain error falls through the classifier to RUNTIME, so "--spread notasize" // told CI this program had a bug instead of saying the value was wrong - // measured on 2026-08-05, exit 1. -func (l commaList) refuse(value, reason string) error { +func (l commaList) refuse(value string, reason core.Said) error { return &format.PropertyValueError{ Format: l.preset, Key: l.param, Value: value, Reason: reason, } @@ -92,7 +108,7 @@ func (l commaList) parse(raw string) ([]string, error) { } // Judged as it was typed, so the message quotes what the reader can see // in their own command line. - if bad := l.check(typed); bad != "" { + if bad := l.check(typed); !bad.IsZero() { return nil, l.refuse(typed, bad) } item := typed @@ -121,12 +137,11 @@ func (l commaList) parse(raw string) ([]string, error) { // pulled from CLAUDE.md the hard way: a list copied by hand goes stale green. // The refusal names what this build has, because a person who typed heic has no // other way to find out what it does have. -func knownFormat(item string) string { +func knownFormat(item string) core.Said { if _, err := format.Get(strings.ToLower(item)); err != nil { - return fmt.Sprintf("this build has no format called that. It has: %s", - strings.Join(format.IDs(), ", ")) + return core.Says("preset.ThisBuildHasNoFormatCalled", "this build has no format called that. It has: %s", core.A("Join", strings.Join(format.IDs(), ", "))) } - return "" + return core.Said{} } // lower is the keep for a list of names the registry spells in lower case. diff --git a/internal/preset/emptyandminimal.go b/internal/preset/emptyandminimal.go index b2815405..762a8fb8 100644 --- a/internal/preset/emptyandminimal.go +++ b/internal/preset/emptyandminimal.go @@ -7,6 +7,8 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/format" "github.com/donislawdev/TestingFilesGenerator/internal/recipe" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) const ( @@ -72,16 +74,14 @@ func init() { var formatsList = commaList{ preset: minimalID, param: "formats", - empty: "no formats were given, so there is nothing to build the set from", + empty: core.Says("preset.NoFormatsWereGivenSoThere", "no formats were given, so there is nothing to build the set from"), check: checkSetFormat, keep: lower, duplicate: repeatedFormat, } -func repeatedFormat(first string) string { - return fmt.Sprintf( - "it is the same format as %q and the set would hold that file twice. Every format appears once, because each one stands for one path through your reader", - first) +func repeatedFormat(first string) core.Said { + return core.Says("preset.ItIsTheSameFormatAs", "it is the same format as %q and the set would hold that file twice. Every format appears once, because each one stands for one path through your reader", core.A("First", first)) } // checkSetFormat lets the keyword through and asks the registry about the rest. @@ -91,9 +91,9 @@ func repeatedFormat(first string) string { // wrong for "all," - a trailing comma leaves one item, the keyword is alone, // and a refusal saying it cannot stand beside a named format would be about a // format nobody wrote. -func checkSetFormat(item string) string { +func checkSetFormat(item string) core.Said { if strings.EqualFold(item, everyFormat) { - return "" + return core.Said{} } return knownFormat(item) } @@ -159,9 +159,7 @@ func spelledOut(ids []string) ([]string, error) { } for _, id := range ids { if id == everyFormat { - return nil, formatsList.refuse(everyFormat, fmt.Sprintf( - "%q already means every format, so it cannot stand beside a named one. Write it on its own, or name only the formats you want", - everyFormat)) + return nil, formatsList.refuse(everyFormat, core.Says("preset.AlreadyMeansEveryFormatSoIt", "%q already means every format, so it cannot stand beside a named one. Write it on its own, or name only the formats you want", core.A("EveryFormat", everyFormat))) } } return ids, nil @@ -276,7 +274,7 @@ func smallest(d format.Descriptor) int64 { // Nothing is said about the other direction. A set that is ALL empty files // cannot happen, because every format has a smallest legal file and this set // always holds it. -func saidAboutTheMinimalSet(args Args) []string { +func saidAboutTheMinimalSet(args Args) []core.Said { descs, err := chosenFormats(args["formats"]) if err != nil { // Expand is about to refuse these same values with a message that names @@ -288,9 +286,9 @@ func saidAboutTheMinimalSet(args Args) []string { return nil } } - return []string{fmt.Sprintf( + return []core.Said{core.Says("preset.NoEmptyForm", "no format in this set has a legal empty form, so the set holds no empty files - only the smallest valid one of each. The formats that go down to nought bytes in this build: %s.", - strings.Join(zeroCapable(), ", "))} + core.A("Formats", strings.Join(zeroCapable(), ", ")))} } // zeroCapable is every format whose smallest legal file is nought bytes. diff --git a/internal/preset/expansion.go b/internal/preset/expansion.go index cd2417bf..c4008565 100644 --- a/internal/preset/expansion.go +++ b/internal/preset/expansion.go @@ -1,5 +1,9 @@ package preset +import ( + "github.com/donislawdev/TestingFilesGenerator/internal/core" +) + import "sort" // Expansion is one preset settled on its parameters and turned into the recipe @@ -58,7 +62,7 @@ func (e *Expansion) Notes() []string { spoken := e.Spoken() out := make([]string, 0, len(spoken)) for _, n := range spoken { - out = append(out, n.Said) + out = append(out, n.Said.String()) } return out } @@ -70,7 +74,7 @@ func (e *Expansion) Notes() []string { // there is nothing fixed to translate them from. type Note struct { About string - Said string + Said core.Said } // Spoken is Notes with what each one is about, in the same order. @@ -78,7 +82,9 @@ func (e *Expansion) Spoken() []Note { var out []Note for _, name := range e.Defaulted { if said := e.Preset.SaidWhenDefaulted[name]; said != "" { - out = append(out, Note{About: name, Said: said}) + // A registry word rather than a sentence of the engine: a window + // finds it by the preset and the parameter it is about. + out = append(out, Note{About: name, Said: core.Says("preset.Defaulted", "%s", core.A("Text", said))}) } } // What those values then laid out, after what we invented, because a diff --git a/internal/preset/filenamehandling.go b/internal/preset/filenamehandling.go index 80d7bfa3..112dabc5 100644 --- a/internal/preset/filenamehandling.go +++ b/internal/preset/filenamehandling.go @@ -1,12 +1,13 @@ package preset import ( - "fmt" "strings" "golang.org/x/text/unicode/norm" "github.com/donislawdev/TestingFilesGenerator/internal/format" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) const ( @@ -236,7 +237,7 @@ func (c nameCase) name(desc format.Descriptor) (string, error) { copies := (c.bytes - len(ext)) / len(c.fill) if copies < 1 { return "", &format.PropertyValueError{Format: namesID, Key: "format", Value: desc.ID, - Reason: fmt.Sprintf("its extension %s is too long for a name of %d bytes, which the file %s is about. Choose a format with a shorter extension", ext, c.bytes, c.id)} + Reason: core.Says("preset.ItsExtensionIsTooLongFor", "its extension %s is too long for a name of %d bytes, which the file %s is about. Choose a format with a shorter extension", core.A("Ext", ext), core.A("Bytes", c.bytes), core.A("ID", c.id))} } stem = strings.Repeat(c.fill, copies) } diff --git a/internal/preset/limitset.go b/internal/preset/limitset.go index 7380f7f4..ab5b610d 100644 --- a/internal/preset/limitset.go +++ b/internal/preset/limitset.go @@ -89,10 +89,10 @@ func (s limitSet) reachable(set []step) error { if one.size >= floor { continue } - what := fmt.Sprintf("%s would be %d B and the smallest %s this build makes is %d B", - one.id, one.size, strings.ToUpper(s.desc.ID), floor) + what := core.Says("preset.StepBelowFloor", "%s would be %d B and the smallest %s this build makes is %d B", + core.A("Step", one.id), core.A("Size", one.size), core.A("Format", strings.ToUpper(s.desc.ID)), core.A("Floor", floor)) if one.size <= 0 { - what = fmt.Sprintf("%s would be %d B, and a file cannot be smaller than nothing", one.id, one.size) + what = core.Says("preset.StepBelowZero", "%s would be %d B, and a file cannot be smaller than nothing", core.A("Step", one.id), core.A("Size", one.size)) } return &ImpossibleError{ Preset: s.preset, @@ -102,15 +102,7 @@ func (s limitSet) reachable(set []step) error { // only stand beside one box. Setting: s.setting, Detail: what, - Hint: fmt.Sprintf( - // The settings are named without a leading dash on purpose. This - // sentence is built in the engine and both surfaces show it word - // for word, so a spelling only one of them has sends the other's - // reader translating: the window labels these fields "limit" and - // "spread", and there is no "--limit" anywhere on it. Seen on - // screen 2026-08-11, O79. - "Raise the {setting} above %d B, narrow the spread, or choose a format with a smaller minimum. The {setting} asked for was %d B.", - floor+deepest(set, s.limit), s.limit), + Hint: core.Says("preset.RaiseTheSettingAboveBNarrow", "Raise the {setting} above %d B, narrow the spread, or choose a format with a smaller minimum. The {setting} asked for was %d B.", core.A("Floor", floor+deepest(set, s.limit)), core.A("Limit", s.limit)), } } return nil diff --git a/internal/preset/preset.go b/internal/preset/preset.go index 64046bd8..534ace25 100644 --- a/internal/preset/preset.go +++ b/internal/preset/preset.go @@ -12,6 +12,7 @@ package preset import ( + "errors" "fmt" "sort" "strings" @@ -123,7 +124,7 @@ type Preset struct { // wiring to remember. // // Nil for a preset whose set is the same shape whatever it is given. - Says func(Args) []string + Says func(Args) []core.Said // Expand builds the recipe. It returns source rather than a structure, so // what a run consumes is what eject prints. @@ -173,7 +174,7 @@ func (p Preset) Check(args Args) error { return &UnknownParameterError{Preset: p.ID, Name: name, Known: p.ParameterNames()} } if raw := args[name]; raw != "" { - if bad := param.Allows(raw); bad != "" { + if bad := param.Allows(raw); !bad.IsZero() { // The remedy comes from the declaration, as it does for a // format's property. Error leaves it out, so the command // line reads as it always did, and a form gets the fourth @@ -265,12 +266,16 @@ type UnknownParameterError struct { Known []string } -func (e *UnknownParameterError) Error() string { +func (e *UnknownParameterError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. +func (e *UnknownParameterError) Said() core.Said { if len(e.Known) == 0 { - return fmt.Sprintf("the preset %s takes no parameters, so %q is not one of them", e.Preset, e.Name) + return core.Says("preset.TakesNoParameters", "the preset %s takes no parameters, so %q is not one of them", + core.A("Preset", e.Preset), core.A("Name", e.Name)) } - return fmt.Sprintf("the preset %s does not have a parameter called %q. It takes: %s", - e.Preset, e.Name, strings.Join(e.Known, ", ")) + return core.Says("preset.NoSuchParameter", "the preset %s does not have a parameter called %q. It takes: %s", + core.A("Preset", e.Preset), core.A("Name", e.Name), core.A("Known", strings.Join(e.Known, ", "))) } // UnknownPresetError is a request for a preset nobody registered. @@ -279,11 +284,14 @@ type UnknownPresetError struct { Known []string } -func (e *UnknownPresetError) Error() string { +func (e *UnknownPresetError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. +func (e *UnknownPresetError) Said() core.Said { if len(e.Known) == 0 { - return fmt.Sprintf("unknown preset %q, and this build registers none", e.ID) + return core.Says("preset.UnknownNone", "unknown preset %q, and this build registers none", core.A("Preset", e.ID)) } - return fmt.Sprintf("unknown preset %q. This build has: %s", e.ID, strings.Join(e.Known, ", ")) + return core.Says("preset.Unknown", "unknown preset %q. This build has: %s", core.A("Preset", e.ID), core.A("Known", strings.Join(e.Known, ", "))) } // ImpossibleError is parameters that describe a set this build cannot produce. @@ -293,8 +301,8 @@ func (e *UnknownPresetError) Error() string { // are missing are the ones the run was about. type ImpossibleError struct { Preset string - Detail string - Hint string + Detail core.Said + Hint core.Said // Setting is which parameter of the preset the refusal is about, where it // is about one. Named the way the parameter is declared, which is what @@ -306,13 +314,21 @@ type ImpossibleError struct { // What happened, why, and what to do instead - the three parts a report keeps // apart, on the refusal a preset can produce. Error still assembles the one // sentence the command line has always printed. -func (e *ImpossibleError) What() string { - return fmt.Sprintf("the preset %s cannot build this set", e.Preset) +func (e *ImpossibleError) What() string { return e.what().String() } + +func (e *ImpossibleError) what() core.Said { + return core.Says("preset.CannotBuild", "the preset %s cannot build this set", core.A("Preset", e.Preset)) } -func (e *ImpossibleError) Why() string { return e.Detail } +func (e *ImpossibleError) Why() string { return e.Detail.String() } -func (e *ImpossibleError) Instead() string { return e.Hint } +func (e *ImpossibleError) Instead() string { return e.Hint.String() } + +// Parts is what happened, why and what to do instead, for a reader that lays +// them out apart and in its own language. +func (e *ImpossibleError) Parts() (what, why, instead core.Said) { + return e.what(), e.Detail, e.Hint +} func (e *ImpossibleError) Error() string { return e.InTheWordsOf(e.Setting) @@ -325,16 +341,23 @@ func (e *ImpossibleError) InTheWordsOf(name string) string { if name == "" { name = e.Setting } - said := fmt.Sprintf("the preset %s cannot build this set - %s", e.Preset, e.Detail) - // A refusal that came up from the registry already ends with what to do - // about it, and a second sentence after that one is worse than no second - // sentence: "Ask for fewer rows or fewer columns. Ask for a set the rows - // can carry" is the tool saying the same thing twice and vaguer the second - // time. Seen on 2026-09-22 while the tabular set was being wired up. - if e.Hint == "" { - return core.InTheWordsOf(said+".", name) + return core.InTheWordsOf(e.Said().String(), name) +} + +// Said is the whole refusal, for a window that says it in its own language, +// with the setting slot left for the window to fill. +// +// A refusal that came up from the registry already ends with what to do +// about it, and a second sentence after that one is worse than no second +// sentence: "Ask for fewer rows or fewer columns. Ask for a set the rows +// can carry" is the tool saying the same thing twice and vaguer the second +// time. Seen on 2026-09-22 while the tabular set was being wired up. +func (e *ImpossibleError) Said() core.Said { + if e.Hint.IsZero() { + return core.Says("preset.ImpossibleWhy", "the preset %s cannot build this set - %s.", core.A("Preset", e.Preset), core.A("Why", e.Detail)) } - return core.InTheWordsOf(said+". "+e.Hint, name) + return core.Says("preset.ImpossibleWhyFix", "the preset %s cannot build this set - %s. %s", + core.A("Preset", e.Preset), core.A("Why", e.Detail), core.A("Fix", e.Hint)) } // AboutSetting lets a window put this message beside the box that caused it, @@ -343,6 +366,19 @@ func (e *ImpossibleError) InTheWordsOf(name string) string { // other field. Empty means the refusal is about the set rather than one value. func (e *ImpossibleError) AboutSetting() string { return e.Setting } +// withoutFormat is a refusal from planning one file of a set, without the +// format's name in front - the set names the file another way. A value the +// declaration refuses says itself in parts, and anything else is said as it +// came. +func withoutFormat(err error, id string) core.Said { + bare := strings.TrimPrefix(err.Error(), id+": ") + var value *format.PropertyValueError + if errors.As(err, &value) && value.Bare().String() == bare { + return value.Bare() + } + return core.Says("preset.Refused", "%s", core.A("Text", bare)) +} + // registry holds what this build knows, written at init and read after. // // No lock, and that is deliberate rather than forgotten. The format registry diff --git a/internal/preset/read.go b/internal/preset/read.go index f8fee07f..60c31cfc 100644 --- a/internal/preset/read.go +++ b/internal/preset/read.go @@ -2,11 +2,12 @@ package preset import ( "errors" - "fmt" "strings" "github.com/donislawdev/TestingFilesGenerator/internal/format" "github.com/donislawdev/TestingFilesGenerator/internal/recipe" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // Read is a recipe file as a run sees it: the recipe, and the preset it was @@ -79,7 +80,7 @@ func ReadRecipe(src []byte, name string) (*Read, error) { // and the refusal has to say so and carry that address - the window marks a // box by it, and the command line's list of problems names it. func aboutTheFile(name, id string, err error) error { - problem := recipe.Problem{At: recipe.KeyExtends, What: err.Error()} + problem := recipe.Problem{At: recipe.KeyExtends, What: core.SaidOf(err)} var unknownPreset *UnknownPresetError var unknownParameter *UnknownParameterError @@ -87,43 +88,41 @@ func aboutTheFile(name, id string, err error) error { var value *format.PropertyValueError var about interface{ AboutSetting() string } var three interface { - What() string - Why() string - Instead() string + Parts() (what, why, instead core.Said) } switch { case errors.As(err, &unknownPreset): - problem.What = fmt.Sprintf("extends names preset:%s, which this build does not have", unknownPreset.ID) - problem.Why = "a recipe can build only on a preset the tool it runs on knows" - problem.Fix = "run \"tfg preset list\" for the ids this build has" + problem.What = core.Says("preset.ExtendsNamesPresetWhichThisBuild", "extends names preset:%s, which this build does not have", core.A("ID", unknownPreset.ID)) + problem.Why = core.Says("preset.ARecipeCanBuildOnlyOn", "a recipe can build only on a preset the tool it runs on knows") + problem.Fix = core.Says("preset.RunTfgPresetListForThe", "run \"tfg preset list\" for the ids this build has") if len(unknownPreset.Known) > 0 { - problem.Fix = fmt.Sprintf("this build has: %s", strings.Join(unknownPreset.Known, ", ")) + problem.Fix = core.Says("preset.ThisBuildHas", "this build has: %s", core.A("Join", strings.Join(unknownPreset.Known, ", "))) } case errors.As(err, &unknownParameter): problem.At = recipe.KeyWith + "." + unknownParameter.Name - problem.What = fmt.Sprintf("with names %s, which the preset %s does not take", unknownParameter.Name, unknownParameter.Preset) - problem.Why = "with fills the parameters the preset declares, and this is not one of them" - problem.Fix = "remove the line" + problem.What = core.Says("preset.WithNamesWhichThePresetDoes", "with names %s, which the preset %s does not take", core.A("Name", unknownParameter.Name), core.A("Preset", unknownParameter.Preset)) + problem.Why = core.Says("preset.WithFillsTheParametersThePreset", "with fills the parameters the preset declares, and this is not one of them") + problem.Fix = core.Says("preset.RemoveTheLine", "remove the line") if len(unknownParameter.Known) > 0 { - problem.Fix = fmt.Sprintf("the preset takes: %s", strings.Join(unknownParameter.Known, ", ")) + problem.Fix = core.Says("preset.ThePresetTakes", "the preset takes: %s", core.A("Join", strings.Join(unknownParameter.Known, ", "))) } case errors.As(err, &value): // A value the parameter refuses. Named by its line in the file // rather than by the preset, which is how the command line's // --preset path words the same refusal - there the flag is the line. problem.At = recipe.KeyWith + "." + value.Key - problem.What = fmt.Sprintf("with.%s cannot be %q", value.Key, value.Value) - problem.Why = value.Why() - problem.Fix = value.Instead() + problem.What = core.Says("preset.WithCannotBe", "with.%s cannot be %q", core.A("Key", value.Key), core.A("Value", value.Value)) + problem.Why = value.Reason + problem.Fix = value.Remedy case errors.As(err, &unknownFormat): // The one global a preset reads. The format registry refuses it in // its own words, which name no line, so the line is named here. problem.At = recipe.KeyWith + ".format" - problem.What = fmt.Sprintf("with.format names %q, which this build does not have", unknownFormat.ID) - problem.Why = "the preset gives every file it builds this format, so it has to be one the build can write" - problem.Fix = "run \"tfg formats\" for the list" + problem.What = core.Says("preset.WithFormatNamesWhichThisBuild", "with.format names %q, which this build does not have", core.A("ID", unknownFormat.ID)) + problem.Why = core.Says("preset.ThePresetGivesEveryFileIt", "the preset gives every file it builds this format, so it has to be one the build can write") + problem.Fix = core.Says("preset.RunTfgFormatsForTheList", "run \"tfg formats\" for the list") if len(unknownFormat.Known) > 0 { - problem.Fix = fmt.Sprintf("this build has: %s", strings.Join(unknownFormat.Known, ", ")) + problem.Fix = core.Says("preset.ThisBuildHas", "this build has: %s", core.A("Join", strings.Join(unknownFormat.Known, ", "))) } case errors.As(err, &three) && errors.As(err, &about) && about.AboutSetting() != "": // A set the preset cannot build from these values. It comes in three @@ -131,15 +130,16 @@ func aboutTheFile(name, id string, err error) error { // the parameter's line. The hint ends in a full stop of its own and // the problem adds one, so the stop comes off here. problem.At = recipe.KeyWith + "." + about.AboutSetting() - problem.What = three.What() - problem.Why = strings.TrimSuffix(three.Why(), ".") - problem.Fix = strings.TrimSuffix(three.Instead(), ".") + what, why, instead := three.Parts() + problem.What = what + problem.Why = why.WithoutFullStop() + problem.Fix = instead.WithoutFullStop() default: // A refusal of a shape this does not know still arrives whole, at // the extends line, with the two parts it lacks filled in rather than // printed as a bare dash and a bare stop. - problem.Why = "the preset refused what the recipe gave it" - problem.Fix = "run \"tfg preset show " + id + "\" for what it takes" + problem.Why = core.Says("preset.ThePresetRefusedWhatTheRecipe", "the preset refused what the recipe gave it") + problem.Fix = core.Says("preset.RunPresetShow", "run \"tfg preset show %s\" for what it takes", core.A("Preset", id)) } return &recipe.ValidationError{Name: name, Problems: []recipe.Problem{problem}} } diff --git a/internal/preset/sizeboundaries.go b/internal/preset/sizeboundaries.go index e4672dd0..5afddd0d 100644 --- a/internal/preset/sizeboundaries.go +++ b/internal/preset/sizeboundaries.go @@ -70,26 +70,24 @@ func init() { var spreadList = commaList{ preset: boundariesID, param: "spread", - empty: "no distances were given, so there is nothing either side of the limit", + empty: core.Says("preset.NoDistancesWereGivenSoThere", "no distances were given, so there is nothing either side of the limit"), check: checkDistance, same: sizeKey, keep: lower, duplicate: repeatedDistance, } -func repeatedDistance(first string) string { - return fmt.Sprintf( - "it is the same distance as %q and the set would hold that step twice. Every distance has to be different, because each one names one file either side of the limit", - first) +func repeatedDistance(first string) core.Said { + return core.Says("preset.ItIsTheSameDistanceAs", "it is the same distance as %q and the set would hold that step twice. Every distance has to be different, because each one names one file either side of the limit", core.A("First", first)) } // badSpread is a value the spread parameter does not accept. -func badSpread(value, reason string) error { +func badSpread(value string, reason core.Said) error { return spreadList.refuse(value, reason) } // checkDistance answers why a piece of the spread is not a distance. -func checkDistance(piece string) string { +func checkDistance(piece string) core.Said { // The text of a distance becomes the id of a target and the name of a // file, so it has to be made of what a size is made of and nothing else. // Found by fuzzing on 2026-08-05: "1\rB" parses as one byte, because the @@ -97,17 +95,16 @@ func checkDistance(piece string) string { // and the character then reached the recipe source raw and broke the // document. if bad := firstUnusable(piece); bad != "" { - return fmt.Sprintf( - "it holds %s, and a distance is written with digits, letters and a dot - such as 1kb, 512 or 1.5mb. Its text becomes the name of a file", bad) + return core.Says("preset.ItHoldsAndADistanceIs", "it holds %s, and a distance is written with digits, letters and a dot - such as 1kb, 512 or 1.5mb. Its text becomes the name of a file", core.A("Bad", bad)) } n, err := core.ParseSize(piece) if err != nil { - return err.Error() + return core.SaidOf(err) } if n <= 0 { - return "a distance from the limit has to be more than nothing" + return core.Says("preset.ADistanceFromTheLimitHas", "a distance from the limit has to be more than nothing") } - return "" + return core.Said{} } // sizeKey is what makes two distances the same one, for the duplicate check. @@ -145,7 +142,7 @@ func parseSpread(raw string) ([]offset, error) { // checkDistance has already refused anything that would fail here. n, err := core.ParseSize(piece) if err != nil { - return nil, badSpread(piece, err.Error()) + return nil, badSpread(piece, core.SaidOf(err)) } out = append(out, offset{text: piece, bytes: n}) } @@ -155,7 +152,7 @@ func parseSpread(raw string) ([]offset, error) { func expandSizeBoundaries(args Args) ([]byte, error) { limit, err := core.ParseSize(args["limit"]) if err != nil { - return nil, fmt.Errorf("limit: %w", err) + return nil, paramCause("limit", err) } // The limit leads every file name, so two sets built around different // limits cannot be told apart only by opening the files. Reported from use @@ -168,9 +165,7 @@ func expandSizeBoundaries(args Args) ([]byte, error) { // source once. limitText := strings.TrimSpace(args["limit"]) if bad := firstUnusable(limitText); bad != "" { - return nil, fmt.Errorf( - "limit: it holds %s, and a limit is written with digits, letters and a dot - "+ - "such as 10mb, 512 or 1.5gb. Its text becomes part of every file name", bad) + return nil, limitUnusable("limit", bad) } spread, err := parseSpread(args["spread"]) if err != nil { diff --git a/internal/preset/tabularimport.go b/internal/preset/tabularimport.go index 45429e90..32767331 100644 --- a/internal/preset/tabularimport.go +++ b/internal/preset/tabularimport.go @@ -281,7 +281,7 @@ func refusedTable(f setFile) error { // No hint of our own. The sheet's refusal ends with what to do about it // and says it better than a general sentence could, because it knows // which of the two counts it was and by how much. - Detail: strings.TrimPrefix(err.Error(), f.desc.ID+": "), + Detail: withoutFormat(err, f.desc.ID), } } diff --git a/internal/preset/textencoding.go b/internal/preset/textencoding.go index fe605ccf..ac6d08c7 100644 --- a/internal/preset/textencoding.go +++ b/internal/preset/textencoding.go @@ -174,7 +174,7 @@ func declared(desc format.Descriptor, name string) (format.Property, bool) { // // Which cells those are is asked of the format rather than written here. The // rule belongs to XML today and the next text format may have its own. -func encodingCells() (files []textFile, left []string) { +func encodingCells() (files []textFile, left []core.Said) { for _, desc := range carrying(textenc.Setting) { kept, dropped := cellsOfFormat(desc) files = append(files, kept...) @@ -184,7 +184,7 @@ func encodingCells() (files []textFile, left []string) { } // cellsOfFormat is the encoding half for one format. -func cellsOfFormat(desc format.Descriptor) (files []textFile, left []string) { +func cellsOfFormat(desc format.Descriptor) (files []textFile, left []core.Said) { enc, _ := declared(desc, textenc.Setting) for _, name := range enc.Choices { kept, dropped := cellsOfEncoding(desc, name) @@ -195,7 +195,7 @@ func cellsOfFormat(desc format.Descriptor) (files []textFile, left []string) { } // cellsOfEncoding is one format in one encoding, with and without a mark. -func cellsOfEncoding(desc format.Descriptor, encoding string) (files []textFile, left []string) { +func cellsOfEncoding(desc format.Descriptor, encoding string) (files []textFile, left []core.Said) { for _, mark := range marks(desc) { props := map[string]string{textenc.Setting: encoding} label := encoding @@ -205,8 +205,8 @@ func cellsOfEncoding(desc format.Descriptor, encoding string) (files []textFile, if mark == "true" { label += "_bom" } - if why := refusedOutright(desc, props); why != "" { - left = append(left, fmt.Sprintf("%s as %s (%s)", desc.ID, label, why)) + if why := refusedOutright(desc, props); !why.IsZero() { + left = append(left, core.Says("preset.LeftOutAs", "%s as %s (%s)", core.A("Format", desc.ID), core.A("As", label), core.A("Why", why))) continue } files = append(files, textFile{ @@ -285,7 +285,7 @@ func lineEndingCells() []textFile { // The format answers rather than this file. Asked at the size the format itself // names as its smallest for these settings, so a refusal that comes back is // about the settings and not about the room they need. -func refusedOutright(desc format.Descriptor, props map[string]string) string { +func refusedOutright(desc format.Descriptor, props map[string]string) core.Said { r := format.Request{Label: true, Properties: props} r.Bytes = format.SmallestRemembered(desc, r) _, err := desc.Generator.Plan(r) @@ -293,7 +293,7 @@ func refusedOutright(desc format.Descriptor, props map[string]string) string { if errors.As(err, &bad) { return bad.Reason } - return "" + return core.Said{} } // evenEnough refuses a size no file of this set could have. @@ -317,9 +317,8 @@ func evenEnough(size int64) error { return &ImpossibleError{ Preset: encodingID, Setting: sampleParam, - Detail: fmt.Sprintf("the set holds files in %s, and %s", name, below.Reason), - Hint: fmt.Sprintf("Set the {setting} to %d B or %d B.", - size-1, below.Minimum), + Detail: core.Says("preset.TheSetHoldsFilesInAnd", "the set holds files in %s, and %s", core.A("Name", name), core.A("Reason", below.Reason)), + Hint: core.Says("preset.SetTheSettingToBOr", "Set the {setting} to %d B or %d B.", core.A("Size", size-1), core.A("Minimum", below.Minimum)), } } } @@ -374,9 +373,8 @@ func roomEnough(files []textFile, size int64) error { return &ImpossibleError{ Preset: encodingID, Setting: sampleParam, - Detail: fmt.Sprintf("%s written as %s cannot be smaller than %d B, and every file of this set is the same size", - strings.ToUpper(tallest.desc.ID), tallest.label, floor), - Hint: fmt.Sprintf("Set the {setting} to %d B or more.", floor), + Detail: core.Says("preset.WrittenAsCannotBeSmallerThan", "%s written as %s cannot be smaller than %d B, and every file of this set is the same size", core.A("ID", strings.ToUpper(tallest.desc.ID)), core.A("Label", tallest.label), core.A("Floor", floor)), + Hint: core.Says("preset.SetTheSettingToBOr2", "Set the {setting} to %d B or more.", core.A("Floor", floor)), } } @@ -387,21 +385,21 @@ func roomEnough(files []textFile, size int64) error { // described. And the two halves look like one grid and are not - somebody // reading the file list will look for the UTF-16 CSV that no format in this // build can produce. -func saidAboutTheEncodingSet(Args) []string { - var out []string +func saidAboutTheEncodingSet(Args) []core.Said { + var out []core.Said cells, left := encodingCells() if len(left) > 0 { - out = append(out, fmt.Sprintf( + out = append(out, core.Says("preset.EncodingLeftOut", "this build refuses %s of the encoding set, so it holds %s rather than %d. Left out: %s.", - core.Count(len(left), "combination", "combinations"), - core.Count(len(cells), "file", "files"), - len(cells)+len(left), strings.Join(left, ", "))) + core.A("Refused", core.SaysN("preset.Combinations", "%d combination", "%d combinations", core.A("Count", len(left)))), + core.A("Holds", core.SaysN("preset.Files", "%d file", "%d files", core.A("Count", len(cells)))), + core.A("Described", len(cells)+len(left)), core.A("Left", core.Sentences(left)))) } if both := carrying(textenc.Setting, lineEndingSetting); len(both) == 0 { - out = append(out, fmt.Sprintf( + out = append(out, core.Says("preset.EncodingNoGrid", "no format in this build carries an encoding and a line ending at once, so the two halves of this set are separate files rather than one grid. Encodings: %s. Line endings: %s.", - strings.Join(idsOf(carrying(textenc.Setting)), ", "), - strings.Join(idsOf(carrying(lineEndingSetting)), ", "))) + core.A("Encodings", strings.Join(idsOf(carrying(textenc.Setting)), ", ")), + core.A("LineEndings", strings.Join(idsOf(carrying(lineEndingSetting)), ", ")))) } return out } @@ -417,7 +415,7 @@ func idsOf(descs []format.Descriptor) []string { func expandTextEncoding(args Args) ([]byte, error) { size, err := core.ParseSize(args[sampleParam]) if err != nil { - return nil, fmt.Errorf("%s: %w", sampleParam, err) + return nil, paramCause(sampleParam, err) } // Before the files are laid out, because this refusal is about the value // somebody typed and the one below it is about the set that value asks for. @@ -430,8 +428,8 @@ func expandTextEncoding(args Args) ([]byte, error) { if len(files) == 0 { return nil, &ImpossibleError{ Preset: encodingID, - Detail: "no format in this build carries an encoding or a line ending, so there is no set to build", - Hint: "Run \"tfg formats\" to see what this build has.", + Detail: core.Says("preset.NoFormatInThisBuildCarries", "no format in this build carries an encoding or a line ending, so there is no set to build"), + Hint: core.Says("preset.RunTfgFormatsToSeeWhat", "Run \"tfg formats\" to see what this build has."), } } if err := roomEnough(files, size); err != nil { diff --git a/internal/preset/uploadset.go b/internal/preset/uploadset.go index 10cefc64..2690bf91 100644 --- a/internal/preset/uploadset.go +++ b/internal/preset/uploadset.go @@ -32,7 +32,7 @@ import ( var allowList = commaList{ preset: uploadID, param: allowParam, - empty: "no types were allowed, so the set has no positive control and no file to name wrongly", + empty: core.Says("preset.NoTypesWereAllowedSoThe", "no types were allowed, so the set has no positive control and no file to name wrongly"), check: knownFormat, keep: lower, duplicate: repeatedAllowed, @@ -41,22 +41,18 @@ var allowList = commaList{ var denyList = commaList{ preset: uploadID, param: denyParam, - empty: "no extensions were denied, so there is nothing for the form to turn away", + empty: core.Says("preset.NoExtensionsWereDeniedSoThere", "no extensions were denied, so there is nothing for the form to turn away"), check: checkExtension, keep: lower, duplicate: repeatedDenied, } -func repeatedAllowed(first string) string { - return fmt.Sprintf( - "it is the same type as %q and the set would hold that file twice. Every allowed type appears once, because each one stands for one path through your form", - first) +func repeatedAllowed(first string) core.Said { + return core.Says("preset.ItIsTheSameTypeAs", "it is the same type as %q and the set would hold that file twice. Every allowed type appears once, because each one stands for one path through your form", core.A("First", first)) } -func repeatedDenied(first string) string { - return fmt.Sprintf( - "it is the same extension as %q and the set would hold that file twice. Every denied extension appears once, because each one stands for one rule your form has", - first) +func repeatedDenied(first string) core.Said { + return core.Says("preset.ItIsTheSameExtensionAs", "it is the same extension as %q and the set would hold that file twice. Every denied extension appears once, because each one stands for one rule your form has", core.A("First", first)) } // checkExtension answers why a piece of the deny list is not an extension. @@ -65,20 +61,17 @@ func repeatedDenied(first string) string { // value reaches a file name, so it is made of what a name is made of and // nothing else - the same defence the boundary set's distances get, and for the // same reason rather than by analogy. -func checkExtension(item string) string { +func checkExtension(item string) core.Said { if strings.HasPrefix(item, ".") { - return fmt.Sprintf("an extension is written without its dot, so %q rather than %q", - strings.TrimPrefix(item, "."), item) + return core.Says("preset.AnExtensionIsWrittenWithoutIts", "an extension is written without its dot, so %q rather than %q", core.A("TrimPrefix", strings.TrimPrefix(item, ".")), core.A("Item", item)) } if len(item) > longestExtension { - return fmt.Sprintf("it is %d characters long and an extension here is at most %d", - len(item), longestExtension) + return core.Says("preset.ItIsCharactersLongAndAn", "it is %d characters long and an extension here is at most %d", core.A("Length", len(item)), core.A("LongestExtension", longestExtension)) } if bad := firstNotAlphanumeric(item); bad != "" { - return fmt.Sprintf( - "it holds %s, and an extension is written with letters and digits - such as exe, sh or svg. Its text becomes the end of a file name", bad) + return core.Says("preset.ItHoldsAndAnExtensionIs", "it holds %s, and an extension is written with letters and digits - such as exe, sh or svg. Its text becomes the end of a file name", core.A("Bad", bad)) } - return "" + return core.Said{} } // firstNotAlphanumeric names the first character that cannot appear in an @@ -137,16 +130,13 @@ func settleUpload(args Args) (uploadSet, error) { var s uploadSet limit, err := core.ParseSize(args[uploadLimitParam]) if err != nil { - return s, fmt.Errorf("%s: %w", uploadLimitParam, err) + return s, paramCause(uploadLimitParam, err) } // The limit leads the names of the files built around it, so its text // reaches a file name and gets the check a distance gets. s.limitText = strings.TrimSpace(args[uploadLimitParam]) if bad := firstUnusable(s.limitText); bad != "" { - return s, fmt.Errorf( - "%s: it holds %s, and a limit is written with digits, letters and a dot - "+ - "such as 10mb, 512 or 1.5gb. Its text becomes part of every file name", - uploadLimitParam, bad) + return s, limitUnusable(uploadLimitParam, bad) } s.limit = limit @@ -163,7 +153,8 @@ func settleUpload(args Args) (uploadSet, error) { return s, err } if s.bulk, err = strconv.Atoi(args[bulkParam]); err != nil { - return s, fmt.Errorf("%s: %q is not a whole number of files", bulkParam, args[bulkParam]) + return s, core.Refuse(core.Says("preset.BulkNotWhole", "%s: %q is not a whole number of files", + core.A("Param", core.LabelTerm(bulkParam)), core.A("Value", args[bulkParam]))) } s.filler, err = format.Get(fillerFormat) return s, err @@ -193,11 +184,8 @@ func listsAgree(allowed []format.Descriptor, denied []deniedEntry) error { } return &ImpossibleError{ Preset: uploadID, Setting: denyParam, - Detail: fmt.Sprintf( - "%s is allowed and %s is denied, and both name a file ending %s - so the set would hold one of them to be taken and one to be turned away", - desc.ID, written, desc.Extension), - Hint: fmt.Sprintf("Take %s out of the %s list, or %s out of the %s list.", - desc.ID, allowParam, written, denyParam), + Detail: core.Says("preset.IsAllowedAndIsDeniedAnd", "%s is allowed and %s is denied, and both name a file ending %s - so the set would hold one of them to be taken and one to be turned away", core.A("ID", desc.ID), core.A("Written", written), core.A("Extension", desc.Extension)), + Hint: core.Says("preset.TakeOutOfTheListOr", "Take %s out of the %s list, or %s out of the %s list.", core.A("ID", desc.ID), core.A("AllowParam", allowParam), core.A("Written", written), core.A("DenyParam", denyParam)), } } return nil @@ -261,7 +249,7 @@ func farOverTimes(raw string) (int64, error) { if err != nil || times < 2 { return 0, &format.PropertyValueError{ Format: uploadID, Key: farOverParam, Value: raw, - Reason: "it has to be a number of times the limit, written with an x - 2x or 10x - or off", + Reason: core.Says("preset.ItHasToBeANumber", "it has to be a number of times the limit, written with an x - 2x or 10x - or off"), } } return times, nil @@ -381,7 +369,7 @@ func (s uploadSet) shortfallOf(f setFile) (shortfall, error) { if !errors.As(err, &below) { return shortfall{}, &ImpossibleError{ Preset: uploadID, Setting: uploadLimitParam, - Detail: strings.TrimPrefix(err.Error(), f.desc.ID+": "), + Detail: withoutFormat(err, f.desc.ID), } } return shortfall{ @@ -397,11 +385,8 @@ func (s uploadSet) cannotReach(short shortfall) error { // Splicing the format's own reason in instead read as a contradiction: // it ends "already needs that much", and after "would be 1024 B" the // words pointed at the wrong number. - Detail: fmt.Sprintf("%s would be %d B and the smallest %s this build makes is %d B", - short.file.id, short.file.bytes(), strings.ToUpper(short.file.desc.ID), short.floor), - Hint: fmt.Sprintf( - "Raise the {setting} to %d B or more, or take %s out of the allowed types. The {setting} asked for was %d B.", - short.need, short.file.desc.ID, s.limit), + Detail: core.Says("preset.WouldBeBAndTheSmallest", "%s would be %d B and the smallest %s this build makes is %d B", core.A("ID", short.file.id), core.A("Bytes", short.file.bytes()), core.A("ID2", strings.ToUpper(short.file.desc.ID)), core.A("Floor", short.floor)), + Hint: core.Says("preset.RaiseTheSettingToBOr", "Raise the {setting} to %d B or more, or take %s out of the allowed types. The {setting} asked for was %d B.", core.A("Need", short.need), core.A("ID", short.file.desc.ID), core.A("Limit", s.limit)), } } diff --git a/internal/preset/uploadvalidation.go b/internal/preset/uploadvalidation.go index 154025c7..1bd3d03d 100644 --- a/internal/preset/uploadvalidation.go +++ b/internal/preset/uploadvalidation.go @@ -1,9 +1,6 @@ package preset import ( - "fmt" - "strings" - "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" ) @@ -146,29 +143,29 @@ func init() { // whose inside is a stand-in reads as a real one: somebody testing a content // sniffer against denied.exe would be testing it against plain text and would // never find out from the file. -func saidAboutTheUploadSet(args Args) []string { +func saidAboutTheUploadSet(args Args) []core.Said { s, err := settleUpload(args) if err != nil { // Expand is about to refuse these same values with a message that names // the one that is wrong. A sentence here would be a second opinion. return nil } - var out []string + var out []core.Said if stood := standIns(s.denied); len(stood) > 0 { - out = append(out, fmt.Sprintf( - "this build has no format called %s, so %s %s plain text under %s. That tests a form reading the end of a name, not one reading what is inside.", - joinWithOr(stood), joinWithAnd(namesOf(stood)), - core.Noun(len(stood), "holds", "hold"), - core.Noun(len(stood), "that name", "those names"))) + out = append(out, core.SaysN("preset.StandIns", + "this build has no format called %s, so %s holds plain text under that name. That tests a form reading the end of a name, not one reading what is inside.", + "this build has no format called %s, so %s hold plain text under those names. That tests a form reading the end of a name, not one reading what is inside.", + core.A("Formats", core.Joined{Items: stood}), core.A("Names", core.Joined{Items: namesOf(stood), And: true}), + core.A("Count", len(stood)))) } if s.farOver == 0 { - out = append(out, "far-over is off, so nothing in this set is well past the limit. The largest file is one byte over it.") + out = append(out, core.Says("preset.FarOverOff", "far-over is off, so nothing in this set is well past the limit. The largest file is one byte over it.")) } if s.bulk == 0 { - out = append(out, "bulk is nought, so this set holds no mass upload.") + out = append(out, core.Says("preset.BulkNought", "bulk is nought, so this set holds no mass upload.")) } if len(s.allowed) < 2 { - out = append(out, "only one type is allowed, so the set holds no file named as one allowed type and filled with another - that needs two.") + out = append(out, core.Says("preset.OneAllowed", "only one type is allowed, so the set holds no file named as one allowed type and filled with another - that needs two.")) } return out } @@ -184,26 +181,6 @@ func standIns(denied []deniedEntry) []string { return out } -// joinWithAnd and joinWithOr write a list the way a sentence takes one. -// -// A comma between every pair is how a machine writes a list and it reads as an -// enumeration rather than as a sentence: "denied.exe, denied.sh holds plain -// text" has no number to agree with. The text rules in CLAUDE.md ask for a -// sentence, so the last pair gets its conjunction. -func joinWithAnd(items []string) string { return joinWith(items, "and") } - -func joinWithOr(items []string) string { return joinWith(items, "or") } - -func joinWith(items []string, conjunction string) string { - switch len(items) { - case 0: - return "" - case 1: - return items[0] - } - return strings.Join(items[:len(items)-1], ", ") + " " + conjunction + " " + items[len(items)-1] -} - func namesOf(extensions []string) []string { out := make([]string, 0, len(extensions)) for _, ext := range extensions { diff --git a/internal/recipe/canonical.go b/internal/recipe/canonical.go index 0d1b8803..f0bdcb76 100644 --- a/internal/recipe/canonical.go +++ b/internal/recipe/canonical.go @@ -3,11 +3,12 @@ package recipe import ( "crypto/sha256" "encoding/hex" - "fmt" "strings" "github.com/goccy/go-yaml/ast" "github.com/goccy/go-yaml/parser" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // Canonical is the recipe in one settled shape, comments and blank lines kept. @@ -80,16 +81,16 @@ func Canonical(src []byte, name string) ([]byte, error) { again, err := oneDocument(withoutBOM(out), name) if err != nil { return nil, &ValidationError{Name: name, Problems: []Problem{{ - What: "the settled shape of this file cannot be read back", - Why: "the layout this file needs is one the formatter cannot write, so settling it would replace it with something no longer readable", - Fix: "leave the file as it is, and check it for an anchor or an alias used where a key belongs", + What: core.Says("recipe.TheSettledShapeOfThisFile", "the settled shape of this file cannot be read back"), + Why: core.Says("recipe.TheLayoutThisFileNeedsIs", "the layout this file needs is one the formatter cannot write, so settling it would replace it with something no longer readable"), + Fix: core.Says("recipe.LeaveTheFileAsItIs", "leave the file as it is, and check it for an anchor or an alias used where a key belongs"), }}} } if again.String() != string(out) { return nil, &ValidationError{Name: name, Problems: []Problem{{ - What: "the settled shape of this file does not stay settled", - Why: "settling it twice gives two different files, so a check would never pass and the recipe hash in a manifest would not describe it", - Fix: "leave the file as it is, and simplify whatever it uses that has no single written form - a byte order mark that is not at the very start is the usual cause", + What: core.Says("recipe.TheSettledShapeOfThisFile2", "the settled shape of this file does not stay settled"), + Why: core.Says("recipe.SettlingItTwiceGivesTwoDifferent", "settling it twice gives two different files, so a check would never pass and the recipe hash in a manifest would not describe it"), + Fix: core.Says("recipe.LeaveTheFileAsItIs2", "leave the file as it is, and simplify whatever it uses that has no single written form - a byte order mark that is not at the very start is the usual cause"), }}} } return out, nil @@ -131,9 +132,9 @@ func oneDocument(src []byte, name string) (*ast.File, error) { } if n := recipesIn(f); n > 1 { return nil, &ValidationError{Name: name, Problems: []Problem{{ - What: fmt.Sprintf("the file holds %d YAML documents", n), - Why: "a recipe is one document, and everything after the first separator would be ignored without a word", - Fix: "remove the --- separators, or split the file into one recipe per file", + What: core.Says("recipe.TheFileHoldsYAMLDocuments", "the file holds %d YAML documents", core.A("Value", n)), + Why: core.Says("recipe.ARecipeIsOneDocumentAnd", "a recipe is one document, and everything after the first separator would be ignored without a word"), + Fix: core.Says("recipe.RemoveTheSeparatorsOrSplitThe", "remove the --- separators, or split the file into one recipe per file"), }}} } return f, nil diff --git a/internal/recipe/compose.go b/internal/recipe/compose.go index d43bb103..1716b5fa 100644 --- a/internal/recipe/compose.go +++ b/internal/recipe/compose.go @@ -1,7 +1,6 @@ package recipe import ( - "fmt" "strconv" "unicode/utf8" @@ -352,9 +351,9 @@ func refuseUnwritable(d Document) error { check := func(at, value string) { if bad, found := firstControl(value); found { p.add(at, - fmt.Sprintf("%s holds the character %q, which cannot be written to a recipe", at, bad), - "a recipe is a text document, and a control character either breaks it or is dropped without a word", - "remove it - it is usually a stray tab or a line break from pasting") + core.Says("recipe.HoldsTheCharacterWhichCannotBe", "%s holds the character %q, which cannot be written to a recipe", core.A("At", at), core.A("Bad", bad)), + core.Says("recipe.ARecipeIsATextDocument", "a recipe is a text document, and a control character either breaks it or is dropped without a word"), + core.Says("recipe.RemoveItItIsUsuallyA", "remove it - it is usually a stray tab or a line break from pasting")) } } diff --git a/internal/recipe/contains.go b/internal/recipe/contains.go index cecaf1f9..a9dcba8f 100644 --- a/internal/recipe/contains.go +++ b/internal/recipe/contains.go @@ -1,8 +1,6 @@ package recipe import ( - "fmt" - "github.com/donislawdev/TestingFilesGenerator/internal/core" ) @@ -37,9 +35,9 @@ func contentGroups(p *problems, where spot, raw []map[string]scalar) []Content { switch key { case "format", "count", "size": default: - p.add(at.key, fmt.Sprintf("%s has the key %q", at, key), - "a contains entry describes files with a format, a count and a size, and anything else would be dropped on the way", - "remove the key, or move it to the target itself") + p.add(at.key, core.Says("recipe.HasTheKey", "%s has the key %q", core.A("At", at), core.A("Key", key)), + core.Says("recipe.AContainsEntryDescribesFilesWith", "a contains entry describes files with a format, a count and a size, and anything else would be dropped on the way"), + core.Says("recipe.RemoveTheKeyOrMoveIt", "remove the key, or move it to the target itself")) } } @@ -47,27 +45,27 @@ func contentGroups(p *problems, where spot, raw []map[string]scalar) []Content { if s, ok := v.value(); ok && s != "" { g.Format = s } else { - p.add(at.of("format"), fmt.Sprintf("%s has a format that is not a name", at), - "a format is the id of a format this build supports", - "use format: pdf, or run tfg formats to see the list") + p.add(at.of("format"), core.Says("recipe.HasAFormatThatIsNot", "%s has a format that is not a name", core.A("At", at)), + core.Says("recipe.AFormatIsTheIdOf", "a format is the id of a format this build supports"), + core.Says("recipe.UseFormatPdfOrRunTfg", "use format: pdf, or run tfg formats to see the list")) } } else { - p.add(at.of("format"), fmt.Sprintf("%s has no format", at), - "a container holds real files, so each group says which format its files are", - "add format: pdf") + p.add(at.of("format"), core.Says("recipe.HasNoFormat", "%s has no format", core.A("At", at)), + core.Says("recipe.AContainerHoldsRealFilesSo", "a container holds real files, so each group says which format its files are"), + core.Says("recipe.AddFormatPdf", "add format: pdf")) } if v, ok := item["count"]; ok { n, ok := v.number() switch { case !ok: - p.add(at.of("count"), fmt.Sprintf("%s has {a} {setting} that is not a whole number", at), - "{a} {setting} is how many files of this group the container holds", - "use count: 50") + p.add(at.of("count"), core.Says("recipe.HasSettingThatIsNotA", "%s has {a} {setting} that is not a whole number", core.A("At", at)), + core.Says("recipe.SettingIsHowManyFilesOf", "{a} {setting} is how many files of this group the container holds"), + core.Says("recipe.UseCount50", "use count: 50")) case n < 0: - p.add(at.of("count"), fmt.Sprintf("%s has a negative {setting}", at), - "a container cannot hold fewer than no files", - "use count: 0 for a group that contributes nothing, or drop the entry") + p.add(at.of("count"), core.Says("recipe.HasANegativeSetting", "%s has a negative {setting}", core.A("At", at)), + core.Says("recipe.AContainerCannotHoldFewerThan", "a container cannot hold fewer than no files"), + core.Says("recipe.UseCount0ForAGroup", "use count: 0 for a group that contributes nothing, or drop the entry")) // The same ceiling the count of a target gets, and for the same // reason - judged before the list is built, because building it is // the failure. It was missing here until 2026-08-20, when CodeQL @@ -77,10 +75,9 @@ func contentGroups(p *problems, where spot, raw []map[string]scalar) []Content { // killed. The target path had been refusing that number since // 2026-08-03 and this path never learned it. case n > core.MaxFilesPerRun: - p.add(at.of("count"), fmt.Sprintf("%s asks for %d files", at, n), - core.ErrTooManyFiles.Error(), - fmt.Sprintf("use {a} {setting} of %d or less, or split the container across several entries", - core.MaxFilesPerRun)) + p.add(at.of("count"), core.Says("recipe.AsksForFiles", "%s asks for %d files", core.A("At", at), core.A("Value", n)), + core.SaidOf(core.ErrTooManyFiles), + core.Says("recipe.UseSettingOfOrLessOr", "use {a} {setting} of %d or less, or split the container across several entries", core.A("MaxFilesPerRun", core.MaxFilesPerRun))) default: g.Count = int(n) } @@ -89,13 +86,13 @@ func contentGroups(p *problems, where spot, raw []map[string]scalar) []Content { if v, ok := item["size"]; ok { s, ok := v.value() if !ok { - p.add(at.of("size"), fmt.Sprintf("%s has {a} {setting} that is neither text nor a number", at), - "{a} {setting} is written as 2mb or as a plain byte count", - "use size: 2mb or size: 2097152") + p.add(at.of("size"), core.Says("recipe.HasSettingThatIsNeitherText", "%s has {a} {setting} that is neither text nor a number", core.A("At", at)), + core.Says("recipe.SettingIsWrittenAs2mbOr", "{a} {setting} is written as 2mb or as a plain byte count"), + core.Says("recipe.UseSize2mbOrSize2097152", "use size: 2mb or size: 2097152")) } else if n, err := core.ParseSize(s); err != nil { - p.add(at.of("size"), fmt.Sprintf("%s: %v", at, err), - "units count in 1024s, so 10mb is 10485760 bytes", - "use {a} {setting} such as 2mb, 512kb or a plain byte count") + p.add(at.of("size"), core.Says("recipe.WhereError", "%s: %v", core.A("Where", at), core.A("Error", err)), + core.Says("recipe.UnitsCountIn1024sSo10mb", "units count in 1024s, so 10mb is 10485760 bytes"), + core.Says("recipe.UseSettingSuchAs2mb512kb", "use {a} {setting} such as 2mb, 512kb or a plain byte count")) } else { g.Bytes = n } @@ -103,9 +100,9 @@ func contentGroups(p *problems, where spot, raw []map[string]scalar) []Content { // The same rule as AR10 one level down. Without it the size of the // container could not be worked out before writing, which is the // whole reason contains counts as a way of declaring a size. - p.add(at.of("size"), fmt.Sprintf("%s has no {setting}", at), - "the {setting} of the container follows from the {setting} of what it holds, so every group states one", - "add size: 2mb") + p.add(at.of("size"), core.Says("recipe.HasNoSetting", "%s has no {setting}", core.A("At", at)), + core.Says("recipe.TheSettingOfTheContainerFollows", "the {setting} of the container follows from the {setting} of what it holds, so every group states one"), + core.Says("recipe.AddSize2mb", "add size: 2mb")) } out = append(out, g) diff --git a/internal/recipe/damage.go b/internal/recipe/damage.go index 04220d12..100601e5 100644 --- a/internal/recipe/damage.go +++ b/internal/recipe/damage.go @@ -2,7 +2,6 @@ package recipe import ( "errors" - "fmt" "sort" "github.com/donislawdev/TestingFilesGenerator/internal/core" @@ -31,9 +30,9 @@ func damages(p *problems, where spot, raw []any) damage.Chain { // Not silently nothing. Somebody wrote the key, so they are expecting // broken files, and a run that quietly produced whole ones would be // the silence untouchable rule 6 forbids. - p.add(where.of(KeyDamage), fmt.Sprintf("%s says damage and names none", where), - "a target that damages nothing produces the same files as one that does not mention it", - "name a damage, or remove the line") + p.add(where.of(KeyDamage), core.Says("recipe.SaysDamageAndNamesNone", "%s says damage and names none", core.A("Where", where)), + core.Says("recipe.ATargetThatDamagesNothingProduces", "a target that damages nothing produces the same files as one that does not mention it"), + core.Says("recipe.NameADamageOrRemoveThe", "name a damage, or remove the line")) return nil } @@ -54,9 +53,9 @@ func oneDamage(p *problems, at spot, entry any) (damage.Spec, bool) { case map[string]any: return mappedDamage(p, at, x) default: - p.add(at.of(KeyDamageType), fmt.Sprintf("%s is neither a name nor a set of settings", at), - "a damage is written as its name, or as a mapping with type and the settings it takes", - "write the name on its own, or a mapping starting with type") + p.add(at.of(KeyDamageType), core.Says("recipe.IsNeitherANameNorA", "%s is neither a name nor a set of settings", core.A("At", at)), + core.Says("recipe.ADamageIsWrittenAsIts", "a damage is written as its name, or as a mapping with type and the settings it takes"), + core.Says("recipe.WriteTheNameOnItsOwn", "write the name on its own, or a mapping starting with type")) return damage.Spec{}, false } } @@ -65,16 +64,16 @@ func oneDamage(p *problems, at spot, entry any) (damage.Spec, bool) { func mappedDamage(p *problems, at spot, x map[string]any) (damage.Spec, bool) { rawType, stated := x[KeyDamageType] if !stated { - p.add(at.of(KeyDamageType), fmt.Sprintf("%s does not say which damage it is", at), - "a damage written as a mapping names itself with type", - "add type, with the name of the damage") + p.add(at.of(KeyDamageType), core.Says("recipe.DoesNotSayWhichDamageIt", "%s does not say which damage it is", core.A("At", at)), + core.Says("recipe.ADamageWrittenAsAMapping", "a damage written as a mapping names itself with type"), + core.Says("recipe.AddTypeWithTheNameOf", "add type, with the name of the damage")) return damage.Spec{}, false } id, isScalar := scalarText(rawType) if !isScalar { - p.add(at.of(KeyDamageType), fmt.Sprintf("%s names a damage that is not a word", at), - "the type of a damage is the name this build knows it by", - "write the name as a word") + p.add(at.of(KeyDamageType), core.Says("recipe.NamesADamageThatIsNot", "%s names a damage that is not a word", core.A("At", at)), + core.Says("recipe.TheTypeOfADamageIs", "the type of a damage is the name this build knows it by"), + core.Says("recipe.WriteTheNameAsAWord", "write the name as a word")) return damage.Spec{}, false } @@ -85,9 +84,9 @@ func mappedDamage(p *problems, at spot, x map[string]any) (damage.Spec, bool) { } text, single := scalarText(x[key]) if !single { - p.add(at.of(key), fmt.Sprintf("%s: %s is a list or a block", at, key), - "a damage setting takes one value, the way a format property does", - "give it one value, for example bytes: 16") + p.add(at.of(key), core.Says("recipe.IsAListOrABlock", "%s: %s is a list or a block", core.A("At", at), core.A("Key", key)), + core.Says("recipe.ADamageSettingTakesOneValue", "a damage setting takes one value, the way a format property does"), + core.Says("recipe.GiveItOneValueForExample", "give it one value, for example bytes: 16")) continue } values[key] = text @@ -108,10 +107,10 @@ func checkedDamage(p *problems, at spot, id string, values damage.Values) (damag if err != nil { var unknown *damage.UnknownError if errors.As(err, &unknown) { - p.add(at.of(KeyDamageType), fmt.Sprintf("%s: %s", at, unknown.What()), - unknown.Why(), unknown.Instead()) + what, why, instead := unknown.Parts() + p.add(at.of(KeyDamageType), whereWhat(at, what), why, instead) } else { - p.add(at.of(KeyDamageType), fmt.Sprintf("%s: %s", at, err.Error()), "", "") + p.add(at.of(KeyDamageType), whereWhat(at, core.SaidOf(err)), core.Said{}, core.Said{}) } return damage.Spec{}, false } @@ -132,23 +131,23 @@ func checkedDamage(p *problems, at spot, id string, values damage.Values) (damag func reportDamageSetting(p *problems, at spot, bad error) { var about interface{ AboutSetting() string } if !errors.As(bad, &about) { - p.add(at.of(KeyDamage), bad.Error(), "", "") + p.add(at.of(KeyDamage), core.SaidOf(bad), core.Said{}, core.Said{}) return } where := at.of(about.AboutSetting()) var value *format.PropertyValueError if errors.As(bad, &value) { - p.add(where, fmt.Sprintf("%s: %s cannot be %q", at, value.Key, value.Value), - core.InTheWordsOf(value.Reason, value.Key), value.Remedy) + p.add(where, cannotBe(at, value), value.Reason, value.Remedy) return } var unknown *format.UnknownPropertyError if errors.As(bad, &unknown) { - p.add(where, fmt.Sprintf("%s: %s", at, unknown.What()), unknown.Why(), unknown.Instead()) + what, why, instead := unknown.Parts() + p.add(where, whereWhat(at, what), why, instead) return } - p.add(where, fmt.Sprintf("%s: %s", at, bad.Error()), "", "") + p.add(where, whereWhat(at, core.SaidOf(bad)), core.Said{}, core.Said{}) } // refuseImpossibleExpectation stops a target that breaks a file and expects it @@ -177,9 +176,8 @@ func refuseImpossibleExpectation(p *problems, where spot, t Target) { if refusal == nil { return } - p.add(where.of(refusal.AboutSetting()), - fmt.Sprintf("%s: %s", where, refusal.What()), - refusal.Why(), refusal.Instead()) + what, why, instead := refusal.Parts() + p.add(where.of(refusal.AboutSetting()), whereWhat(where, what), why, instead) } // sortedKeysOf puts the settings of one entry in a stable order, so the same diff --git a/internal/recipe/errors.go b/internal/recipe/errors.go index 6b6813bf..3d1e7c8e 100644 --- a/internal/recipe/errors.go +++ b/internal/recipe/errors.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/donislawdev/TestingFilesGenerator/internal/core" + "github.com/donislawdev/TestingFilesGenerator/internal/format" ) // A recipe is refused with every problem it has, not the first one. @@ -19,9 +20,9 @@ import ( // what to do instead. An error that only states the first leaves the reader to // guess the other two. type Problem struct { - What string - Why string - Fix string + What core.Said + Why core.Said + Fix core.Said // At is the setting this problem is about, in the vocabulary the two // surfaces share: a recipe key, with a 1-based index wherever a list is // involved, as in targets[2].size or targets[2].contains[1].format. @@ -51,8 +52,13 @@ func (p Problem) InTheWordsOf(name string) string { if name == "" { name = core.LastSettingSegment(p.At) } - return core.InTheWordsOf( - fmt.Sprintf("%s - %s.\n %s.", p.What, p.Why, p.Fix), name) + return core.InTheWordsOf(p.Said().String(), name) +} + +// Said is the whole problem, for a window that says it in its own language, +// with the setting slot left for the window to fill. +func (p Problem) Said() core.Said { + return core.Says("recipe.Problem", "%s - %s.\n %s.", core.A("What", p.What), core.A("Why", p.Why), core.A("Fix", p.Fix)) } // Error makes one problem an error in its own right, so a refused recipe can be @@ -97,11 +103,8 @@ func (e *ValidationError) Unwrap() []error { func (e *ValidationError) Error() string { var b strings.Builder - word := "problems" - if len(e.Problems) == 1 { - word = "problem" - } - fmt.Fprintf(&b, "%s has %d %s and nothing was written:", e.Name, len(e.Problems), word) + b.WriteString(core.SaysN("recipe.Refused", "%s has %d problem and nothing was written:", "%s has %d problems and nothing was written:", + core.A("Name", e.Name), core.A("Count", len(e.Problems))).String()) for _, p := range e.Problems { fmt.Fprintf(&b, "\n\n- %s", p.String()) } @@ -122,8 +125,26 @@ type SyntaxError struct { UTF16 bool } -func (e *SyntaxError) Error() string { - return fmt.Sprintf("%s cannot be read as a recipe:\n%s", e.Name, e.Detail) +func (e *SyntaxError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. The +// parser's own words follow it as they came - they point at a line and a +// column, and rewriting them would lose that. +func (e *SyntaxError) Said() core.Said { + return core.Says("recipe.Unreadable", "%s cannot be read as a recipe:\n%s", core.A("Name", e.Name), core.A("Detail", e.Detail)) +} + +// whereWhat is a refusal from below the reader, said with the place it is +// about in front of it. +func whereWhat(where spot, what core.Said) core.Said { + return core.Says("recipe.WhereWhat", "%s: %s", core.A("Where", where), core.A("What", what)) +} + +// cannotBe is a declared setting refused its value, with the place in front. +// The setting is named as a registry word, so a window says its label. +func cannotBe(where spot, value *format.PropertyValueError) core.Said { + return core.Says("recipe.CannotBe", "%s: %s cannot be %q", + core.A("Where", where), core.A("Key", core.LabelTerm(value.Key)), core.A("Value", value.Value)) } // problems collects everything wrong with one recipe. @@ -132,7 +153,7 @@ type problems struct { list []Problem } -func (p *problems) add(at, what, why, fix string) { +func (p *problems) add(at string, what, why, fix core.Said) { p.list = append(p.list, Problem{What: what, Why: why, Fix: fix, At: at}) } @@ -141,14 +162,14 @@ func (p *problems) add(at, what, why, fix string) { // It is its own kind of message because the answer is different: the recipe is // not wrong, the tool is not there yet. Saying "unknown key" would send the // reader looking for a typo that does not exist. -func (p *problems) notYet(key, why, fix string) { - p.add(key, fmt.Sprintf("%s is not in this build yet", key), why, fix) +func (p *problems) notYet(key string, why, fix core.Said) { + p.add(key, core.Says("recipe.NotYet", "%s is not in this build yet", core.A("Key", key)), why, fix) } // notYetIn is the same for a key inside a target, where the sentence names the // target and the address does not - see spot. -func (p *problems) notYetIn(where spot, setting, why, fix string) { - p.add(where.of(setting), fmt.Sprintf("%s: %s is not in this build yet", where, setting), why, fix) +func (p *problems) notYetIn(where spot, setting string, why, fix core.Said) { + p.add(where.of(setting), core.Says("recipe.NotYetIn", "%s: %s is not in this build yet", core.A("Where", where), core.A("Key", setting)), why, fix) } // spot is one place in a recipe, in the two vocabularies this tool needs at @@ -163,7 +184,7 @@ func (p *problems) notYetIn(where spot, setting, why, fix string) { // It renders as the prose, so every message built with %s reads as it did // before this type existed. type spot struct { - says string + says core.Said key string // whole means the address is the whole of this spot whatever setting is // named under it. A preset's target has no boxes of its own on any @@ -172,7 +193,10 @@ type spot struct { whole bool } -func (s spot) String() string { return s.says } +func (s spot) String() string { return s.says.String() } + +// Said is the place as a sentence, so a window names it in its own language. +func (s spot) Said() core.Said { return s.says } // of names a setting inside this spot: targets[2] and "size" make // targets[2].size. A dotted setting is passed through, for the settings that @@ -191,14 +215,19 @@ func (s spot) of(setting string) string { // prose counts, so the two halves agree about which entry is meant. func (s spot) entry(list string, index int) spot { if s.whole { - return spot{says: fmt.Sprintf("%s: %s entry %d", s.says, list, index+1), key: s.key, whole: true} + return spot{says: entrySaid(s, list, index), key: s.key, whole: true} } return spot{ - says: fmt.Sprintf("%s: %s entry %d", s.says, list, index+1), + says: entrySaid(s, list, index), key: fmt.Sprintf("%s.%s[%d]", s.key, list, index+1), } } +// entrySaid is one entry of a list inside a spot, counted from one. +func entrySaid(s spot, list string, index int) core.Said { + return core.Says("recipe.Entry", "%s: %s entry %d", core.A("Where", s.says), core.A("List", list), core.A("Number", index+1)) +} + // presetTargetSpot is where one target the preset contributed is. // // Its prose says whose the target is, because a person looking at the file @@ -207,12 +236,12 @@ func (s spot) entry(list string, index int) spot { // preset's own list, which is what "tfg preset eject" prints. func presetTargetSpot(index int, id string) spot { s := spot{ - says: fmt.Sprintf("the preset's target %d", index+1), + says: core.Says("recipe.PresetTargetNumber", "the preset's target %d", core.A("Number", index+1)), key: KeyExtends, whole: true, } if id != "" { - s.says = fmt.Sprintf("the preset's target %q", id) + s.says = core.Says("recipe.PresetTarget", "the preset's target %q", core.A("Target", id)) } return s } @@ -220,7 +249,7 @@ func presetTargetSpot(index int, id string) spot { // targetSpot is where one entry of the targets list is. func targetSpot(index int, id string) spot { s := spot{ - says: fmt.Sprintf("target %d", index+1), + says: core.Says("recipe.TargetNumber", "target %d", core.A("Number", index+1)), // Built from the same helper a surface uses, so the address a refusal // arrives with and the address a box is registered under cannot differ // in their shape. What they can still differ in is which setting was @@ -228,7 +257,7 @@ func targetSpot(index int, id string) spot { key: core.TargetPrefix(index + 1), } if id != "" { - s.says = fmt.Sprintf("target %q", id) + s.says = core.Says("recipe.Target", "target %q", core.A("Target", id)) } return s } diff --git a/internal/recipe/extends.go b/internal/recipe/extends.go index 096c7930..d8736327 100644 --- a/internal/recipe/extends.go +++ b/internal/recipe/extends.go @@ -4,6 +4,8 @@ import ( "fmt" "sort" "strings" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // A recipe that builds on a preset. @@ -76,9 +78,9 @@ func (e *Extension) Parse(base []byte) (*Recipe, error) { func (raw rawRecipe) extension(p *problems) *Extension { if raw.Extends == nil { if raw.With != nil { - p.add(KeyWith, "with fills the parameters of a preset, and no preset is named", - "with belongs beside extends: it says what to fill in, and extends says what to fill it into", - "add extends: preset: above it, or remove with") + p.add(KeyWith, core.Says("recipe.WithFillsTheParametersOfA", "with fills the parameters of a preset, and no preset is named"), + core.Says("recipe.WithBelongsBesideExtendsItSays", "with belongs beside extends: it says what to fill in, and extends says what to fill it into"), + core.Says("recipe.AddExtendsPresetIdAboveIt", "add extends: preset: above it, or remove with")) } return nil } @@ -88,9 +90,9 @@ func (raw rawRecipe) extension(p *problems) *Extension { } id := strings.TrimPrefix(value, presetScheme) if id == value || id == "" { - p.add(KeyExtends, fmt.Sprintf("extends %q does not name a preset", value), - "a recipe can build on a preset, and on nothing else in this build - not on another file", - "write extends: preset:, and run \"tfg preset list\" for the ids this build has") + p.add(KeyExtends, core.Says("recipe.ExtendsDoesNotNameAPreset", "extends %q does not name a preset", core.A("Value", value)), + core.Says("recipe.ARecipeCanBuildOnA", "a recipe can build on a preset, and on nothing else in this build - not on another file"), + core.Says("recipe.WriteExtendsPresetIdAndRun", "write extends: preset:, and run \"tfg preset list\" for the ids this build has")) return nil } ext := &Extension{Preset: id} @@ -155,16 +157,16 @@ func (own rawRecipe) parseExtending(name string, base []byte) (*Recipe, error) { // caller's mistake rather than the file's, and reading the file as // if it did would run targets the file never asked for. if p.err() == nil { - p.add(KeyExtends, "this recipe does not build on a preset", - "a preset's targets were supplied to the reader, and the recipe names none", - "read the file with Parse, or add extends: preset:") + p.add(KeyExtends, core.Says("recipe.ThisRecipeDoesNotBuildOn", "this recipe does not build on a preset"), + core.Says("recipe.APresetSTargetsWereSupplied", "a preset's targets were supplied to the reader, and the recipe names none"), + core.Says("recipe.ReadTheFileWithParseOr", "read the file with Parse, or add extends: preset:")) } return nil, p.err() } if err := from.onlyTargets(); err != nil { - p.add(KeyExtends, err.Error(), - "a preset contributes targets and nothing else, so that the seed, the defaults and the output section are always the file's own", - "this is a mistake in the preset rather than in the recipe - run \"tfg preset eject\" and edit the result instead") + p.add(KeyExtends, core.SaidOf(err), + core.Says("recipe.APresetContributesTargetsAndNothing", "a preset contributes targets and nothing else, so that the seed, the defaults and the output section are always the file's own"), + core.Says("recipe.ThisIsAMistakeInThe", "this is a mistake in the preset rather than in the recipe - run \"tfg preset eject\" and edit the result instead")) return nil, p.err() } @@ -229,6 +231,6 @@ func (raw rawRecipe) onlyTargets() error { if len(carries) == 0 { return nil } - return fmt.Errorf("the preset's recipe carries %s, which a recipe building on it cannot inherit", - strings.Join(carries, ", ")) + return core.Refuse(core.Says("recipe.PresetCarries", "the preset's recipe carries %s, which a recipe building on it cannot inherit", + core.A("Keys", strings.Join(carries, ", ")))) } diff --git a/internal/recipe/limits.go b/internal/recipe/limits.go index c417fafe..792e3c6d 100644 --- a/internal/recipe/limits.go +++ b/internal/recipe/limits.go @@ -2,10 +2,10 @@ package recipe import ( - "fmt" - "github.com/goccy/go-yaml/lexer" "github.com/goccy/go-yaml/token" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // What a recipe is allowed to cost to read, beyond its size. @@ -77,10 +77,13 @@ type TooDeepError struct { Depth int } -func (e *TooDeepError) Error() string { - return fmt.Sprintf( +func (e *TooDeepError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. +func (e *TooDeepError) Said() core.Said { + return core.Says("recipe.TooDeep", "%s nests lists and mappings %d deep and the limit is %d. Reading a deeply nested document costs memory that grows far faster than the document does, so a small file can exhaust this machine before anything is written. Write the targets out as an ordinary list instead", - e.Name, e.Depth, MaxNestingDepth) + core.A("Name", e.Name), core.A("Depth", e.Depth), core.A("Limit", MaxNestingDepth)) } // nestingDepth is the deepest the collections in src nest, counting both diff --git a/internal/recipe/recipe.go b/internal/recipe/recipe.go index 8c50a4fc..4a90df0a 100644 --- a/internal/recipe/recipe.go +++ b/internal/recipe/recipe.go @@ -1,7 +1,6 @@ package recipe import ( - "fmt" "sort" "strconv" "strings" @@ -11,6 +10,8 @@ import ( "github.com/goccy/go-yaml/ast" "github.com/donislawdev/TestingFilesGenerator/internal/damage" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // SchemaVersion is the recipe schema this build understands. It is versioned @@ -170,10 +171,13 @@ type TooLargeError struct { Bytes int64 } -func (e *TooLargeError) Error() string { - return fmt.Sprintf( +func (e *TooLargeError) Error() string { return e.Said().String() } + +// Said is the refusal, for a window that says it in its own language. +func (e *TooLargeError) Said() core.Said { + return core.Says("recipe.TooLarge", "%s is %d B and the limit is %d B. A recipe is a document somebody writes, and reading time grows with its size, so an unbounded one is a way to hang a build. Split it into several recipes, or generate the targets with a loop in your own script", - e.Name, e.Bytes, MaxBytes) + core.A("Name", e.Name), core.A("Bytes", e.Bytes), core.A("Limit", int64(MaxBytes))) } // Parse reads a recipe and returns it only when every check passes. @@ -286,7 +290,7 @@ func decodeStrict(f *ast.File, raw *rawRecipe) (err error) { // The panic value itself says "invalid memory address", which tells // a person testing an upload form nothing at all. What helps is the // shape of the thing that does it. - err = fmt.Errorf("this file could not be read as YAML. Look for a tag or anchor marker such as ! or & with nothing after it") + err = core.Refuse(core.Says("recipe.NotYAML", "this file could not be read as YAML. Look for a tag or anchor marker such as ! or & with nothing after it")) } }() // Decoded from the document the one-document check already parsed, rather @@ -382,17 +386,17 @@ func (raw rawRecipe) validate(p *problems, fromPreset int) *Recipe { } switch { case raw.Version == nil: - p.add("version", "the recipe has no version", - "every recipe declares the schema version it was written against", - fmt.Sprintf("add version: %d as the first line", SchemaVersion)) + p.add("version", core.Says("recipe.TheRecipeHasNoVersion", "the recipe has no version"), + core.Says("recipe.EveryRecipeDeclaresTheSchemaVersion", "every recipe declares the schema version it was written against"), + core.Says("recipe.AddVersionAsTheFirstLine", "add version: %d as the first line", core.A("SchemaVersion", SchemaVersion))) case !versionIsNumber: - p.add("version", fmt.Sprintf("version %q is not a whole number", raw.Version.text), - "the version decides how the rest of the file is read, so it is never guessed at", - fmt.Sprintf("write version: %d", SchemaVersion)) + p.add("version", core.Says("recipe.VersionIsNotAWholeNumber", "version %q is not a whole number", core.A("Text", raw.Version.text)), + core.Says("recipe.TheVersionDecidesHowTheRest", "the version decides how the rest of the file is read, so it is never guessed at"), + core.Says("recipe.WriteVersion", "write version: %d", core.A("SchemaVersion", SchemaVersion))) case version != SchemaVersion: - p.add("version", fmt.Sprintf("version %d is not a schema this build knows", version), - fmt.Sprintf("this build understands version %d", SchemaVersion), - "upgrade the tool, or write the recipe against the version above") + p.add("version", core.Says("recipe.VersionIsNotASchemaThis", "version %d is not a schema this build knows", core.A("Version", version)), + core.Says("recipe.ThisBuildUnderstandsVersion", "this build understands version %d", core.A("SchemaVersion", SchemaVersion)), + core.Says("recipe.UpgradeTheToolOrWriteThe", "upgrade the tool, or write the recipe against the version above")) } raw.refuseUnsupported(p) @@ -408,16 +412,16 @@ func (raw rawRecipe) validate(p *problems, fromPreset int) *Recipe { // README, which says a recipe of extends alone is legal. Until // 2026-09-22 both keys were refused here as "not in this build yet". if ext := raw.extension(p); ext != nil { - p.add(KeyExtends, fmt.Sprintf("this recipe builds on preset:%s, and the preset's targets were not supplied", ext.Preset), - "a recipe that builds on a preset is read by a reader that expands the preset first, and this one does not", - "read the file with \"tfg generate\" or \"tfg validate\", which do") + p.add(KeyExtends, core.Says("recipe.ThisRecipeBuildsOnPresetAnd", "this recipe builds on preset:%s, and the preset's targets were not supplied", core.A("Preset", ext.Preset)), + core.Says("recipe.ARecipeThatBuildsOnA", "a recipe that builds on a preset is read by a reader that expands the preset first, and this one does not"), + core.Says("recipe.ReadTheFileWithTfgGenerate", "read the file with \"tfg generate\" or \"tfg validate\", which do")) return rec } if len(raw.Targets) == 0 { - p.add("targets", "the recipe asks for no files", - "a recipe without targets has nothing to produce", - "add at least one entry under targets:") + p.add("targets", core.Says("recipe.TheRecipeAsksForNoFiles", "the recipe asks for no files"), + core.Says("recipe.ARecipeWithoutTargetsHasNothing", "a recipe without targets has nothing to produce"), + core.Says("recipe.AddAtLeastOneEntryUnder", "add at least one entry under targets:")) return rec } @@ -447,14 +451,14 @@ func (raw rawRecipe) validate(p *problems, fromPreset int) *Recipe { // lies. ofThePreset says which. func usedTwice(p *problems, where spot, id string, ofThePreset bool) { if ofThePreset { - p.add(where.of("id"), fmt.Sprintf("%s has the {setting} of a target the preset already builds", where), - "the preset's targets come first and every {setting} anchors a seed, so a second one would be a target nobody can tell from the first", - "give it a different {setting}, or leave the preset's target out by ejecting the preset and editing the recipe") + p.add(where.of("id"), core.Says("recipe.HasTheSettingOfATarget", "%s has the {setting} of a target the preset already builds", core.A("Where", where)), + core.Says("recipe.ThePresetSTargetsComeFirst", "the preset's targets come first and every {setting} anchors a seed, so a second one would be a target nobody can tell from the first"), + core.Says("recipe.GiveItADifferentSettingOr", "give it a different {setting}, or leave the preset's target out by ejecting the preset and editing the recipe")) return } - p.add(where.of("id"), fmt.Sprintf("target {setting} %q is used twice", id), - "{a} {setting} identifies a target, anchors its seed and links it to the manifest", - "give one of them a different {setting}") + p.add(where.of("id"), core.Says("recipe.TargetSettingIsUsedTwice", "target {setting} %q is used twice", core.A("ID", id)), + core.Says("recipe.SettingIdentifiesATargetAnchorsIts", "{a} {setting} identifies a target, anchors its seed and links it to the manifest"), + core.Says("recipe.GiveOneOfThemADifferent", "give one of them a different {setting}")) } // spotOfTarget is where the target at position i of the merged list is, as a @@ -479,22 +483,22 @@ func spotOfTarget(i, fromPreset int) func(id string) spot { // asked for, which is the one thing this tool must never do. func (raw rawRecipe) refuseUnsupported(p *problems) { if raw.Engine != nil { - p.notYet("engine", "the tool version this recipe requires is not checked yet", - "remove the line - the manifest records the version that ran") + p.notYet("engine", core.Says("recipe.TheToolVersionThisRecipeRequires", "the tool version this recipe requires is not checked yet"), + core.Says("recipe.RemoveTheLineTheManifestRecords", "remove the line - the manifest records the version that ran")) } if locale, ok := oneValue(p, "locale", "locale", "locale: en", raw.Locale); ok && locale != "en" { - p.add("locale", fmt.Sprintf("locale %q is not available in this build", locale), - "generated content is English only so far", - "use locale: en, or leave the line out") + p.add("locale", core.Says("recipe.LocaleIsNotAvailableInThis", "locale %q is not available in this build", core.A("Locale", locale)), + core.Says("recipe.GeneratedContentIsEnglishOnlySo", "generated content is English only so far"), + core.Says("recipe.UseLocaleEnOrLeaveThe", "use locale: en, or leave the line out")) } if on, ok := oneFlag(p, "allow_nondeterministic", "allow_nondeterministic", raw.AllowNondeterministic); ok && on { - p.add("allow_nondeterministic", "allow_nondeterministic: true has nothing to allow in this build", - "every format here repeats to the byte, so no consent is needed", - "remove the line - it will be needed by the formats that use a system encoder") + p.add("allow_nondeterministic", core.Says("recipe.AllowNondeterministicTrueHasNothingTo", "allow_nondeterministic: true has nothing to allow in this build"), + core.Says("recipe.EveryFormatHereRepeatsToThe", "every format here repeats to the byte, so no consent is needed"), + core.Says("recipe.RemoveTheLineItWillBe", "remove the line - it will be needed by the formats that use a system encoder")) } if raw.Policy != nil { - p.notYet("policy", "unspecified expectations are left in the manifest for the consumer to settle", - "remove the section - the expected field on a target already works") + p.notYet("policy", core.Says("recipe.UnspecifiedExpectationsAreLeftInThe", "unspecified expectations are left in the manifest for the consumer to settle"), + core.Says("recipe.RemoveTheSectionTheExpectedField", "remove the section - the expected field on a target already works")) } } @@ -504,9 +508,9 @@ func (raw rawRecipe) applySettings(p *problems, rec *Recipe) { if raw.Seed != nil { n, ok := raw.Seed.number() if !ok { - p.add("seed", fmt.Sprintf("seed %q is not a whole number", raw.Seed.text), - "the seed decides every byte of the run, so it is read exactly as written and never guessed at", - "write a decimal number such as seed: 20260802") + p.add("seed", core.Says("recipe.SeedIsNotAWholeNumber", "seed %q is not a whole number", core.A("Text", raw.Seed.text)), + core.Says("recipe.TheSeedDecidesEveryByteOf", "the seed decides every byte of the run, so it is read exactly as written and never guessed at"), + core.Says("recipe.WriteADecimalNumberSuchAs", "write a decimal number such as seed: 20260802")) } else { rec.Seed = n rec.SeedSet = true @@ -515,8 +519,8 @@ func (raw rawRecipe) applySettings(p *problems, rec *Recipe) { if raw.Defaults != nil { if raw.Defaults.Fill != nil { - p.notYet("defaults.fill", "the fill mode is not settable yet", - "remove the line - content is generated from the seed") + p.notYet("defaults.fill", core.Says("recipe.TheFillModeIsNotSettable", "the fill mode is not settable yet"), + core.Says("recipe.RemoveTheLineContentIsGenerated", "remove the line - content is generated from the seed")) } if on, ok := oneFlag(p, "defaults.label", "defaults.label", raw.Defaults.Label); ok { rec.Defaults.Label = on @@ -525,8 +529,8 @@ func (raw rawRecipe) applySettings(p *problems, rec *Recipe) { if raw.Output != nil { if raw.Output.SplitThreshold != nil { - p.notYet("output.split_threshold", "the manifest is always written as one file so far", - "remove the line") + p.notYet("output.split_threshold", core.Says("recipe.TheManifestIsAlwaysWrittenAs", "the manifest is always written as one file so far"), + core.Says("recipe.RemoveTheLine", "remove the line")) } if dir, ok := oneValue(p, "output.dir", "output.dir", "dir: ./fixtures", raw.Output.Dir); ok { rec.Output.Dir = dir diff --git a/internal/recipe/shape.go b/internal/recipe/shape.go index d5e559c7..b4520b75 100644 --- a/internal/recipe/shape.go +++ b/internal/recipe/shape.go @@ -1,6 +1,6 @@ package recipe -import "fmt" +import "github.com/donislawdev/TestingFilesGenerator/internal/core" // A setting written as a block where a single value belongs. // @@ -36,9 +36,9 @@ func oneValue(p *problems, at, subject, example string, s *scalar) (string, bool v, ok := s.value() if !ok { p.add(at, - fmt.Sprintf("%s is written as a list or a block", subject), - "this setting is one value, written beside its name rather than indented under it", - fmt.Sprintf("write it on one line, such as %s", example)) + core.Says("recipe.IsWrittenAsAListOr", "%s is written as a list or a block", core.A("Subject", subject)), + core.Says("recipe.ThisSettingIsOneValueWritten", "this setting is one value, written beside its name rather than indented under it"), + core.Says("recipe.WriteItOnOneLineSuch", "write it on one line, such as %s", core.A("Example", example))) return "", false } return v, true @@ -62,8 +62,8 @@ func oneFlag(p *problems, at, subject string, s *scalar) (bool, bool) { return false, true } p.add(at, - fmt.Sprintf("%s is %q, which is neither true nor false", subject, v), - "this setting is one of two words, and anything else would have to be guessed at", - fmt.Sprintf("use %s: true or %s: false", subject, subject)) + core.Says("recipe.IsWhichIsNeitherTrueNor", "%s is %q, which is neither true nor false", core.A("Subject", subject), core.A("Value", v)), + core.Says("recipe.ThisSettingIsOneOfTwo", "this setting is one of two words, and anything else would have to be guessed at"), + core.Says("recipe.UseTrueOrFalse", "use %s: true or %s: false", core.A("Subject", subject), core.A("Subject2", subject))) return false, false } diff --git a/internal/recipe/target.go b/internal/recipe/target.go index 57c06bab..a7ad38fe 100644 --- a/internal/recipe/target.go +++ b/internal/recipe/target.go @@ -2,9 +2,7 @@ package recipe import ( "errors" - "fmt" "sort" - "strings" "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" @@ -80,16 +78,16 @@ func (rt rawTarget) validate(p *problems, at func(id string) spot, def Defaults) t.ID = id where = at(t.ID) } else { - p.add(where.of("id"), fmt.Sprintf("%s has no {setting}", where), - "{a} {setting} anchors the seed of a target, so editing one target never moves the bytes of another", - "give it {a} {setting}, for example id: invoices") + p.add(where.of("id"), core.Says("recipe.HasNoSetting", "%s has no {setting}", core.A("At", where)), + core.Says("recipe.SettingAnchorsTheSeedOfA", "{a} {setting} anchors the seed of a target, so editing one target never moves the bytes of another"), + core.Says("recipe.GiveItSettingForExampleId", "give it {a} {setting}, for example id: invoices")) } format, formatGiven := oneValue(p, where.of("format"), where.String()+" format", "format: txt", rt.Format) if !formatGiven || format == "" { - p.add(where.of("format"), fmt.Sprintf("%s has no format", where), - "a target has to say what kind of file it produces", - "add format: txt, or run \"tfg formats\" to see the whole list") + p.add(where.of("format"), core.Says("recipe.HasNoFormat", "%s has no format", core.A("At", where)), + core.Says("recipe.ATargetHasToSayWhat", "a target has to say what kind of file it produces"), + core.Says("recipe.AddFormatTxtOrRunTfg", "add format: txt, or run \"tfg formats\" to see the whole list")) } else { t.Format = format } @@ -98,20 +96,20 @@ func (rt rawTarget) validate(p *problems, at func(id string) spot, def Defaults) n, ok := rt.Count.number() switch { case !ok: - p.add(where.of("count"), fmt.Sprintf("%s has {a} {setting} of %q, which is not a whole number", where, rt.Count.text), - "{a} {setting} is how many files this target produces, so it is read exactly as written", - "write a decimal number such as count: 10") + p.add(where.of("count"), core.Says("recipe.HasSettingOfWhichIsNot", "%s has {a} {setting} of %q, which is not a whole number", core.A("Where", where), core.A("Text", rt.Count.text)), + core.Says("recipe.SettingIsHowManyFilesThis", "{a} {setting} is how many files this target produces, so it is read exactly as written"), + core.Says("recipe.WriteADecimalNumberSuchAs2", "write a decimal number such as count: 10")) case n <= 0: - p.add(where.of("count"), fmt.Sprintf("%s asks for %d files", where, n), - "a target that produces nothing is almost always a mistake rather than an intention", - "ask for at least one, or delete the target") + p.add(where.of("count"), core.Says("recipe.AsksForFiles", "%s asks for %d files", core.A("At", where), core.A("Value", n)), + core.Says("recipe.ATargetThatProducesNothingIs", "a target that produces nothing is almost always a mistake rather than an intention"), + core.Says("recipe.AskForAtLeastOneOr", "ask for at least one, or delete the target")) // Judged before the list is built. The reader used to grow it one entry // at a time and reached a 13 GB allocation on a count of 2^63 - so this // has to refuse the number rather than the result of using it. case n > core.MaxFilesPerRun: - p.add(where.of("count"), fmt.Sprintf("%s asks for %d files", where, n), - core.ErrTooManyFiles.Error(), - fmt.Sprintf("use {a} {setting} of %d or less, or split the target across several recipes", core.MaxFilesPerRun)) + p.add(where.of("count"), core.Says("recipe.AsksForFiles", "%s asks for %d files", core.A("At", where), core.A("Value", n)), + core.SaidOf(core.ErrTooManyFiles), + core.Says("recipe.UseSettingOfOrLessOr2", "use {a} {setting} of %d or less, or split the target across several recipes", core.A("MaxFilesPerRun", core.MaxFilesPerRun))) default: count = int(n) } @@ -153,8 +151,8 @@ func (rt rawTarget) describe(p *problems, where spot, t *Target) { // refuseSections names the parts of a target this build cannot honour. func (rt rawTarget) refuseSections(p *problems, where spot) { if rt.Fill != nil { - p.notYetIn(where, "fill", "the fill mode is not settable yet", - "remove the line - content is generated from the seed") + p.notYetIn(where, "fill", core.Says("recipe.TheFillModeIsNotSettable", "the fill mode is not settable yet"), + core.Says("recipe.RemoveTheLineContentIsGenerated", "remove the line - content is generated from the seed")) } } @@ -170,24 +168,24 @@ func (rt rawTarget) resolveSize(p *problems, where spot, count int, t *Target) { // and picking one of them quietly is how a recipe stops meaning what it // says. Every pairing is named rather than resolved. case rt.SizeRange != nil && rt.Size != nil: - p.add(where.of("size"), fmt.Sprintf("%s states both {a} {setting} and a size-range", where), - "one names an exact {setting} and the other draws one, so together they say two different things", - "keep {setting} for identical files, or keep size-range for a different {setting} each") + p.add(where.of("size"), core.Says("recipe.StatesBothSettingAndASize", "%s states both {a} {setting} and a size-range", core.A("Where", where)), + core.Says("recipe.OneNamesAnExactSettingAnd", "one names an exact {setting} and the other draws one, so together they say two different things"), + core.Says("recipe.KeepSettingForIdenticalFilesOr", "keep {setting} for identical files, or keep size-range for a different {setting} each")) case rt.SizeRange != nil && rt.Boundary != nil: - p.add(where.of("boundary"), fmt.Sprintf("%s states both {a} {setting} and a size-range", where), - "{a} {setting} is three chosen sizes around a limit, so drawing sizes as well would throw away the reason for choosing them", - "keep {setting} to test a limit, or keep size-range for files of varying size") + p.add(where.of("boundary"), core.Says("recipe.StatesBothSettingAndASize", "%s states both {a} {setting} and a size-range", core.A("Where", where)), + core.Says("recipe.SettingIsThreeChosenSizesAround", "{a} {setting} is three chosen sizes around a limit, so drawing sizes as well would throw away the reason for choosing them"), + core.Says("recipe.KeepSettingToTestALimit", "keep {setting} to test a limit, or keep size-range for files of varying size")) case rt.Boundary != nil && rt.Size != nil: - p.add(where.of("size"), fmt.Sprintf("%s states both {a} {setting} and a boundary", where), - "a boundary already decides the sizes, so {a} {setting} beside it means two different things at once", - "keep boundary for the three sizes around a limit, or keep {setting} for one exact {setting}") + p.add(where.of("size"), core.Says("recipe.StatesBothSettingAndABoundary", "%s states both {a} {setting} and a boundary", core.A("Where", where)), + core.Says("recipe.ABoundaryAlreadyDecidesTheSizes", "a boundary already decides the sizes, so {a} {setting} beside it means two different things at once"), + core.Says("recipe.KeepBoundaryForTheThreeSizes", "keep boundary for the three sizes around a limit, or keep {setting} for one exact {setting}")) case rt.Boundary != nil && rt.Count != nil: - p.add(where.of("count"), fmt.Sprintf("%s states both {a} {setting} and a boundary", where), - "a boundary set is exactly three files, one below the limit, one at it and one above", - "remove {setting}, or use size with {setting} to ask for identical files") + p.add(where.of("count"), core.Says("recipe.StatesBothSettingAndABoundary", "%s states both {a} {setting} and a boundary", core.A("Where", where)), + core.Says("recipe.ABoundarySetIsExactlyThree", "a boundary set is exactly three files, one below the limit, one at it and one above"), + core.Says("recipe.RemoveSettingOrUseSizeWith", "remove {setting}, or use size with {setting} to ask for identical files")) case rt.Boundary != nil: t.Size = boundaryText(p, where, rt.Boundary) @@ -204,17 +202,17 @@ func (rt rawTarget) resolveSize(p *problems, where spot, count int, t *Target) { case rt.Size != nil: s, ok := rt.Size.value() if !ok { - p.add(where.of("size"), fmt.Sprintf("%s has {a} {setting} that is neither text nor a number", where), - "{a} {setting} is written as 2mb or as a plain byte count", - "use size: 2mb or size: 2097152") + p.add(where.of("size"), core.Says("recipe.HasSettingThatIsNeitherText", "%s has {a} {setting} that is neither text nor a number", core.A("At", where)), + core.Says("recipe.SettingIsWrittenAs2mbOr", "{a} {setting} is written as 2mb or as a plain byte count"), + core.Says("recipe.UseSize2mbOrSize2097152", "use size: 2mb or size: 2097152")) break } t.Size = s n, err := core.ParseSize(s) if err != nil { - p.add(where.of("size"), fmt.Sprintf("%s: %v", where, err), - "units count in 1024s, so 10mb is 10485760 bytes", - "use {a} {setting} such as 2mb, 512kb or a plain byte count") + p.add(where.of("size"), core.Says("recipe.WhereError", "%s: %v", core.A("Where", where), core.A("Error", err)), + core.Says("recipe.UnitsCountIn1024sSo10mb", "units count in 1024s, so 10mb is 10485760 bytes"), + core.Says("recipe.UseSettingSuchAs2mb512kb", "use {a} {setting} such as 2mb, 512kb or a plain byte count")) break } for i := 0; i < count; i++ { @@ -228,9 +226,9 @@ func (rt rawTarget) resolveSize(p *problems, where spot, count int, t *Target) { // per file. text, ok := rt.SizeRange.value() if !ok { - p.add(where.of("size-range"), fmt.Sprintf("%s has {a} {setting} that is not text", where), - "a range is two sizes with a hyphen between them", - "use size-range: 1kb-8kb") + p.add(where.of("size-range"), core.Says("recipe.HasSettingThatIsNotText", "%s has {a} {setting} that is not text", core.A("Where", where)), + core.Says("recipe.ARangeIsTwoSizesWith", "a range is two sizes with a hyphen between them"), + core.Says("recipe.UseSizeRange1kb8kb", "use size-range: 1kb-8kb")) break } t.Size = text @@ -257,9 +255,9 @@ func (rt rawTarget) resolveSize(p *problems, where spot, count int, t *Target) { } default: - p.add(where.of("size"), fmt.Sprintf("%s has no {setting}", where), - "every target declares its {setting}, which is what lets a dry run report exact numbers before anything reaches the disk", - "add size: 2mb, size-range: 1kb-8kb, a boundary, contains, or a plain number of bytes") + p.add(where.of("size"), core.Says("recipe.HasNoSetting", "%s has no {setting}", core.A("At", where)), + core.Says("recipe.EveryTargetDeclaresItsSettingWhich", "every target declares its {setting}, which is what lets a dry run report exact numbers before anything reaches the disk"), + core.Says("recipe.AddSize2mbSizeRange1kb", "add size: 2mb, size-range: 1kb-8kb, a boundary, contains, or a plain number of bytes")) } } @@ -271,9 +269,9 @@ func (rt rawTarget) resolveSize(p *problems, where spot, count int, t *Target) { func parseSizeRange(p *problems, where spot, text string) (low, high int64, ok bool) { lo, hi, err := core.ParseSizeRange(text) if err != nil { - p.add(where.of("size-range"), fmt.Sprintf("%s: %v", where, err), - "both ends of a range are sizes, and units count in 1024s", - "use size-range: 1kb-8kb or a pair of plain byte counts") + p.add(where.of("size-range"), core.Says("recipe.WhereError", "%s: %v", core.A("Where", where), core.A("Error", err)), + core.Says("recipe.BothEndsOfARangeAre", "both ends of a range are sizes, and units count in 1024s"), + core.Says("recipe.UseSizeRange1kb8kbOr", "use size-range: 1kb-8kb or a pair of plain byte counts")) return 0, 0, false } return lo, hi, true @@ -306,10 +304,6 @@ var reasons = map[string]bool{ "malware_signature": true, "duplicate": true, "none": true, } -func reasonList() string { - return strings.Join(Reasons(), ", ") -} - // Reasons is the closed list, for the surfaces that have to offer it. // // Exported because the command line needs the same list the recipe uses. It @@ -374,9 +368,9 @@ func KnownOutcome(o string) bool { func boundaryText(p *problems, where spot, v *scalar) string { s, ok := v.value() if !ok { - p.add(where.of("boundary"), fmt.Sprintf("%s has {a} {setting} that is neither text nor a number", where), - "{a} {setting} is one size, and the set is built either side of it", - "use boundary: 10mb or a plain byte count") + p.add(where.of("boundary"), core.Says("recipe.HasSettingThatIsNeitherText", "%s has {a} {setting} that is neither text nor a number", core.A("At", where)), + core.Says("recipe.SettingIsOneSizeAndThe", "{a} {setting} is one size, and the set is built either side of it"), + core.Says("recipe.UseBoundary10mbOrAPlain", "use boundary: 10mb or a plain byte count")) return "" } return s @@ -395,21 +389,21 @@ func boundarySizes(p *problems, where spot, text string) []int64 { } limit, err := core.ParseBoundary(text) if err != nil { - p.add(where.of("boundary"), fmt.Sprintf("%s: %v", where, err), - "units count in 1024s, so 10mb is 10485760 bytes", - "use {a} {setting} such as 10mb, 512kb or a plain byte count") + p.add(where.of("boundary"), core.Says("recipe.WhereError", "%s: %v", core.A("Where", where), core.A("Error", err)), + core.Says("recipe.UnitsCountIn1024sSo10mb", "units count in 1024s, so 10mb is 10485760 bytes"), + core.Says("recipe.UseSettingSuchAs10mb512kb", "use {a} {setting} such as 10mb, 512kb or a plain byte count")) return nil } sizes, err := core.BoundarySizes(limit) if errors.Is(err, core.ErrBoundaryTooSmall) { - p.add(where.of("boundary"), fmt.Sprintf("%s has {a} {setting} of %d B", where, limit), + p.add(where.of("boundary"), core.Says("recipe.HasSettingOfB", "%s has {a} {setting} of %d B", core.A("Where", where), core.A("Limit", limit)), core.BoundaryTooSmallWhy, core.BoundaryTooSmallFix) return nil } if err != nil { - p.add(where.of("boundary"), fmt.Sprintf("%s has {a} {setting} of %d B", where, limit), - err.Error(), - "use {a} {setting} at least one byte below the largest number") + p.add(where.of("boundary"), core.Says("recipe.HasSettingOfB", "%s has {a} {setting} of %d B", core.A("Where", where), core.A("Limit", limit)), + core.SaidOf(err), + core.Says("recipe.UseSettingAtLeastOneByte", "use {a} {setting} at least one byte below the largest number")) return nil } return sizes @@ -429,16 +423,16 @@ func expectation(p *problems, where spot, v any) (string, string) { case map[string]any: o, ok := x["outcome"] if !ok { - p.add(where.of("expected"), fmt.Sprintf("%s declares an expectation with no outcome", where), - "an expectation says what the system under test should do with the file", - "add outcome: accept, reject, sanitize or unspecified") + p.add(where.of("expected"), core.Says("recipe.DeclaresAnExpectationWithNoOutcome", "%s declares an expectation with no outcome", core.A("Where", where)), + core.Says("recipe.AnExpectationSaysWhatTheSystem", "an expectation says what the system under test should do with the file"), + core.Says("recipe.AddOutcomeAcceptRejectSanitizeOr", "add outcome: accept, reject, sanitize or unspecified")) return "", "" } s, ok := scalarText(o) if !ok { - p.add(where.of("expected"), fmt.Sprintf("%s declares an outcome that is not a word", where), - "an outcome is one of four words", - "use accept, reject, sanitize or unspecified") + p.add(where.of("expected"), core.Says("recipe.DeclaresAnOutcomeThatIsNot", "%s declares an outcome that is not a word", core.A("Where", where)), + core.Says("recipe.AnOutcomeIsOneOfFour", "an outcome is one of four words"), + core.Says("recipe.UseAcceptRejectSanitizeOrUnspecified", "use accept, reject, sanitize or unspecified")) return "", "" } outcome = s @@ -449,30 +443,30 @@ func expectation(p *problems, where spot, v any) (string, string) { switch k { case "outcome", "reason": default: - p.add(where.of("expected"), fmt.Sprintf("%s declares %q inside its expectation", where, k), - "an expectation carries an outcome and a reason, and anything else would be dropped without a word", - "remove the line, or put the explanation in reason") + p.add(where.of("expected"), core.Says("recipe.DeclaresInsideItsExpectation", "%s declares %q inside its expectation", core.A("Where", where), core.A("K", k)), + core.Says("recipe.AnExpectationCarriesAnOutcomeAnd", "an expectation carries an outcome and a reason, and anything else would be dropped without a word"), + core.Says("recipe.RemoveTheLineOrPutThe", "remove the line, or put the explanation in reason")) } } if r, ok := x["reason"]; ok { s, ok := scalarText(r) if !ok { - p.add(where.of("expected.reason"), fmt.Sprintf("%s declares a reason that is not a word", where), - "a reason is one value from a closed list", - "use one of: "+reasonList()) + p.add(where.of("expected.reason"), core.Says("recipe.DeclaresAReasonThatIsNot", "%s declares a reason that is not a word", core.A("Where", where)), + core.Says("recipe.AReasonIsOneValueFrom", "a reason is one value from a closed list"), + core.Says("recipe.UseOneOfReasons", "use one of: %s", core.A("Reasons", core.Choices{Of: KeyExpectedReason, Values: Reasons()}))) } else if !reasons[s] { - p.add(where.of("expected.reason"), fmt.Sprintf("%s gives the reason %q, which is not on the list", where, s), - "the list is closed so that a report can group by reason, and a typo would make a category of one", - "use one of: "+reasonList()) + p.add(where.of("expected.reason"), core.Says("recipe.GivesTheReasonWhichIsNot", "%s gives the reason %q, which is not on the list", core.A("Where", where), core.A("Value", s)), + core.Says("recipe.TheListIsClosedSoThat", "the list is closed so that a report can group by reason, and a typo would make a category of one"), + core.Says("recipe.UseOneOfReasons", "use one of: %s", core.A("Reasons", core.Choices{Of: KeyExpectedReason, Values: Reasons()}))) } else { reason = s } } default: - p.add(where.of("expected"), fmt.Sprintf("%s declares an expectation this build cannot read", where), - "an expectation is either one word or a block with an outcome", - "use expected: accept, or a block with outcome:") + p.add(where.of("expected"), core.Says("recipe.DeclaresAnExpectationThisBuildCannot", "%s declares an expectation this build cannot read", core.A("Where", where)), + core.Says("recipe.AnExpectationIsEitherOneWord", "an expectation is either one word or a block with an outcome"), + core.Says("recipe.UseExpectedAcceptOrABlock", "use expected: accept, or a block with outcome:")) return "", "" } @@ -482,9 +476,9 @@ func expectation(p *problems, where spot, v any) (string, string) { case KnownOutcome(outcome): return outcome, reason default: - p.add(where.of("expected"), fmt.Sprintf("%s expects %q, which is not a known outcome", where, outcome), - "a typo accepted in silence becomes an expectation no test will ever check", - "use accept, reject, sanitize or unspecified") + p.add(where.of("expected"), core.Says("recipe.ExpectsWhichIsNotAKnown", "%s expects %q, which is not a known outcome", core.A("Where", where), core.A("Outcome", outcome)), + core.Says("recipe.ATypoAcceptedInSilenceBecomes", "a typo accepted in silence becomes an expectation no test will ever check"), + core.Says("recipe.UseAcceptRejectSanitizeOrUnspecified", "use accept, reject, sanitize or unspecified")) return "", "" } } @@ -507,9 +501,9 @@ func properties(p *problems, where spot, formatID string, in map[string]scalar) for _, k := range keys { s, ok := in[k].value() if !ok { - p.add(where.of("properties."+k), fmt.Sprintf("%s: property %q is a list or a block", where, k), - "a format property is a single value", - "give it one value, for example pages: 3") + p.add(where.of("properties."+k), core.Says("recipe.PropertyIsAListOrA", "%s: property %q is a list or a block", core.A("Where", where), core.A("K", k)), + core.Says("recipe.AFormatPropertyIsASingle", "a format property is a single value"), + core.Says("recipe.GiveItOneValueForExample2", "give it one value, for example pages: 3")) continue } out[k] = s @@ -553,25 +547,24 @@ func askTheFormat(p *problems, where spot, formatID string, stated map[string]st // Every problem this can return names its key. Kept as a branch // rather than assumed, because a third kind added without one would // otherwise vanish instead of arriving unaddressed. - p.add(where.of(KeyProperties), bad.Error(), "", "") + p.add(where.of(KeyProperties), core.SaidOf(bad), core.Said{}, core.Said{}) continue } at := where.of(KeyProperties + "." + about.AboutSetting()) var value *format.PropertyValueError if errors.As(bad, &value) { - p.add(at, fmt.Sprintf("%s: %s cannot be %q", where, value.Key, value.Value), - core.InTheWordsOf(value.Reason, value.Key), value.Remedy) + p.add(at, cannotBe(where, value), value.Reason, value.Remedy) continue } var unknown *format.UnknownPropertyError if errors.As(bad, &unknown) { - p.add(at, fmt.Sprintf("%s: %s", where, unknown.What()), - unknown.Why(), unknown.Instead()) + what, why, instead := unknown.Parts() + p.add(at, whereWhat(where, what), why, instead) continue } // A kind of problem this does not know the shape of. It still names its // box, and the sentence still arrives whole - an empty why would print // as a bare dash, which is what the first version of this did. - p.add(at, fmt.Sprintf("%s: %s", where, bad.Error()), "", "") + p.add(at, whereWhat(where, core.SaidOf(bad)), core.Said{}, core.Said{}) } } diff --git a/internal/tool/checksum/check.go b/internal/tool/checksum/check.go index b5cb13ee..319d2119 100644 --- a/internal/tool/checksum/check.go +++ b/internal/tool/checksum/check.go @@ -242,7 +242,9 @@ func checkResult(sumsPath string, parsed Sums, targets []target, answers []Probl notes = appendNote(notes, noteItself, data.NotChecked.Itself) notes = appendNote(notes, noteTwice, data.NotChecked.Twice) notes = appendNote(notes, noteNotChecksums, ranges(data.NotChecked.NotChecksums)) - notes = appendNote(notes, noteUnknown, data.NotChecked.Unknown) + if unknown := data.NotChecked.Unknown; len(unknown) > 0 { + notes = append(notes, tool.Noted{ID: noteUnknown, Items: unknown, Words: parsed.UnknownWords}) + } verdict := tool.Verdict{Outcome: tool.Match, About: filepath.Base(sumsPath), Listed: true} if len(data.Problems) > 0 { diff --git a/internal/tool/checksum/refusals.go b/internal/tool/checksum/refusals.go index 0eed2c72..0c9eed44 100644 --- a/internal/tool/checksum/refusals.go +++ b/internal/tool/checksum/refusals.go @@ -17,21 +17,24 @@ type UnknownAlgorithmError struct { func (e *UnknownAlgorithmError) AboutSetting() string { return SettingAlgorithm } // What happened. -func (e *UnknownAlgorithmError) What() string { - return fmt.Sprintf("there is no algorithm called %q", e.Name) +func (e *UnknownAlgorithmError) what() core.Said { + return core.Says("checksum.ThereIsNoAlgorithmCalled", "there is no algorithm called %q", core.A("Name", e.Name)) } // Why it is refused rather than matched to the nearest name. -func (e *UnknownAlgorithmError) Why() string { - return "an algorithm is named exactly, because two that sound alike give different checksums" +func (e *UnknownAlgorithmError) why() core.Said { + return core.Says("checksum.AnAlgorithmIsNamedExactlyBecause", "an algorithm is named exactly, because two that sound alike give different checksums") } // Instead is the list, from the table the tool works from. -func (e *UnknownAlgorithmError) Instead() string { - return "use " + strings.Join(Names(), ", ") + ", several separated by commas, or all on its own" +func (e *UnknownAlgorithmError) instead() core.Said { + return core.Says("checksum.UseAlgorithms", "use %s, several separated by commas, or all on its own", core.A("Algorithms", strings.Join(Names(), ", "))) } -func (e *UnknownAlgorithmError) Error() string { return tool.Sentence(e.What(), e.Why(), e.Instead()) } +func (e *UnknownAlgorithmError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *UnknownAlgorithmError) Said() core.Said { return tool.Said(e.what(), e.why(), e.instead()) } // Class says this is a mistake in the request. func (e *UnknownAlgorithmError) Class() tool.Class { return tool.Asked } @@ -48,28 +51,31 @@ type ExpectedError struct { func (e *ExpectedError) AboutSetting() string { return SettingExpect } // What happened. -func (e *ExpectedError) What() string { +func (e *ExpectedError) what() core.Said { if e.Digits == 0 { - return fmt.Sprintf("%q is not a checksum written in hexadecimal", e.Given) + return core.Says("checksum.IsNotAChecksumWrittenIn", "%q is not a checksum written in hexadecimal", core.A("Given", e.Given)) } - return fmt.Sprintf("a checksum %d digits long is not one this tool works out", e.Digits) + return core.Says("checksum.AChecksumDigitsLongIsNot", "a checksum %d digits long is not one this tool works out", core.A("Digits", e.Digits)) } // Why the length matters. -func (e *ExpectedError) Why() string { - return "the length of a checksum says which algorithm made it, and that is the one worked out to compare" +func (e *ExpectedError) why() core.Said { + return core.Says("checksum.TheLengthOfAChecksumSays", "the length of a checksum says which algorithm made it, and that is the one worked out to compare") } // Instead is the lengths there are. -func (e *ExpectedError) Instead() string { +func (e *ExpectedError) instead() core.Said { lengths := make([]string, 0, len(algorithms)) for _, a := range algorithms { lengths = append(lengths, fmt.Sprintf("%s %d", a.name, a.digits)) } - return "paste the checksum alone, without the file name. The lengths are: " + strings.Join(lengths, ", ") + return core.Says("checksum.PasteAlone", "paste the checksum alone, without the file name. The lengths are: %s", core.A("Lengths", strings.Join(lengths, ", "))) } -func (e *ExpectedError) Error() string { return tool.Sentence(e.What(), e.Why(), e.Instead()) } +func (e *ExpectedError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *ExpectedError) Said() core.Said { return tool.Said(e.what(), e.why(), e.instead()) } // Class says this is a mistake in the request. func (e *ExpectedError) Class() tool.Class { return tool.Asked } @@ -92,30 +98,33 @@ func (e *NotAFileError) AboutSetting() string { } // What happened. -func (e *NotAFileError) What() string { +func (e *NotAFileError) what() core.Said { if e.Directory && e.Input == InputChecksumFile { - return fmt.Sprintf("%s is a directory and this reads one checksum file", e.Path) + return core.Says("checksum.IsADirectoryAndThisReads", "%s is a directory and this reads one checksum file", core.A("Path", e.Path)) } if e.Directory { - return fmt.Sprintf("%s is a directory and this works out the checksum of one file", e.Path) + return core.Says("checksum.IsADirectoryAndThisWorks", "%s is a directory and this works out the checksum of one file", core.A("Path", e.Path)) } - return fmt.Sprintf("%s is not a file but a device, a pipe or a socket", e.Path) + return core.Says("checksum.IsNotAFileButA", "%s is not a file but a device, a pipe or a socket", core.A("Path", e.Path)) } // Why it is not read anyway. -func (e *NotAFileError) Why() string { +func (e *NotAFileError) why() core.Said { if e.Directory { - return "a directory has no bytes of its own to work a checksum out of" + return core.Says("checksum.ADirectoryHasNoBytesOf", "a directory has no bytes of its own to work a checksum out of") } - return "reading one may never end, so it is not started" + return core.Says("checksum.ReadingOneMayNeverEndSo", "reading one may never end, so it is not started") } // Instead is what to name. -func (e *NotAFileError) Instead() string { - return "name one file" +func (e *NotAFileError) instead() core.Said { + return core.Says("checksum.NameOneFile", "name one file") } -func (e *NotAFileError) Error() string { return tool.Sentence(e.What(), e.Why(), e.Instead()) } +func (e *NotAFileError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *NotAFileError) Said() core.Said { return tool.Said(e.what(), e.why(), e.instead()) } // Class says this is a mistake in the request: the path names something // that is there, and it is the wrong kind of thing. @@ -130,21 +139,24 @@ type ChangedError struct { func (e *ChangedError) AboutSetting() string { return InputFile } // What happened. -func (e *ChangedError) What() string { - return fmt.Sprintf("%s changed while it was being read", e.Path) +func (e *ChangedError) what() core.Said { + return core.Says("checksum.ChangedWhileItWasBeingRead", "%s changed while it was being read", core.A("Path", e.Path)) } // Why no checksum is given. -func (e *ChangedError) Why() string { - return "a checksum of a file being written is the checksum of no version of it" +func (e *ChangedError) why() core.Said { + return core.Says("checksum.AChecksumOfAFileBeing", "a checksum of a file being written is the checksum of no version of it") } // Instead is when to try again. -func (e *ChangedError) Instead() string { - return "wait until whatever is writing it has finished, and run this again" +func (e *ChangedError) instead() core.Said { + return core.Says("checksum.WaitUntilWhateverIsWritingIt", "wait until whatever is writing it has finished, and run this again") } -func (e *ChangedError) Error() string { return tool.Sentence(e.What(), e.Why(), e.Instead()) } +func (e *ChangedError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *ChangedError) Said() core.Said { return tool.Said(e.what(), e.why(), e.instead()) } // Class says the request was fine and the file did not hold still. func (e *ChangedError) Class() tool.Class { return tool.Reading } @@ -158,21 +170,24 @@ type NotAFolderError struct { func (e *NotAFolderError) AboutSetting() string { return InputFolder } // What happened. -func (e *NotAFolderError) What() string { - return fmt.Sprintf("%s is a file and this lists a folder", e.Path) +func (e *NotAFolderError) what() core.Said { + return core.Says("checksum.IsAFileAndThisLists", "%s is a file and this lists a folder", core.A("Path", e.Path)) } // Why a file will not do. -func (e *NotAFolderError) Why() string { - return "the checksum file is written beside the files it lists, so it needs the folder they are in" +func (e *NotAFolderError) why() core.Said { + return core.Says("checksum.TheChecksumFileIsWrittenBeside", "the checksum file is written beside the files it lists, so it needs the folder they are in") } // Instead is what to name. -func (e *NotAFolderError) Instead() string { - return "name the folder, or use tfg tool checksum for the one file" +func (e *NotAFolderError) instead() core.Said { + return core.Says("checksum.NameTheFolderOrUseTfg", "name the folder, or use tfg tool checksum for the one file") } -func (e *NotAFolderError) Error() string { return tool.Sentence(e.What(), e.Why(), e.Instead()) } +func (e *NotAFolderError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *NotAFolderError) Said() core.Said { return tool.Said(e.what(), e.why(), e.instead()) } // Class says this is a mistake in the request. func (e *NotAFolderError) Class() tool.Class { return tool.Asked } @@ -186,21 +201,24 @@ type SumsExistError struct { func (e *SumsExistError) AboutSetting() string { return InputFolder } // What happened. -func (e *SumsExistError) What() string { - return fmt.Sprintf("%s is already there, so nothing was written", e.Path) +func (e *SumsExistError) what() core.Said { + return core.Says("checksum.IsAlreadyThereSoNothingWas", "%s is already there, so nothing was written", core.A("Path", e.Path)) } // Why it is not written over. -func (e *SumsExistError) Why() string { - return "a checksum file is never written over, since it may be the only record of what the folder held" +func (e *SumsExistError) why() core.Said { + return core.Says("checksum.AChecksumFileIsNeverWritten", "a checksum file is never written over, since it may be the only record of what the folder held") } // Instead is what to do with it. -func (e *SumsExistError) Instead() string { - return "check the folder against it with tfg tool checksum-check, or move it away and run this again" +func (e *SumsExistError) instead() core.Said { + return core.Says("checksum.CheckTheFolderAgainstItWith", "check the folder against it with tfg tool checksum-check, or move it away and run this again") } -func (e *SumsExistError) Error() string { return tool.Sentence(e.What(), e.Why(), e.Instead()) } +func (e *SumsExistError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *SumsExistError) Said() core.Said { return tool.Said(e.what(), e.why(), e.instead()) } // Class says the request was fine and the folder is not in the state it needs. func (e *SumsExistError) Class() tool.Class { return tool.Reading } @@ -217,24 +235,27 @@ type NothingToListError struct { func (e *NothingToListError) AboutSetting() string { return InputFolder } // What happened. -func (e *NothingToListError) What() string { +func (e *NothingToListError) what() core.Said { if e.LeftOut > 0 { - return fmt.Sprintf("%s holds no file to list, only %s that are not files", e.Folder, core.Count(e.LeftOut, "name", "names")) + return core.Says("checksum.HoldsNoFileToListOnly", "%s holds no file to list, only %s that are not files", core.A("Folder", e.Folder), core.A("Names", core.SaysN("checksum.Names", "%d name", "%d names", core.A("Count", e.LeftOut)))) } - return fmt.Sprintf("%s holds no file to list", e.Folder) + return core.Says("checksum.HoldsNoFileToList", "%s holds no file to list", core.A("Folder", e.Folder)) } // Why an empty checksum file is not written. -func (e *NothingToListError) Why() string { - return "a checksum file without a line in it is refused by sha256sum -c, so it would check nothing" +func (e *NothingToListError) why() core.Said { + return core.Says("checksum.AChecksumFileWithoutALine", "a checksum file without a line in it is refused by sha256sum -c, so it would check nothing") } // Instead is what to name. -func (e *NothingToListError) Instead() string { - return "name a folder with at least one file in it" +func (e *NothingToListError) instead() core.Said { + return core.Says("checksum.NameAFolderWithAtLeast", "name a folder with at least one file in it") } -func (e *NothingToListError) Error() string { return tool.Sentence(e.What(), e.Why(), e.Instead()) } +func (e *NothingToListError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *NothingToListError) Said() core.Said { return tool.Said(e.what(), e.why(), e.instead()) } // Class says this is a mistake in the request. func (e *NothingToListError) Class() tool.Class { return tool.Asked } @@ -244,31 +265,36 @@ func (e *NothingToListError) Class() tool.Class { return tool.Asked } type FolderUnreadableError struct { Folder string // Problems is every one of them, each a path and what the system said. - Problems []string + Problems []core.Said } // AboutSetting is the input this is about. func (e *FolderUnreadableError) AboutSetting() string { return InputFolder } // What happened. -func (e *FolderUnreadableError) What() string { - return fmt.Sprintf("not everything under %s could be read, so nothing was written", e.Folder) +func (e *FolderUnreadableError) what() core.Said { + return core.Says("checksum.NotEverythingUnderCouldBeRead", "not everything under %s could be read, so nothing was written", core.A("Folder", e.Folder)) } // Why a checksum file with a gap is not written. -func (e *FolderUnreadableError) Why() string { - return "a checksum file promises every file in the folder, and one with a gap in it would not say where the gap is" +func (e *FolderUnreadableError) why() core.Said { + return core.Says("checksum.AChecksumFilePromisesEveryFile", "a checksum file promises every file in the folder, and one with a gap in it would not say where the gap is") } // Instead is what to do. The list follows it. -func (e *FolderUnreadableError) Instead() string { - return "make these readable or move them out of the folder, and run this again" +func (e *FolderUnreadableError) instead() core.Said { + return core.Says("checksum.MakeTheseReadableOrMoveThem", "make these readable or move them out of the folder, and run this again") } // Error is the sentence and then every problem on a line of its own, so none // of them is left for the next try to find. -func (e *FolderUnreadableError) Error() string { - return tool.Sentence(e.What(), e.Why(), e.Instead()) + ":\n " + strings.Join(e.Problems, "\n ") +func (e *FolderUnreadableError) Error() string { return e.Said().String() } + +// Said is the whole refusal with its list, for a window that says it in its +// own language. +func (e *FolderUnreadableError) Said() core.Said { + return core.Says("checksum.FolderUnreadable", "%s:\n %s", + core.A("Sentence", tool.Said(e.what(), e.why(), e.instead())), core.A("Problems", core.Lines(e.Problems))) } // Class says the request was fine and the disk did not cooperate. @@ -281,21 +307,24 @@ type NoRoomError struct { } // What happened. -func (e *NoRoomError) What() string { - return fmt.Sprintf("there is no room for %s, so nothing was written", e.Path) +func (e *NoRoomError) what() core.Said { + return core.Says("checksum.ThereIsNoRoomForSo", "there is no room for %s, so nothing was written", core.A("Path", e.Path)) } // Why, with the two numbers. -func (e *NoRoomError) Why() string { - return fmt.Sprintf("it takes %s and the disk has %s free", core.HumanBytes(e.Need), core.HumanBytes(e.Have)) +func (e *NoRoomError) why() core.Said { + return core.Says("checksum.ItTakesAndTheDiskHas", "it takes %s and the disk has %s free", core.A("Need", core.HumanBytes(e.Need)), core.A("Have", core.HumanBytes(e.Have))) } // Instead is where to find the room. -func (e *NoRoomError) Instead() string { - return "free some space on that disk and run this again" +func (e *NoRoomError) instead() core.Said { + return core.Says("checksum.FreeSomeSpaceOnThatDisk", "free some space on that disk and run this again") } -func (e *NoRoomError) Error() string { return tool.Sentence(e.What(), e.Why(), e.Instead()) } +func (e *NoRoomError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *NoRoomError) Said() core.Said { return tool.Said(e.what(), e.why(), e.instead()) } // Class says the disk is full, which has a code of its own. func (e *NoRoomError) Class() tool.Class { return tool.Room } @@ -310,22 +339,24 @@ type SumsTooLargeError struct { func (e *SumsTooLargeError) AboutSetting() string { return InputChecksumFile } // What happened. -func (e *SumsTooLargeError) What() string { - return fmt.Sprintf("%s is %s, larger than a checksum file this reads", e.Path, core.HumanBytes(e.Bytes)) +func (e *SumsTooLargeError) what() core.Said { + return core.Says("checksum.IsLargerThanAChecksumFile", "%s is %s, larger than a checksum file this reads", core.A("Path", e.Path), core.A("Bytes", core.HumanBytes(e.Bytes))) } // Why there is a limit, and what it is. -func (e *SumsTooLargeError) Why() string { - return fmt.Sprintf("the most is %s, about half a million files, and a larger file is more likely not a checksum file at all", - core.HumanBytes(sumsMostBytes)) +func (e *SumsTooLargeError) why() core.Said { + return core.Says("checksum.TheMostIsAboutHalfA", "the most is %s, about half a million files, and a larger file is more likely not a checksum file at all", core.A("SumsMostBytes", core.HumanBytes(sumsMostBytes))) } // Instead is how to check a folder that large. -func (e *SumsTooLargeError) Instead() string { - return "check the folder in parts, each with a checksum file of its own" +func (e *SumsTooLargeError) instead() core.Said { + return core.Says("checksum.CheckTheFolderInPartsEach", "check the folder in parts, each with a checksum file of its own") } -func (e *SumsTooLargeError) Error() string { return tool.Sentence(e.What(), e.Why(), e.Instead()) } +func (e *SumsTooLargeError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *SumsTooLargeError) Said() core.Said { return tool.Said(e.what(), e.why(), e.instead()) } // Class says the file handed in is not one this takes. func (e *SumsTooLargeError) Class() tool.Class { return tool.Reading } @@ -339,21 +370,90 @@ type NoSumsError struct { func (e *NoSumsError) AboutSetting() string { return InputChecksumFile } // What happened. -func (e *NoSumsError) What() string { - return fmt.Sprintf("%s holds no checksum line", e.Path) +func (e *NoSumsError) what() core.Said { + return core.Says("checksum.HoldsNoChecksumLine", "%s holds no checksum line", core.A("Path", e.Path)) } // Why that is refused rather than passed. -func (e *NoSumsError) Why() string { - return "a checksum line is a checksum and a path, written as hex path or as SHA256 (path) = hex, and without one there is nothing to check" +func (e *NoSumsError) why() core.Said { + return core.Says("checksum.AChecksumLineIsAChecksum", "a checksum line is a checksum and a path, written as hex path or as SHA256 (path) = hex, and without one there is nothing to check") } // Instead is what to name. -func (e *NoSumsError) Instead() string { - return "name the checksum file itself, such as SHA256SUMS, rather than a page or a signature beside it" +func (e *NoSumsError) instead() core.Said { + return core.Says("checksum.NameTheChecksumFileItselfSuch", "name the checksum file itself, such as SHA256SUMS, rather than a page or a signature beside it") } -func (e *NoSumsError) Error() string { return tool.Sentence(e.What(), e.Why(), e.Instead()) } +func (e *NoSumsError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *NoSumsError) Said() core.Said { return tool.Said(e.what(), e.why(), e.instead()) } // Class says the file handed in is not one this can use. func (e *NoSumsError) Class() tool.Class { return tool.Reading } + +// What, Why and Instead are the parts in English, for a report that lays +// them out apart. +func (e *ChangedError) What() string { return e.what().String() } +func (e *ChangedError) Why() string { return e.why().String() } +func (e *ChangedError) Instead() string { return e.instead().String() } + +// What, Why and Instead are the parts in English, for a report that lays +// them out apart. +func (e *ExpectedError) What() string { return e.what().String() } +func (e *ExpectedError) Why() string { return e.why().String() } +func (e *ExpectedError) Instead() string { return e.instead().String() } + +// What, Why and Instead are the parts in English, for a report that lays +// them out apart. +func (e *FolderUnreadableError) What() string { return e.what().String() } +func (e *FolderUnreadableError) Why() string { return e.why().String() } +func (e *FolderUnreadableError) Instead() string { return e.instead().String() } + +// What, Why and Instead are the parts in English, for a report that lays +// them out apart. +func (e *NoRoomError) What() string { return e.what().String() } +func (e *NoRoomError) Why() string { return e.why().String() } +func (e *NoRoomError) Instead() string { return e.instead().String() } + +// What, Why and Instead are the parts in English, for a report that lays +// them out apart. +func (e *NoSumsError) What() string { return e.what().String() } +func (e *NoSumsError) Why() string { return e.why().String() } +func (e *NoSumsError) Instead() string { return e.instead().String() } + +// What, Why and Instead are the parts in English, for a report that lays +// them out apart. +func (e *NotAFileError) What() string { return e.what().String() } +func (e *NotAFileError) Why() string { return e.why().String() } +func (e *NotAFileError) Instead() string { return e.instead().String() } + +// What, Why and Instead are the parts in English, for a report that lays +// them out apart. +func (e *NotAFolderError) What() string { return e.what().String() } +func (e *NotAFolderError) Why() string { return e.why().String() } +func (e *NotAFolderError) Instead() string { return e.instead().String() } + +// What, Why and Instead are the parts in English, for a report that lays +// them out apart. +func (e *NothingToListError) What() string { return e.what().String() } +func (e *NothingToListError) Why() string { return e.why().String() } +func (e *NothingToListError) Instead() string { return e.instead().String() } + +// What, Why and Instead are the parts in English, for a report that lays +// them out apart. +func (e *SumsExistError) What() string { return e.what().String() } +func (e *SumsExistError) Why() string { return e.why().String() } +func (e *SumsExistError) Instead() string { return e.instead().String() } + +// What, Why and Instead are the parts in English, for a report that lays +// them out apart. +func (e *SumsTooLargeError) What() string { return e.what().String() } +func (e *SumsTooLargeError) Why() string { return e.why().String() } +func (e *SumsTooLargeError) Instead() string { return e.instead().String() } + +// What, Why and Instead are the parts in English, for a report that lays +// them out apart. +func (e *UnknownAlgorithmError) What() string { return e.what().String() } +func (e *UnknownAlgorithmError) Why() string { return e.why().String() } +func (e *UnknownAlgorithmError) Instead() string { return e.instead().String() } diff --git a/internal/tool/checksum/sums.go b/internal/tool/checksum/sums.go index 0fe051aa..c3616987 100644 --- a/internal/tool/checksum/sums.go +++ b/internal/tool/checksum/sums.go @@ -4,10 +4,11 @@ import ( "bufio" "bytes" "errors" - "fmt" "io" "os" "strings" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // The checksum file: the format GNU coreutils writes and reads, and the one @@ -96,6 +97,8 @@ type Sums struct { // Unknown are lines of an algorithm this tool does not work out, as // "line N: NAME" - a sha224, a BLAKE2b. Unknown []string + // UnknownWords is Unknown as sentences, one for each, for a window. + UnknownWords []core.Said `json:"-"` } // ParseSums reads a checksum file, line by line, never keeping more of a line @@ -129,7 +132,9 @@ func (s *Sums) add(number int, line []byte, tooLong bool) { entry, unknown, ok := readEntry(string(line)) switch { case unknown != "": - s.Unknown = append(s.Unknown, fmt.Sprintf("line %d: %s", number, unknown)) + line := core.Says("checksum.UnknownLine", "line %d: %s", core.A("Line", number), core.A("Name", unknown)) + s.Unknown = append(s.Unknown, line.String()) + s.UnknownWords = append(s.UnknownWords, line) case ok: entry.Line = number s.Listed = append(s.Listed, entry) diff --git a/internal/tool/checksum/write.go b/internal/tool/checksum/write.go index eed3d036..9f388a67 100644 --- a/internal/tool/checksum/write.go +++ b/internal/tool/checksum/write.go @@ -217,10 +217,10 @@ func hashAll(ctx context.Context, dir string, files []audit.Entry, algorithm str return nil, stopped } sums := make([]string, len(answers)) - var problems []string + var problems []core.Said for i, a := range answers { if a.err != nil { - problems = append(problems, files[i].Path+" - "+a.err.Error()) + problems = append(problems, core.Says("checksum.PathProblem", "%s - %v", core.A("Path", files[i].Path), core.A("Error", a.err))) } sums[i] = a.sum } @@ -231,10 +231,10 @@ func hashAll(ctx context.Context, dir string, files []audit.Entry, algorithm str } // problemsOf is each error as a line of a refusal. -func problemsOf(errs []error) []string { - out := make([]string, 0, len(errs)) +func problemsOf(errs []error) []core.Said { + out := make([]core.Said, 0, len(errs)) for _, err := range errs { - out = append(out, err.Error()) + out = append(out, core.SaidOf(err)) } return out } diff --git a/internal/tool/refusals.go b/internal/tool/refusals.go index 4129f340..244539de 100644 --- a/internal/tool/refusals.go +++ b/internal/tool/refusals.go @@ -2,16 +2,18 @@ package tool import ( "errors" - "fmt" "strings" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) -// Sentence is a refusal of a tool as one line of the command line: what -// happened, why, and what to do instead - the shape "PNG cannot be smaller -// than 74 B - ... Ask for 74 B or more" already has. One function, so every -// tool refuses in the same shape without each writing it. -func Sentence(what, why, instead string) string { - return what + " - " + why + ". " + strings.ToUpper(instead[:1]) + instead[1:] +// Said is a refusal of a tool in one sentence: what happened, why, and what to +// do instead - the shape "PNG cannot be smaller than 74 B - ... Ask for 74 B or +// more" already has. One function, so every tool refuses in the same shape +// without each writing it. What to do starts with a capital in every language, +// because it starts a sentence. +func Said(what, why, instead core.Said) core.Said { + return core.Says("tool.Sentence", "%s - %s. %s", core.A("What", what), core.A("Why", why), core.A("Instead", instead.Capitalized())) } // Class is what kind of mistake a refusal is, which is what the command line @@ -46,24 +48,27 @@ type UnknownError struct { } // What happened, without the list of names. -func (e *UnknownError) What() string { - return fmt.Sprintf("there is no tool called %q", e.ID) +func (e *UnknownError) what() core.Said { + return core.Says("tool.ThereIsNoToolCalled", "there is no tool called %q", core.A("ID", e.ID)) } // Why this is refused rather than guessed at. -func (e *UnknownError) Why() string { - return "a tool is named exactly, and the nearest name to a mistyped one may do something else" +func (e *UnknownError) why() core.Said { + return core.Says("tool.AToolIsNamedExactlyAnd", "a tool is named exactly, and the nearest name to a mistyped one may do something else") } // Instead names what there is, from the registry rather than from a list. -func (e *UnknownError) Instead() string { +func (e *UnknownError) instead() core.Said { if len(e.Known) == 0 { - return "this build has no tools" + return core.Says("tool.ThisBuildHasNoTools", "this build has no tools") } - return "use one of: " + strings.Join(e.Known, ", ") + return core.Says("tool.UseOneOf", "use one of: %s", core.A("Known", strings.Join(e.Known, ", "))) } -func (e *UnknownError) Error() string { return Sentence(e.What(), e.Why(), e.Instead()) } +func (e *UnknownError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *UnknownError) Said() core.Said { return Said(e.what(), e.why(), e.instead()) } // Class says this is a mistake in the request. func (e *UnknownError) Class() Class { return Asked } @@ -111,21 +116,36 @@ func (e *MissingInputError) AboutSetting() string { return e.Input.Name } // What happened. Worded for both surfaces - a window shows this under the // box and the command line prints it - so it names neither a flag nor a // button. -func (e *MissingInputError) What() string { - return fmt.Sprintf("no %s was given", e.Input.Kind) +func (e *MissingInputError) what() core.Said { + return core.Says("tool.NoWasGiven", "no %s was given", core.A("Kind", core.Term{Key: core.KindKey(string(e.Input.Kind)), Text: string(e.Input.Kind)})) } // Why this is refused rather than started. -func (e *MissingInputError) Why() string { - return "the tool has nothing to read without one" +func (e *MissingInputError) why() core.Said { + return core.Says("tool.TheToolHasNothingToRead", "the tool has nothing to read without one") } // Instead is what to give it. -func (e *MissingInputError) Instead() string { - return fmt.Sprintf("name the %s to work on", e.Input.Kind) +func (e *MissingInputError) instead() core.Said { + return core.Says("tool.NameTheToWorkOn", "name the %s to work on", core.A("Kind", core.Term{Key: core.KindKey(string(e.Input.Kind)), Text: string(e.Input.Kind)})) } -func (e *MissingInputError) Error() string { return Sentence(e.What(), e.Why(), e.Instead()) } +func (e *MissingInputError) Error() string { return e.Said().String() } + +// Said is the whole refusal, for a window that says it in its own language. +func (e *MissingInputError) Said() core.Said { return Said(e.what(), e.why(), e.instead()) } // Class says this is a mistake in the request. func (e *MissingInputError) Class() Class { return Asked } + +// What, Why and Instead are the parts in English, for a report that lays +// them out apart. +func (e *MissingInputError) What() string { return e.what().String() } +func (e *MissingInputError) Why() string { return e.why().String() } +func (e *MissingInputError) Instead() string { return e.instead().String() } + +// What, Why and Instead are the parts in English, for a report that lays +// them out apart. +func (e *UnknownError) What() string { return e.what().String() } +func (e *UnknownError) Why() string { return e.why().String() } +func (e *UnknownError) Instead() string { return e.instead().String() } diff --git a/internal/tool/tool.go b/internal/tool/tool.go index d44eb9fa..dc01930c 100644 --- a/internal/tool/tool.go +++ b/internal/tool/tool.go @@ -27,6 +27,8 @@ import ( "sync" "github.com/donislawdev/TestingFilesGenerator/internal/format" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" ) // InputKind is what an input names on the disk. @@ -129,6 +131,10 @@ type Noted struct { // Items are data - paths, line numbers, a count - shown as they are in // every language. Items []string + // Words is the items in words, one for each, where an item is more than + // data - "line 3: blake2b" - so a window says it in its own language. + // Empty where every item is data. + Words []core.Said } // Request is one run of a tool: the paths it works on and the settings it was @@ -370,7 +376,7 @@ func (d Descriptor) Start(ctx context.Context, in Request, progress Progress) (R // show a key. Refused loudly, as a fault in the build, rather than shown. for _, n := range result.Notes { if d.NoteSays(n.ID) == "" { - return Result{}, fmt.Errorf("the tool %s said a note it never declared (%s). This is a fault in the build", d.ID, n.ID) + return Result{}, core.Defect(fmt.Errorf("the tool %s said a note it never declared (%s). This is a fault in the build", d.ID, n.ID)) } } return result, nil