Make LANGUAGE support work

This commit is contained in:
Daniel Oaks 2018-01-22 17:30:31 +10:00
parent a7fdade41d
commit e99f22488f
8 changed files with 255 additions and 14 deletions

@ -131,6 +131,11 @@ var Commands = map[string]Command{
minParams: 1, minParams: 1,
oper: true, oper: true,
}, },
"LANGUAGE": {
handler: languageHandler,
usablePreReg: true,
minParams: 1,
},
"LIST": { "LIST": {
handler: listHandler, handler: listHandler,
minParams: 0, minParams: 0,

@ -11,6 +11,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"path/filepath"
"strings" "strings"
"time" "time"
@ -143,6 +144,15 @@ type StackImpactConfig struct {
AppName string `yaml:"app-name"` AppName string `yaml:"app-name"`
} }
// LangData is the data contained in a language file.
type LangData struct {
Name string
Code string
Maintainers string
Incomplete bool
Translations map[string]string
}
// Config defines the overall configuration. // Config defines the overall configuration.
type Config struct { type Config struct {
Network struct { Network struct {
@ -170,6 +180,8 @@ type Config struct {
Languages struct { Languages struct {
Enabled bool Enabled bool
Path string Path string
Default string
Data map[string]LangData
} }
Datastore struct { Datastore struct {
@ -470,5 +482,72 @@ func LoadConfig(filename string) (config *Config, err error) {
return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error()) return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())
} }
// get language files
config.Languages.Data = make(map[string]LangData)
if config.Languages.Enabled {
files, err := ioutil.ReadDir(config.Languages.Path)
if err != nil {
return nil, fmt.Errorf("Could not load language files: %s", err.Error())
}
for _, f := range files {
// skip dirs
if f.IsDir() {
continue
}
// only load .lang.yaml files
name := f.Name()
if !strings.HasSuffix(strings.ToLower(name), ".lang.yaml") {
continue
}
data, err = ioutil.ReadFile(filepath.Join(config.Languages.Path, name))
if err != nil {
return nil, fmt.Errorf("Could not load language file [%s]: %s", name, err.Error())
}
var langInfo LangData
err = yaml.Unmarshal(data, &langInfo)
if err != nil {
return nil, fmt.Errorf("Could not parse language file [%s]: %s", name, err.Error())
}
// confirm that values are correct
if langInfo.Code == "en" {
return nil, fmt.Errorf("Cannot have language file with code 'en' (this is the default language using strings inside the server code). If you're making an English variant, name it with a more specific code")
}
if langInfo.Code == "" || langInfo.Name == "" || langInfo.Maintainers == "" {
return nil, fmt.Errorf("Code, name or maintainers is empty in language file [%s]", name)
}
if len(langInfo.Translations) == 0 {
return nil, fmt.Errorf("Language file [%s] contains no translations", name)
}
// check for duplicate languages
_, exists := config.Languages.Data[strings.ToLower(langInfo.Code)]
if exists {
return nil, fmt.Errorf("Language code [%s] defined twice", langInfo.Code)
}
// and insert into lang info
config.Languages.Data[strings.ToLower(langInfo.Code)] = langInfo
}
// confirm that default language exists
if config.Languages.Default == "" {
config.Languages.Default = "en"
} else {
config.Languages.Default = strings.ToLower(config.Languages.Default)
}
_, exists := config.Languages.Data[config.Languages.Default]
if config.Languages.Default != "en" && !exists {
return nil, fmt.Errorf("Cannot find default language [%s]", config.Languages.Default)
}
}
return config, nil return config, nil
} }

@ -249,6 +249,11 @@ ON <server> specifies that the ban is to be set on that specific server.
[reason] and [oper reason], if they exist, are separated by a vertical bar (|). [reason] and [oper reason], if they exist, are separated by a vertical bar (|).
If "KLINE LIST" is sent, the server sends back a list of our current KLINEs.`, If "KLINE LIST" is sent, the server sends back a list of our current KLINEs.`,
},
"language": {
text: `LANGUAGE <code>{ <code>}
Sets your preferred languages to the given ones.`,
}, },
"list": { "list": {
text: `LIST [<channel>{,<channel>}] [<elistcond>{,<elistcond>}] text: `LIST [<channel>{,<channel>}] [<elistcond>{,<elistcond>}]

@ -4,30 +4,72 @@
package irc package irc
import ( import (
"strings"
"sync" "sync"
) )
// LanguageManager manages our languages and provides translation abilities. // LanguageManager manages our languages and provides translation abilities.
type LanguageManager struct { type LanguageManager struct {
sync.RWMutex sync.RWMutex
langMap map[string]map[string]string Info map[string]LangData
translations map[string]map[string]string
} }
// NewLanguageManager returns a new LanguageManager. // NewLanguageManager returns a new LanguageManager.
func NewLanguageManager() *LanguageManager { func NewLanguageManager(languageData map[string]LangData) *LanguageManager {
lm := LanguageManager{ lm := LanguageManager{
langMap: make(map[string]map[string]string), Info: make(map[string]LangData),
translations: make(map[string]map[string]string),
} }
//TODO(dan): load language files here // make fake "en" info
lm.Info["en"] = LangData{
Code: "en",
Name: "English",
Maintainers: "Oragono contributors and the IRC community",
}
// load language data
for name, data := range languageData {
lm.Info[name] = data
lm.translations[name] = data.Translations
}
return &lm return &lm
} }
// Count returns how many languages we have.
func (lm *LanguageManager) Count() int {
lm.RLock()
defer lm.RUnlock()
return len(lm.Info)
}
// Codes returns the proper language codes for the given casefolded language codes.
func (lm *LanguageManager) Codes(codes []string) []string {
lm.RLock()
defer lm.RUnlock()
var newCodes []string
for _, code := range codes {
info, exists := lm.Info[code]
if exists {
newCodes = append(newCodes, info.Code)
}
}
if len(newCodes) == 0 {
newCodes = []string{"en"}
}
return newCodes
}
// Translate returns the given string, translated into the given language. // Translate returns the given string, translated into the given language.
func (lm *LanguageManager) Translate(languages []string, originalString string) string { func (lm *LanguageManager) Translate(languages []string, originalString string) string {
// not using any special languages // not using any special languages
if len(languages) == 0 { if len(languages) == 0 || languages[0] == "en" || len(lm.translations) == 0 {
return originalString return originalString
} }
@ -35,12 +77,17 @@ func (lm *LanguageManager) Translate(languages []string, originalString string)
defer lm.RUnlock() defer lm.RUnlock()
for _, lang := range languages { for _, lang := range languages {
langMap, exists := lm.langMap[lang] lang = strings.ToLower(lang)
if lang == "en" {
return originalString
}
translations, exists := lm.translations[lang]
if !exists { if !exists {
continue continue
} }
newString, exists := langMap[originalString] newString, exists := translations[originalString]
if !exists { if !exists {
continue continue
} }

@ -161,6 +161,8 @@ const (
ERR_HELPNOTFOUND = "524" ERR_HELPNOTFOUND = "524"
ERR_CANNOTSENDRP = "573" ERR_CANNOTSENDRP = "573"
RPL_WHOISSECURE = "671" RPL_WHOISSECURE = "671"
RPL_YOURLANGUAGESARE = "687"
RPL_WHOISLANGUAGE = "690"
RPL_HELPSTART = "704" RPL_HELPSTART = "704"
RPL_HELPTXT = "705" RPL_HELPTXT = "705"
RPL_ENDOFHELP = "706" RPL_ENDOFHELP = "706"
@ -188,4 +190,6 @@ const (
RPL_REG_VERIFICATION_REQUIRED = "927" RPL_REG_VERIFICATION_REQUIRED = "927"
ERR_REG_INVALID_CRED_TYPE = "928" ERR_REG_INVALID_CRED_TYPE = "928"
ERR_REG_INVALID_CALLBACK = "929" ERR_REG_INVALID_CALLBACK = "929"
ERR_TOOMANYLANGUAGES = "981"
ERR_NOLANGUAGE = "982"
) )

@ -154,7 +154,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
commands: make(chan Command), commands: make(chan Command),
connectionLimiter: connection_limits.NewLimiter(), connectionLimiter: connection_limits.NewLimiter(),
connectionThrottler: connection_limits.NewThrottler(), connectionThrottler: connection_limits.NewThrottler(),
languages: NewLanguageManager(), languages: NewLanguageManager(config.Languages.Data),
listeners: make(map[string]*ListenerWrapper), listeners: make(map[string]*ListenerWrapper),
logger: logger, logger: logger,
monitorManager: NewMonitorManager(), monitorManager: NewMonitorManager(),
@ -984,6 +984,9 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
} }
func (client *Client) getWhoisOf(target *Client) { func (client *Client) getWhoisOf(target *Client) {
target.stateMutex.RLock()
defer target.stateMutex.RUnlock()
client.Send(nil, client.server.name, RPL_WHOISUSER, client.nick, target.nick, target.username, target.hostname, "*", target.realname) client.Send(nil, client.server.name, RPL_WHOISUSER, client.nick, target.nick, target.username, target.hostname, "*", target.realname)
whoischannels := client.WhoisChannelsNames(target) whoischannels := client.WhoisChannelsNames(target)
@ -1002,6 +1005,16 @@ func (client *Client) getWhoisOf(target *Client) {
if target.flags[Bot] { if target.flags[Bot] {
client.Send(nil, client.server.name, RPL_WHOISBOT, client.nick, target.nick, ircfmt.Unescape("is a $bBot$b on ")+client.server.networkName) client.Send(nil, client.server.name, RPL_WHOISBOT, client.nick, target.nick, ircfmt.Unescape("is a $bBot$b on ")+client.server.networkName)
} }
if 0 < len(target.languages) {
params := []string{client.nick, target.nick}
for _, str := range client.server.languages.Codes(target.languages) {
params = append(params, str)
}
params = append(params, "can speak these languages")
client.Send(nil, client.server.name, RPL_WHOISLANGUAGE, params...)
}
if target.certfp != "" && (client.flags[Operator] || client == target) { if target.certfp != "" && (client.flags[Operator] || client == target) {
client.Send(nil, client.server.name, RPL_WHOISCERTFP, client.nick, target.nick, fmt.Sprintf("has client certificate fingerprint %s", target.certfp)) client.Send(nil, client.server.name, RPL_WHOISCERTFP, client.nick, target.nick, fmt.Sprintf("has client certificate fingerprint %s", target.certfp))
} }
@ -1237,6 +1250,25 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
removedCaps := caps.NewSet() removedCaps := caps.NewSet()
updatedCaps := caps.NewSet() updatedCaps := caps.NewSet()
// Translations
currentLanguageValue, _ := CapValues.Get(caps.Languages)
langCodes := []string{strconv.Itoa(len(config.Languages.Data) + 1), "en"}
for _, info := range config.Languages.Data {
if info.Incomplete {
langCodes = append(langCodes, "~"+info.Code)
} else {
langCodes = append(langCodes, info.Code)
}
}
newLanguageValue := strings.Join(langCodes, ",")
server.logger.Debug("rehash", "Languages:", newLanguageValue)
if currentLanguageValue != newLanguageValue {
updatedCaps.Add(caps.Languages)
CapValues.Set(caps.Languages, newLanguageValue)
}
// SASL // SASL
if config.Accounts.AuthenticationEnabled && !server.accountAuthenticationEnabled { if config.Accounts.AuthenticationEnabled && !server.accountAuthenticationEnabled {
// enabling SASL // enabling SASL
@ -2077,6 +2109,62 @@ func userhostHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool
return false return false
} }
// LANGUAGE <code>{ <code>}
func languageHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
alreadyDoneLanguages := make(map[string]bool)
var appliedLanguages []string
supportedLanguagesCount := server.languages.Count()
if supportedLanguagesCount < len(msg.Params) {
client.Send(nil, client.server.name, ERR_TOOMANYLANGUAGES, client.nick, strconv.Itoa(supportedLanguagesCount), "You specified too many languages")
return false
}
for _, value := range msg.Params {
value = strings.ToLower(value)
// strip ~ from the language if it has it
value = strings.TrimPrefix(value, "~")
// silently ignore empty languages or those with spaces in them
if len(value) == 0 || strings.Contains(value, " ") {
continue
}
_, exists := server.languages.Info[value]
if !exists {
client.Send(nil, client.server.name, ERR_NOLANGUAGE, client.nick, "Languages are not supported by this server")
return false
}
// if we've already applied the given language, skip it
_, exists = alreadyDoneLanguages[value]
if exists {
continue
}
appliedLanguages = append(appliedLanguages, value)
}
client.stateMutex.Lock()
if len(appliedLanguages) == 1 && appliedLanguages[0] == "en" {
// premature optimisation ahoy!
client.languages = []string{}
} else {
client.languages = appliedLanguages
}
client.stateMutex.Unlock()
params := []string{client.nick}
for _, lang := range appliedLanguages {
params = append(params, lang)
}
params = append(params, client.t("Language preferences have been set"))
client.Send(nil, client.server.name, RPL_YOURLANGUAGESARE, params...)
return false
}
var ( var (
infoString = strings.Split(` · infoString = strings.Split(` ·
· ·

@ -13,9 +13,10 @@ maintainers: "Daniel Oaks <daniel@danieloaks.net>"
# incomplete - whether to mark this language as incomplete # incomplete - whether to mark this language as incomplete
incomplete: true incomplete: true
# strings - this holds the actual replacements # translations - this holds the actual replacements
# make sure this is the last part of the file, and that the below string, "strings:", stays as-is # make sure this is the last part of the file, and that the below string, "translations:",
# the language-update processor uses the next line to designate which part of the file to ignore and # stays as-is. the language-update processor uses the next line to designate which part of
# which part to actually process. # the file to ignore and which part to actually process.
strings: translations:
"Welcome to the Internet Relay Network %s": "Welcome bro to the IRN broski %s" "Welcome to the Internet Relay Network %s": "Welcome braaaah to the IRN broski %s"
"Language preferences have been set": "You've set your languages man, wicked!"

@ -291,6 +291,18 @@ datastore:
# path to the datastore # path to the datastore
path: ircd.db path: ircd.db
# languages config
languages:
# whether to load languages
enabled: true
# default language to use for new clients
# 'en' is the default English language in the code
default: en
# which directory contains our language files
path: languages
# limits - these need to be the same across the network # limits - these need to be the same across the network
limits: limits:
# nicklen is the max nick length allowed # nicklen is the max nick length allowed