bad3fa4035
Same nil-safe shape as the four families in phraser: a floor holding the exact literals that lived in Go, a load-time placeholder check on every entry whose job is to read the aggregate back, and S/IsS for the callers and their tests. No call site moved yet.
192 lines
7.6 KiB
Go
192 lines
7.6 KiB
Go
package say
|
|
|
|
// The summary sentences — what she says around aggregated data: the morning
|
|
// plan, the ranked task list, and the habits read back out of behaviour records.
|
|
//
|
|
// Fifth family on the deck, and the first one outside internal/phraser. It
|
|
// lives here because its three callers — internal/morning, internal/tasks and
|
|
// internal/memory — sit under phraser in the import graph and cannot reach it.
|
|
//
|
|
// The "I have not seen enough yet" sentences are the load-bearing ones. Three
|
|
// days of taps and a year of them produce the same "обычно ты ...", and only one
|
|
// of those is worth believing, so the empty cases say she has not seen a
|
|
// pattern rather than that he has none.
|
|
|
|
import (
|
|
_ "embed"
|
|
"log"
|
|
"math/rand"
|
|
"sync"
|
|
)
|
|
|
|
//go:embed summary_ru_v1.json
|
|
var summaryJSON []byte
|
|
|
|
// SummarySchemaVersion — this family's own version.
|
|
const SummarySchemaVersion = 1
|
|
|
|
// The entry keys.
|
|
const (
|
|
PlanRestEmpty = "plan_rest_empty"
|
|
PlanDayEmpty = "plan_day_empty"
|
|
PlanDay = "plan_day"
|
|
PlanUncertain = "plan_uncertain"
|
|
|
|
TasksNone = "tasks_none"
|
|
TasksFirst = "tasks_first"
|
|
TasksCandidates = "tasks_candidates"
|
|
|
|
ReasonOverdue = "reason_overdue"
|
|
ReasonOverdueDay = "reason_overdue_day"
|
|
ReasonOverdueDays = "reason_overdue_days"
|
|
ReasonToday = "reason_today"
|
|
ReasonTomorrow = "reason_tomorrow"
|
|
ReasonInDays = "reason_in_days"
|
|
ReasonImportant = "reason_important"
|
|
ReasonUrgent = "reason_urgent"
|
|
ReasonStale = "reason_stale"
|
|
|
|
HabitWeekday = "habit_weekday"
|
|
HabitWeekdaySame = "habit_weekday_same"
|
|
HabitWeekdayNone = "habit_weekday_none"
|
|
HabitWeekendBoth = "habit_weekend_both"
|
|
HabitWeekendSat = "habit_weekend_sat"
|
|
HabitWeekendSun = "habit_weekend_sun"
|
|
HabitWeekendSame = "habit_weekend_same"
|
|
HabitWeekendNone = "habit_weekend_none"
|
|
HabitOverall = "habit_overall"
|
|
HabitOverallNone = "habit_overall_none"
|
|
HabitSpanToday = "habit_span_today"
|
|
HabitSpanDays = "habit_span_days"
|
|
HabitUnglossed = "habit_unglossed"
|
|
HabitAt = "habit_at"
|
|
)
|
|
|
|
var summaryKeys = []string{
|
|
PlanRestEmpty, PlanDayEmpty, PlanDay, PlanUncertain,
|
|
TasksNone, TasksFirst, TasksCandidates,
|
|
ReasonOverdue, ReasonOverdueDay, ReasonOverdueDays, ReasonToday, ReasonTomorrow,
|
|
ReasonInDays, ReasonImportant, ReasonUrgent, ReasonStale,
|
|
HabitWeekday, HabitWeekdaySame, HabitWeekdayNone,
|
|
HabitWeekendBoth, HabitWeekendSat, HabitWeekendSun, HabitWeekendSame, HabitWeekendNone,
|
|
HabitOverall, HabitOverallNone, HabitSpanToday, HabitSpanDays,
|
|
HabitUnglossed, HabitAt,
|
|
}
|
|
|
|
// summaryFloor — the literal each key falls back to when the file is unusable.
|
|
// These are the exact strings that lived in Go before this file existed.
|
|
var summaryFloor = RegisterFloor(map[string]string{
|
|
PlanRestEmpty: "на сегодня больше ничего не запланировано.",
|
|
PlanDayEmpty: "на {date} ничего не запланировано.",
|
|
PlanDay: "план на {date}: {items}.",
|
|
PlanUncertain: "похоже, {line}",
|
|
|
|
TasksNone: "задач нет.",
|
|
TasksFirst: "сначала: {items}.",
|
|
TasksCandidates: "ещё я нашла, но ты не подтвердил: {items}.",
|
|
|
|
ReasonOverdue: "просрочено",
|
|
ReasonOverdueDay: "просрочено на день",
|
|
ReasonOverdueDays: "просрочено на {n} дн.",
|
|
ReasonToday: "сегодня",
|
|
ReasonTomorrow: "завтра",
|
|
ReasonInDays: "через {n} дн.",
|
|
ReasonImportant: "важно",
|
|
ReasonUrgent: "срочно",
|
|
ReasonStale: "давно в списке",
|
|
|
|
HabitWeekday: "по {day} ты обычно {items}.",
|
|
HabitWeekdaySame: "по {day} у тебя нет ничего особенного — то же, что и в остальные дни: {items}.",
|
|
HabitWeekdayNone: "по {day} я пока не вижу у тебя ничего постоянного.",
|
|
HabitWeekendBoth: "по субботам ты обычно {sat}, по воскресеньям — {sun}.",
|
|
HabitWeekendSat: "по субботам ты обычно {items}, а по воскресеньям ничего постоянного.",
|
|
HabitWeekendSun: "по воскресеньям ты обычно {items}, а по субботам ничего постоянного.",
|
|
HabitWeekendSame: "по выходным у тебя нет ничего особенного — то же, что и в остальные дни: {items}.",
|
|
HabitWeekendNone: "по выходным я пока не вижу у тебя ничего постоянного.",
|
|
HabitOverall: "обычно ты {items} — {span}.",
|
|
HabitOverallNone: "я ещё не набрала достаточно записей, чтобы говорить о привычках.",
|
|
HabitSpanToday: "по записям за сегодня",
|
|
HabitSpanDays: "по записям за последние {n} {word}",
|
|
HabitUnglossed: "отмечаешь «{key}»",
|
|
HabitAt: "{gloss} около {time}",
|
|
})
|
|
|
|
// Summaries picks a hand-written Russian summary sentence. Safe for concurrent
|
|
// use.
|
|
type Summaries struct{ d *Deck }
|
|
|
|
// LoadSummaries reads the embedded file. Pass a source to make the picking
|
|
// reproducible in tests; nil seeds from the clock.
|
|
func LoadSummaries(src rand.Source) (*Summaries, error) {
|
|
d, err := Load(summaryJSON, SummarySchemaVersion, summaryKeys, summaryFloor, src)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// The entries that exist to read the aggregate back. A variant without the
|
|
// placeholder would summarise the data by dropping it.
|
|
for _, req := range []struct{ key, ph string }{
|
|
{PlanDayEmpty, "{date}"}, {PlanDay, "{date}"}, {PlanDay, "{items}"},
|
|
{PlanUncertain, "{line}"},
|
|
{TasksFirst, "{items}"}, {TasksCandidates, "{items}"},
|
|
{ReasonOverdueDays, "{n}"}, {ReasonInDays, "{n}"},
|
|
{HabitWeekday, "{day}"}, {HabitWeekday, "{items}"},
|
|
{HabitWeekdaySame, "{day}"}, {HabitWeekdaySame, "{items}"},
|
|
{HabitWeekdayNone, "{day}"},
|
|
{HabitWeekendBoth, "{sat}"}, {HabitWeekendBoth, "{sun}"},
|
|
{HabitWeekendSat, "{items}"}, {HabitWeekendSun, "{items}"},
|
|
{HabitWeekendSame, "{items}"},
|
|
{HabitOverall, "{items}"}, {HabitOverall, "{span}"},
|
|
{HabitSpanDays, "{n}"}, {HabitSpanDays, "{word}"},
|
|
{HabitUnglossed, "{key}"}, {HabitAt, "{gloss}"}, {HabitAt, "{time}"},
|
|
} {
|
|
if err := d.RequirePlaceholder(req.key, req.ph); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &Summaries{d: d}, nil
|
|
}
|
|
|
|
// deck reads through a nil *Summaries, which is the unloadable-file case.
|
|
func (s *Summaries) deck() *Deck {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
return s.d
|
|
}
|
|
|
|
// Say returns one line for key, with the values filled into the frame.
|
|
func (s *Summaries) Say(key string, vars map[string]string) string {
|
|
return s.deck().Text(key, vars)
|
|
}
|
|
|
|
// Variants returns every line the file can produce, for the persona scorer.
|
|
func (s *Summaries) Variants() []string { return s.deck().Variants() }
|
|
|
|
var (
|
|
summaryOnce sync.Once
|
|
summaries *Summaries
|
|
)
|
|
|
|
// DefaultSummaries returns the shared instance, loading it on first use. A
|
|
// broken file logs once and leaves a nil *Summaries, which still answers from
|
|
// summaryFloor.
|
|
func DefaultSummaries() *Summaries {
|
|
summaryOnce.Do(func() {
|
|
s, err := LoadSummaries(nil)
|
|
if err != nil {
|
|
log.Printf("say: summary lines unavailable, using the built-in ones: %v", err)
|
|
return
|
|
}
|
|
summaries = s
|
|
})
|
|
return summaries
|
|
}
|
|
|
|
// S — one summary sentence, the way every caller says it.
|
|
func S(key string, vars map[string]string) string { return DefaultSummaries().Say(key, vars) }
|
|
|
|
// IsS reports whether text is a line key could have produced, for the tests.
|
|
func IsS(key string, vars map[string]string, text string) bool {
|
|
return DefaultSummaries().deck().Matches(key, vars, text)
|
|
}
|