// Package lang är språksystemet: ordboken över japanska skyltord, spelarens // kunskap, tokenisering av skylttext och den delvisa översättning spelaren // ser. Ren Go – inga Ebitengine-beroenden. package lang import ( "encoding/json" "fmt" "sort" "strings" "unicode" ) // Entry är ett ord i ordboken. type Entry struct { Word string `json:"-"` Kana string `json:"kana"` Romaji string `json:"romaji"` En string `json:"en"` Sv string `json:"sv"` Tier int `json:"tier"` } // Translation returnerar översättningen på UI-språket (sv/en) med fallback. func (e *Entry) Translation(lang string) string { if lang == "en" || e.Sv == "" { return e.En } return e.Sv } // Dictionary är ordboken. Orden hålls sorterade längst först för // längsta-matchning vid tokenisering. type Dictionary struct { entries map[string]*Entry words []string } // NewDictionary skapar en tom ordbok (praktiskt i tester). func NewDictionary() *Dictionary { return &Dictionary{entries: map[string]*Entry{}} } // ParseDictionary läser jp-dictionary.json och validerar att varje post har // kana, romaji, en, sv och tier. Nycklar som börjar med "_" ignoreras. func ParseDictionary(data []byte) (*Dictionary, error) { var raw map[string]json.RawMessage if err := json.Unmarshal(data, &raw); err != nil { return nil, fmt.Errorf("lang: ogiltig ordbok: %w", err) } d := NewDictionary() for word, msg := range raw { if strings.HasPrefix(word, "_") { continue } var e Entry if err := json.Unmarshal(msg, &e); err != nil { return nil, fmt.Errorf("lang: ordet %q: %w", word, err) } if e.Kana == "" || e.Romaji == "" || e.En == "" || e.Sv == "" || e.Tier < 1 { return nil, fmt.Errorf("lang: ordet %q saknar kana/romaji/en/sv/tier", word) } if !IsJapanese(word) { return nil, fmt.Errorf("lang: nyckeln %q innehåller inte japanska tecken", word) } d.Add(word, e) } return d, nil } // Add lägger till eller ersätter ett ord. func (d *Dictionary) Add(word string, e Entry) { e.Word = word if _, exists := d.entries[word]; !exists { d.words = append(d.words, word) sort.Slice(d.words, func(i, j int) bool { ri, rj := len([]rune(d.words[i])), len([]rune(d.words[j])) if ri != rj { return ri > rj } return d.words[i] < d.words[j] }) } d.entries[word] = &e } // Lookup returnerar posten för ett ord, eller nil. func (d *Dictionary) Lookup(word string) *Entry { return d.entries[word] } // Len är antalet ord. func (d *Dictionary) Len() int { return len(d.entries) } // Words returnerar alla ord, längsta först. func (d *Dictionary) Words() []string { return append([]string(nil), d.words...) } // IsKana rapporterar om tecknet är hiragana eller katakana (inkl. ー). func IsKana(r rune) bool { return unicode.Is(unicode.Hiragana, r) || unicode.Is(unicode.Katakana, r) || r == 'ー' } // IsKanji rapporterar om tecknet är ett han-tecken. func IsKanji(r rune) bool { return unicode.Is(unicode.Han, r) || r == '々' } // IsJapanese rapporterar om strängen innehåller minst ett kana- eller kanji-tecken. func IsJapanese(s string) bool { for _, r := range s { if IsKana(r) || IsKanji(r) { return true } } return false } // HasKana rapporterar om ordet innehåller kana. func HasKana(s string) bool { for _, r := range s { if IsKana(r) { return true } } return false } // IsAllKana rapporterar om ordet bara består av kana. func IsAllKana(s string) bool { if s == "" { return false } for _, r := range s { if !IsKana(r) { return false } } return true }