morning, tasks, memory: say the summaries from the file (V-506)
The three callers now read their sentences out of summary_ru_v1.json: the
plan lines in morning.Plan.FormatRU, the list and reason words in
tasks.FormatRU, and the habit readouts in memory.Profile.
Two behaviour_test assertions moved from substring to say.IsS, because the
habit gaps have variants now and a substring pins one of them. The
"по {day} у тебя обычно" variant was dropped on sight: the activities are
verbs, so it read "у тебя обычно тренируешься".
The persona scorer covers the family, and a new test asserts every gap
variant still says she has not seen enough rather than that he has nothing.
This commit is contained in:
+30
-20
@@ -3,8 +3,11 @@ package memory
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/say"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Behavioural memory — "what do I usually do?" (Vikunja #254).
|
// Behavioural memory — "what do I usually do?" (Vikunja #254).
|
||||||
@@ -337,13 +340,14 @@ func (p Profile) FormatWeekdayRU(wd time.Weekday) string {
|
|||||||
day := weekdayRU[int(wd)%7]
|
day := weekdayRU[int(wd)%7]
|
||||||
acts := p.Weekly[wd]
|
acts := p.Weekly[wd]
|
||||||
if len(acts) > 0 {
|
if len(acts) > 0 {
|
||||||
return fmt.Sprintf("по %s ты обычно %s.", day, joinActivities(acts))
|
return say.S(say.HabitWeekday, map[string]string{"day": day, "items": joinActivities(acts)})
|
||||||
}
|
}
|
||||||
if len(p.Everyday) > 0 {
|
if len(p.Everyday) > 0 {
|
||||||
return fmt.Sprintf("по %s у тебя нет ничего особенного — то же, что и в остальные дни: %s.",
|
return say.S(say.HabitWeekdaySame, map[string]string{
|
||||||
day, joinActivities(p.Everyday))
|
"day": day, "items": joinActivities(p.Everyday),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("по %s я пока не вижу у тебя ничего постоянного.", day)
|
return say.S(say.HabitWeekdayNone, map[string]string{"day": day})
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatWeekendRU reads back what distinguishes Saturday and Sunday.
|
// FormatWeekendRU reads back what distinguishes Saturday and Sunday.
|
||||||
@@ -355,19 +359,17 @@ func (p Profile) FormatWeekendRU() string {
|
|||||||
sat, sun := p.Weekly[time.Saturday], p.Weekly[time.Sunday]
|
sat, sun := p.Weekly[time.Saturday], p.Weekly[time.Sunday]
|
||||||
switch {
|
switch {
|
||||||
case len(sat) > 0 && len(sun) > 0:
|
case len(sat) > 0 && len(sun) > 0:
|
||||||
return fmt.Sprintf("по субботам ты обычно %s, по воскресеньям — %s.",
|
return say.S(say.HabitWeekendBoth, map[string]string{
|
||||||
joinActivities(sat), joinActivities(sun))
|
"sat": joinActivities(sat), "sun": joinActivities(sun),
|
||||||
|
})
|
||||||
case len(sat) > 0:
|
case len(sat) > 0:
|
||||||
return fmt.Sprintf("по субботам ты обычно %s, а по воскресеньям ничего постоянного.",
|
return say.S(say.HabitWeekendSat, map[string]string{"items": joinActivities(sat)})
|
||||||
joinActivities(sat))
|
|
||||||
case len(sun) > 0:
|
case len(sun) > 0:
|
||||||
return fmt.Sprintf("по воскресеньям ты обычно %s, а по субботам ничего постоянного.",
|
return say.S(say.HabitWeekendSun, map[string]string{"items": joinActivities(sun)})
|
||||||
joinActivities(sun))
|
|
||||||
case len(p.Everyday) > 0:
|
case len(p.Everyday) > 0:
|
||||||
return fmt.Sprintf("по выходным у тебя нет ничего особенного — то же, что и в остальные дни: %s.",
|
return say.S(say.HabitWeekendSame, map[string]string{"items": joinActivities(p.Everyday)})
|
||||||
joinActivities(p.Everyday))
|
|
||||||
}
|
}
|
||||||
return "по выходным я пока не вижу у тебя ничего постоянного."
|
return say.S(say.HabitWeekendNone, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatOverallRU reads back the habits that hold across the whole week, and
|
// FormatOverallRU reads back the habits that hold across the whole week, and
|
||||||
@@ -378,19 +380,23 @@ func (p Profile) FormatWeekendRU() string {
|
|||||||
// a year of them, and only one of those is worth believing.
|
// a year of them, and only one of those is worth believing.
|
||||||
func (p Profile) FormatOverallRU() string {
|
func (p Profile) FormatOverallRU() string {
|
||||||
if len(p.All) == 0 {
|
if len(p.All) == 0 {
|
||||||
return "я ещё не набрала достаточно записей, чтобы говорить о привычках."
|
return say.S(say.HabitOverallNone, nil)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("обычно ты %s — %s.", joinActivities(p.All), p.spanRU())
|
return say.S(say.HabitOverall, map[string]string{
|
||||||
|
"items": joinActivities(p.All), "span": p.spanRU(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// spanRU — "по записям за последние N дней", or a vaguer phrase when the window
|
// spanRU — "по записям за последние N дней", or a vaguer phrase when the window
|
||||||
// is too short to name in days.
|
// is too short to name in days.
|
||||||
func (p Profile) spanRU() string {
|
func (p Profile) spanRU() string {
|
||||||
if p.Since.IsZero() || !p.Until.After(p.Since) {
|
if p.Since.IsZero() || !p.Until.After(p.Since) {
|
||||||
return "по записям за сегодня"
|
return say.S(say.HabitSpanToday, nil)
|
||||||
}
|
}
|
||||||
days := int(p.Until.Sub(p.Since).Hours()/24) + 1
|
days := int(p.Until.Sub(p.Since).Hours()/24) + 1
|
||||||
return fmt.Sprintf("по записям за последние %d %s", days, pluralDaysRU(days))
|
return say.S(say.HabitSpanDays, map[string]string{
|
||||||
|
"n": strconv.Itoa(days), "word": pluralDaysRU(days),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// pluralDaysRU — the Russian count form of "день" for n.
|
// pluralDaysRU — the Russian count form of "день" for n.
|
||||||
@@ -423,14 +429,18 @@ func joinActivities(acts []Activity) string {
|
|||||||
// come from the model, so an unglossed one is as likely to be
|
// come from the model, so an unglossed one is as likely to be
|
||||||
// "выпил_воды" as a noun, and "обычно ты выпил_воды около 09:00" is
|
// "выпил_воды" as a noun, and "обычно ты выпил_воды около 09:00" is
|
||||||
// not a sentence.
|
// not a sentence.
|
||||||
gloss = fmt.Sprintf("отмечаешь «%s»", strings.ReplaceAll(a.Key, "_", " "))
|
gloss = say.S(say.HabitUnglossed, map[string]string{
|
||||||
|
"key": strings.ReplaceAll(a.Key, "_", " "),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if !a.HasTypical {
|
if !a.HasTypical {
|
||||||
parts[i] = gloss
|
parts[i] = gloss
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
parts[i] = fmt.Sprintf("%s около %02d:%02d", gloss,
|
parts[i] = say.S(say.HabitAt, map[string]string{
|
||||||
int(a.TypicalAt.Hours()), int(a.TypicalAt.Minutes())%60)
|
"gloss": gloss,
|
||||||
|
"time": fmt.Sprintf("%02d:%02d", int(a.TypicalAt.Hours()), int(a.TypicalAt.Minutes())%60),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if len(parts) == 1 {
|
if len(parts) == 1 {
|
||||||
return parts[0]
|
return parts[0]
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/say"
|
||||||
)
|
)
|
||||||
|
|
||||||
// habitHistory — n weeks of the same weekday, at the given local time.
|
// habitHistory — n weeks of the same weekday, at the given local time.
|
||||||
@@ -70,7 +72,7 @@ func TestBuildProfileNeedsMoreThanOneDay(t *testing.T) {
|
|||||||
if len(p.All) != 0 || len(p.Weekly) != 0 {
|
if len(p.All) != 0 || len(p.Weekly) != 0 {
|
||||||
t.Fatalf("one day of rows must produce no habit: %+v / %+v", p.All, p.Weekly)
|
t.Fatalf("one day of rows must produce no habit: %+v / %+v", p.All, p.Weekly)
|
||||||
}
|
}
|
||||||
if got := p.FormatOverallRU(); !strings.Contains(got, "не набрала достаточно") {
|
if got := p.FormatOverallRU(); !say.IsS(say.HabitOverallNone, nil, got) {
|
||||||
t.Errorf("empty profile reads %q", got)
|
t.Errorf("empty profile reads %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,7 +205,9 @@ func TestWeekdayProfileExcludesEverydayHabits(t *testing.T) {
|
|||||||
// A day with nothing of its own says so rather than reciting water as if
|
// A day with nothing of its own says so rather than reciting water as if
|
||||||
// Wednesday were the reason for it.
|
// Wednesday were the reason for it.
|
||||||
wed := p.FormatWeekdayRU(time.Wednesday)
|
wed := p.FormatWeekdayRU(time.Wednesday)
|
||||||
if !strings.Contains(wed, "ничего особенного") || !strings.Contains(wed, "воду") {
|
if !say.IsS(say.HabitWeekdaySame, map[string]string{
|
||||||
|
"day": "средам", "items": "пьёшь воду около 13:30",
|
||||||
|
}, wed) {
|
||||||
t.Fatalf("plain weekday readout should say the day is unremarkable and name the daily habits: %q", wed)
|
t.Fatalf("plain weekday readout should say the day is unremarkable and name the daily habits: %q", wed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/say"
|
||||||
"github.com/kami/maven/internal/store"
|
"github.com/kami/maven/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -164,17 +165,20 @@ func (p Plan) FormatRU() string {
|
|||||||
// it is over, and saying it was empty is a false statement about a day
|
// it is over, and saying it was empty is a false statement about a day
|
||||||
// he just lived.
|
// he just lived.
|
||||||
if p.Rest {
|
if p.Rest {
|
||||||
return "на сегодня больше ничего не запланировано."
|
return say.S(say.PlanRestEmpty, nil)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("на %s ничего не запланировано.", p.Date.Format("02.01.2006"))
|
return say.S(say.PlanDayEmpty, map[string]string{"date": p.Date.Format("02.01.2006")})
|
||||||
}
|
}
|
||||||
parts := make([]string, len(p.Items))
|
parts := make([]string, len(p.Items))
|
||||||
for i, it := range p.Items {
|
for i, it := range p.Items {
|
||||||
line := fmt.Sprintf("%s — %s", it.At.Format("15:04"), it.Text)
|
line := fmt.Sprintf("%s — %s", it.At.Format("15:04"), it.Text)
|
||||||
if it.Uncertain {
|
if it.Uncertain {
|
||||||
line = "похоже, " + line
|
line = say.S(say.PlanUncertain, map[string]string{"line": line})
|
||||||
}
|
}
|
||||||
parts[i] = line
|
parts[i] = line
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("план на %s: %s.", p.Date.Format("02.01.2006"), strings.Join(parts, "; "))
|
return say.S(say.PlanDay, map[string]string{
|
||||||
|
"date": p.Date.Format("02.01.2006"),
|
||||||
|
"items": strings.Join(parts, "; "),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/kami/maven/internal/phraser"
|
"github.com/kami/maven/internal/phraser"
|
||||||
|
"github.com/kami/maven/internal/say"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestFallbackPersona scores every line in every hand-written family on the
|
// TestFallbackPersona scores every line in every hand-written family on the
|
||||||
@@ -43,6 +44,11 @@ func TestFallbackPersona(t *testing.T) {
|
|||||||
t.Fatalf("LoadActs: %v", err)
|
t.Fatalf("LoadActs: %v", err)
|
||||||
}
|
}
|
||||||
variants = append(variants, act.Variants()...)
|
variants = append(variants, act.Variants()...)
|
||||||
|
sum, err := say.LoadSummaries(rand.NewSource(20260804))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadSummaries: %v", err)
|
||||||
|
}
|
||||||
|
variants = append(variants, sum.Variants()...)
|
||||||
if len(variants) == 0 {
|
if len(variants) == 0 {
|
||||||
t.Fatal("no variants — the file loaded empty")
|
t.Fatal("no variants — the file loaded empty")
|
||||||
}
|
}
|
||||||
@@ -51,7 +57,8 @@ func TestFallbackPersona(t *testing.T) {
|
|||||||
body := v
|
body := v
|
||||||
for _, ph := range []string{"{sources}", "{key}", "{value}", "{fn}", "{text}", "{when}", "{items}",
|
for _, ph := range []string{"{sources}", "{key}", "{value}", "{fn}", "{text}", "{when}", "{items}",
|
||||||
"{location}", "{temp}", "{condition}", "{tail}", "{out}", "{name}",
|
"{location}", "{temp}", "{condition}", "{tail}", "{out}", "{name}",
|
||||||
"{entity}", "{count}", "{word}"} {
|
"{entity}", "{count}", "{word}",
|
||||||
|
"{date}", "{line}", "{n}", "{day}", "{sat}", "{sun}", "{span}", "{gloss}", "{time}"} {
|
||||||
body = strings.ReplaceAll(body, ph, "вода")
|
body = strings.ReplaceAll(body, ph, "вода")
|
||||||
}
|
}
|
||||||
for _, r := range RunChecks(Case{}, body, "neutral") {
|
for _, r := range RunChecks(Case{}, body, "neutral") {
|
||||||
|
|||||||
@@ -78,7 +78,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"habit_weekday": {
|
"habit_weekday": {
|
||||||
"variants": ["по {day} ты обычно {items}.", "по {day} у тебя обычно {items}."]
|
"fixed": true,
|
||||||
|
"variants": ["по {day} ты обычно {items}."]
|
||||||
},
|
},
|
||||||
"habit_weekday_same": {
|
"habit_weekday_same": {
|
||||||
"variants": [
|
"variants": [
|
||||||
@@ -89,7 +90,7 @@
|
|||||||
"habit_weekday_none": {
|
"habit_weekday_none": {
|
||||||
"variants": [
|
"variants": [
|
||||||
"по {day} я пока не вижу у тебя ничего постоянного.",
|
"по {day} я пока не вижу у тебя ничего постоянного.",
|
||||||
"по {day} я пока не набрала записей, чтобы говорить о постоянном."
|
"по {day} у тебя пока ничего постоянного не вижу — записей мало."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"habit_weekend_both": {
|
"habit_weekend_both": {
|
||||||
@@ -110,7 +111,7 @@
|
|||||||
"habit_weekend_none": {
|
"habit_weekend_none": {
|
||||||
"variants": [
|
"variants": [
|
||||||
"по выходным я пока не вижу у тебя ничего постоянного.",
|
"по выходным я пока не вижу у тебя ничего постоянного.",
|
||||||
"по выходным я пока не набрала записей, чтобы говорить о постоянном."
|
"по выходным у тебя пока ничего постоянного не вижу — записей мало."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"habit_overall": {
|
"habit_overall": {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package say
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The file has to load, and every key the code names has to be in it.
|
||||||
|
func TestSummariesLoad(t *testing.T) {
|
||||||
|
s, err := LoadSummaries(rand.NewSource(1))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load: %v", err)
|
||||||
|
}
|
||||||
|
for _, key := range summaryKeys {
|
||||||
|
if got := s.Say(key, nil); got == "" {
|
||||||
|
t.Errorf("%s says nothing", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A nil *Summaries is the unloadable-file case, and it must still speak. The
|
||||||
|
// habit sentences are the ones that matter here: falling back must not turn
|
||||||
|
// "I have not seen enough" into silence.
|
||||||
|
func TestNilSummariesAnswerFromTheFloor(t *testing.T) {
|
||||||
|
var s *Summaries
|
||||||
|
if got, want := s.Say(HabitOverallNone, nil), summaryFloor[HabitOverallNone]; got != want {
|
||||||
|
t.Errorf("got %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
if got := s.Say(PlanDay, map[string]string{"date": "03.08.2026", "items": "x"}); !strings.Contains(got, "03.08.2026") {
|
||||||
|
t.Errorf("the floor dropped the date: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The empty cases claim she has not seen enough, never that he has no habits.
|
||||||
|
// Every variant has to hold that line, since the picker treats them as equals.
|
||||||
|
func TestHabitGapsSaySheHasNotSeenEnough(t *testing.T) {
|
||||||
|
s, err := LoadSummaries(rand.NewSource(1))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load: %v", err)
|
||||||
|
}
|
||||||
|
for _, key := range []string{HabitWeekdayNone, HabitWeekendNone, HabitOverallNone} {
|
||||||
|
for _, v := range s.d.file.Entries[key].Variants {
|
||||||
|
if !strings.Contains(v, "пока") && !strings.Contains(v, "ещё") {
|
||||||
|
t.Errorf("%s variant %q reads as a fact about him, not as a gap in her records", key, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-16
@@ -19,8 +19,11 @@ package tasks
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/say"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Status values, mirroring internal/store so a caller can rank ipc.Task rows
|
// Status values, mirroring internal/store so a caller can rank ipc.Task rows
|
||||||
@@ -117,21 +120,21 @@ func score(it Item, now time.Time) (float64, string) {
|
|||||||
bonus = scoreOverdueCap
|
bonus = scoreOverdueCap
|
||||||
}
|
}
|
||||||
total += scoreOverdue + bonus
|
total += scoreOverdue + bonus
|
||||||
reason = "просрочено"
|
reason = say.S(say.ReasonOverdue, nil)
|
||||||
if late == 1 {
|
if late == 1 {
|
||||||
reason = "просрочено на день"
|
reason = say.S(say.ReasonOverdueDay, nil)
|
||||||
} else if late > 1 {
|
} else if late > 1 {
|
||||||
reason = fmt.Sprintf("просрочено на %d дн.", late)
|
reason = say.S(say.ReasonOverdueDays, map[string]string{"n": strconv.Itoa(late)})
|
||||||
}
|
}
|
||||||
case days == 0:
|
case days == 0:
|
||||||
total += scoreDueToday
|
total += scoreDueToday
|
||||||
reason = "сегодня"
|
reason = say.S(say.ReasonToday, nil)
|
||||||
case days == 1:
|
case days == 1:
|
||||||
total += scoreDueTomorrow
|
total += scoreDueTomorrow
|
||||||
reason = "завтра"
|
reason = say.S(say.ReasonTomorrow, nil)
|
||||||
case days <= 7:
|
case days <= 7:
|
||||||
total += scoreDueWeek
|
total += scoreDueWeek
|
||||||
reason = fmt.Sprintf("через %d дн.", days)
|
reason = say.S(say.ReasonInDays, map[string]string{"n": strconv.Itoa(days)})
|
||||||
default:
|
default:
|
||||||
total += scoreDueLater
|
total += scoreDueLater
|
||||||
}
|
}
|
||||||
@@ -147,9 +150,9 @@ func score(it Item, now time.Time) (float64, string) {
|
|||||||
// The rungs get their own words. The reason string is the one place
|
// The rungs get their own words. The reason string is the one place
|
||||||
// the ranking explains itself, and reading "важно" back at a task
|
// the ranking explains itself, and reading "важно" back at a task
|
||||||
// he flagged "срочно" reports a word he did not say.
|
// he flagged "срочно" reports a word he did not say.
|
||||||
reason = "важно"
|
reason = say.S(say.ReasonImportant, nil)
|
||||||
if w >= MaxWeight {
|
if w >= MaxWeight {
|
||||||
reason = "срочно"
|
reason = say.S(say.ReasonUrgent, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,7 +166,7 @@ func score(it Item, now time.Time) (float64, string) {
|
|||||||
}
|
}
|
||||||
total += age
|
total += age
|
||||||
if reason == "" && weeks >= 2 {
|
if reason == "" && weeks >= 2 {
|
||||||
reason = "давно в списке"
|
reason = say.S(say.ReasonStale, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,22 +213,22 @@ func FormatRU(ranked []Ranked) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(open) == 0 && len(cands) == 0 {
|
if len(open) == 0 && len(cands) == 0 {
|
||||||
return "задач нет."
|
return say.S(say.TasksNone, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
if len(open) > 0 {
|
if len(open) > 0 {
|
||||||
b.WriteString("сначала: ")
|
b.WriteString(say.S(say.TasksFirst, map[string]string{
|
||||||
b.WriteString(joinRU(open, SpokenLimit, true))
|
"items": joinRU(open, SpokenLimit, true),
|
||||||
b.WriteString(".")
|
}))
|
||||||
}
|
}
|
||||||
if len(cands) > 0 {
|
if len(cands) > 0 {
|
||||||
if b.Len() > 0 {
|
if b.Len() > 0 {
|
||||||
b.WriteString(" ")
|
b.WriteString(" ")
|
||||||
}
|
}
|
||||||
b.WriteString("ещё я нашла, но ты не подтвердил: ")
|
b.WriteString(say.S(say.TasksCandidates, map[string]string{
|
||||||
b.WriteString(joinRU(cands, SpokenLimit, false))
|
"items": joinRU(cands, SpokenLimit, false),
|
||||||
b.WriteString(".")
|
}))
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user