diff --git a/.travis.yml b/.travis.yml index 972a1c6a..58a3b553 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,6 @@ language: go go: - "1.11.x" -install: make deps - script: -- wget https://github.com/goreleaser/goreleaser/releases/download/v0.62.2/goreleaser_Linux_x86_64.tar.gz -- tar -xzf goreleaser_Linux_x86_64.tar.gz -C $GOPATH/bin - make - make test diff --git a/DEVELOPING.md b/DEVELOPING.md index 839ea879..d7a9ec4f 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -2,6 +2,13 @@ This is just a bunch of tips and tricks we keep in mind while developing Oragono. If you wanna help develop as well, they might also be worth keeping in mind! +## Golang issues + +You should use the [latest distribution of the Go language for your OS and architecture](https://golang.org/dl/). (If `uname -m` on your Raspberry Pi reports `armv7l`, use the `armv6l` distribution of Go; if it reports v8, you may be able to use the `arm64` distribution.) + +Oragono vendors all its dependencies. The vendored code is tracked via a git submodule: `vendor/` is a submodule pointing to the [oragono-vendor](https://github.com/oragono/oragono-vendor) repository. As long as you're not modifying the vendored dependencies, `make` should take care of everything for you --- but if you are, see the "vendor" section below. + +Because of this, Oragono is self-contained and you should not need to fetch any dependencies with `go get`. Doing so is not recommended, since it may fetch incompatible versions of the dependencies. If you're having trouble building the code, it's very likely because your clone of the repository is in the wrong place: Go is very opinionated about where you should keep your code. Take a look at the [go workspaces documentation](https://golang.org/doc/code.html) if you're having trouble. ## Branches @@ -21,7 +28,7 @@ Develop branches are either used to work out implementation details in preperati 5. Remove unused sections from the changelog, change the date/version number and write release notes. 6. Commit the new changelog and constants change. 7. Tag the release with `git tag v0.0.0 -m "Release v0.0.0"` (`0.0.0` replaced with the real ver number). -8. Build binaries using the Makefile, upload release to Github including the changelog and binaries. +8. Build binaries using `make release`, upload release to Github including the changelog and binaries. 9. If it's a proper release (i.e. not an alpha/beta), merge the updates into the `stable` branch. Once it's built and released, you need to setup the new development version. To do so: diff --git a/Makefile b/Makefile index 864031f0..8b35ab9d 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,13 @@ -.PHONY: all build +.PHONY: all install release capdefs deps test capdef_file = ./irc/caps/defs.go -all: build +all: install -build: - goreleaser --snapshot --rm-dist +install: deps + ./install.sh -buildrelease: +release: goreleaser --skip-publish --rm-dist capdefs: diff --git a/README.md b/README.md index f0d44db1..a6d26ab4 100644 --- a/README.md +++ b/README.md @@ -70,21 +70,19 @@ The `stable` branch contains the latest release. You can run this for a producti #### Building -Clone the appropriate branch. You should also run this command to set up vendored dependencies: -``` -git submodule update --init -``` +You'll need an [up-to-date distribution of the Go language for your OS and architecture](https://golang.org/dl/). You'll also need to set up a [Go workspace](https://golang.org/doc/code.html). Typically, this is just a directory `~/go`, with the `GOPATH` environment variable exported to its path with `export GOPATH=~/go`. -From the root folder, you can run `make`, using [GoReleaser](https://goreleaser.com/) to generate all of our release binaries in `/dist`: -``` +Clone the repository where `go` expects it to be and then run `make`, i.e., + +```bash +mkdir -p ${GOPATH}/src/github.com/oragono +cd ${GOPATH}/src/github.com/oragono +git clone https://github.com/oragono/oragono +cd oragono +# check out the appropriate branch if necessary +# now, this will install a development copy of oragono at ${GOPATH}/bin/oragono: make -``` - -However, when just developing I instead just use this command to rebuild and run Oragono on the fly with the latest changes: -``` -go run oragono.go -``` - +```` ## Configuration diff --git a/install.sh b/install.sh new file mode 100755 index 00000000..96d60781 --- /dev/null +++ b/install.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -e + +if [ -z "$GOPATH" ]; then + echo Error: \$GOPATH is unset + echo See https://golang.org/doc/code.html for details, or try these steps: + echo -e "\tmkdir -p ~/go" + echo -e "\texport GOPATH=~/go" + exit 1 +fi + +EXPECTED_DIR=${GOPATH}/src/github.com/oragono/oragono + +if [ "$PWD" != "$EXPECTED_DIR" ] ; then + echo Error: working directory is not where \$GOPATH expects it to be + echo "Expected: $EXPECTED_DIR" + echo "Actual: $PWD" + echo See https://golang.org/doc/code.html for details, or try these steps: + echo -e "\tmkdir -p ${GOPATH}/src/github.com/oragono" + echo -e "\tcd ${GOPATH}/src/github.com/oragono" + echo -e "\tmv $PWD oragono" + echo -e "\tcd oragono" + exit 1 +fi + +go install -v +echo successfully installed as ${GOPATH}/bin/oragono diff --git a/irc/client.go b/irc/client.go index 898feadc..843c3cf0 100644 --- a/irc/client.go +++ b/irc/client.go @@ -133,6 +133,7 @@ func RunNewClient(server *Server, conn clientConn) { ctime: now, flags: modes.NewModeSet(), isTor: conn.IsTor, + languages: server.Languages().Default(), loginThrottle: connection_limits.GenericThrottle{ Duration: config.Accounts.LoginThrottling.Duration, Limit: config.Accounts.LoginThrottling.MaxAttempts, @@ -145,7 +146,6 @@ func RunNewClient(server *Server, conn clientConn) { nickMaskString: "*", // * is used until actual nick is given history: history.NewHistoryBuffer(config.History.ClientLength), } - client.languages = server.languages.Default() client.recomputeMaxlens() diff --git a/irc/config.go b/irc/config.go index 4fd71faa..2d1cf2f5 100644 --- a/irc/config.go +++ b/irc/config.go @@ -7,13 +7,11 @@ package irc import ( "crypto/tls" - "encoding/json" "fmt" "io/ioutil" "log" "net" "os" - "path/filepath" "regexp" "strings" "time" @@ -293,9 +291,10 @@ type Config struct { Enabled bool Path string Default string - Data map[string]languages.LangData } + languageManager *languages.Manager + Datastore struct { Path string AutoUpgrade bool @@ -648,120 +647,9 @@ func LoadConfig(filename string) (config *Config, err error) { } config.Server.MaxSendQBytes = int(maxSendQBytes) - // get language files - config.Languages.Data = make(map[string]languages.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 core .lang.yaml file, and ignore help/irc files - name := f.Name() - lowerName := strings.ToLower(name) - if !strings.HasSuffix(lowerName, ".lang.yaml") { - continue - } - // don't load our example files in practice - if strings.HasPrefix(lowerName, "example") { - continue - } - - // load core info file - 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 languages.LangData - err = yaml.Unmarshal(data, &langInfo) - if err != nil { - return nil, fmt.Errorf("Could not parse language file [%s]: %s", name, err.Error()) - } - langInfo.Translations = make(map[string]string) - - // load actual translation files - var tlList map[string]string - - // load irc strings file - ircName := strings.TrimSuffix(name, ".lang.yaml") + "-irc.lang.json" - - data, err = ioutil.ReadFile(filepath.Join(config.Languages.Path, ircName)) - if err == nil { - err = json.Unmarshal(data, &tlList) - if err != nil { - return nil, fmt.Errorf("Could not parse language's irc file [%s]: %s", ircName, err.Error()) - } - - for key, value := range tlList { - // because of how crowdin works, this is how we skip untranslated lines - if key == value || value == "" { - continue - } - langInfo.Translations[key] = value - } - } - - // load help strings file - helpName := strings.TrimSuffix(name, ".lang.yaml") + "-help.lang.json" - - data, err = ioutil.ReadFile(filepath.Join(config.Languages.Path, helpName)) - if err == nil { - err = json.Unmarshal(data, &tlList) - if err != nil { - return nil, fmt.Errorf("Could not parse language's help file [%s]: %s", helpName, err.Error()) - } - - for key, value := range tlList { - // because of how crowdin works, this is how we skip untranslated lines - if key == value || value == "" { - continue - } - langInfo.Translations[key] = value - } - } - - // 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 len(langInfo.Translations) == 0 { - // skip empty translations - continue - } - - if langInfo.Code == "" || langInfo.Name == "" || langInfo.Contributors == "" { - return nil, fmt.Errorf("Code, name or contributors is empty in language file [%s]", 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) - } + config.languageManager, err = languages.NewManager(config.Languages.Enabled, config.Languages.Path, config.Languages.Default) + if err != nil { + return nil, fmt.Errorf("Could not load languages: %s", err.Error()) } // RecoverFromErrors defaults to true diff --git a/irc/getters.go b/irc/getters.go index 6fef1fd8..96b32309 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -5,13 +5,15 @@ package irc import ( "github.com/oragono/oragono/irc/isupport" + "github.com/oragono/oragono/irc/languages" "github.com/oragono/oragono/irc/modes" ) -func (server *Server) Config() *Config { +func (server *Server) Config() (config *Config) { server.configurableStateMutex.RLock() - defer server.configurableStateMutex.RUnlock() - return server.config + config = server.config + server.configurableStateMutex.RUnlock() + return } func (server *Server) ISupport() *isupport.List { @@ -58,6 +60,10 @@ func (server *Server) GetOperator(name string) (oper *Oper) { return server.config.operators[name] } +func (server *Server) Languages() (lm *languages.Manager) { + return server.Config().languageManager +} + func (client *Client) Nick() string { client.stateMutex.RLock() defer client.stateMutex.RUnlock() @@ -198,6 +204,19 @@ func (client *Client) SetAccountName(account string) (changed bool) { return } +func (client *Client) Languages() (languages []string) { + client.stateMutex.RLock() + languages = client.languages + client.stateMutex.RUnlock() + return languages +} + +func (client *Client) SetLanguages(languages []string) { + client.stateMutex.Lock() + client.languages = languages + client.stateMutex.Unlock() +} + func (client *Client) HasMode(mode modes.Mode) bool { // client.flags has its own synch return client.flags.HasMode(mode) diff --git a/irc/handlers.go b/irc/handlers.go index ce3fb292..791a75e7 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -988,11 +988,7 @@ Get an explanation of , or "index" for a list of help topics.`), rb) // handle index if argument == "index" { - if client.HasMode(modes.Operator) { - client.sendHelp("HELP", GetHelpIndex(client.languages, HelpIndexOpers), rb) - } else { - client.sendHelp("HELP", GetHelpIndex(client.languages, HelpIndex), rb) - } + client.sendHelp("HELP", server.helpIndexManager.GetIndex(client.Languages(), client.HasMode(modes.Operator)), rb) return false } @@ -1088,7 +1084,7 @@ func infoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp rb.Add(nil, server.name, RPL_INFO, client.nick, line) } // show translators for languages other than good ole' regular English - tlines := server.languages.Translators() + tlines := server.Languages().Translators() if 0 < len(tlines) { rb.Add(nil, server.name, RPL_INFO, client.nick, client.t("Translators:")) for _, line := range tlines { @@ -1422,12 +1418,14 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res // LANGUAGE { } func languageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { + nick := client.Nick() alreadyDoneLanguages := make(map[string]bool) var appliedLanguages []string - supportedLanguagesCount := server.languages.Count() + lm := server.Languages() + supportedLanguagesCount := lm.Count() if supportedLanguagesCount < len(msg.Params) { - rb.Add(nil, client.server.name, ERR_TOOMANYLANGUAGES, client.nick, strconv.Itoa(supportedLanguagesCount), client.t("You specified too many languages")) + rb.Add(nil, client.server.name, ERR_TOOMANYLANGUAGES, nick, strconv.Itoa(supportedLanguagesCount), client.t("You specified too many languages")) return false } @@ -1441,9 +1439,9 @@ func languageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb * continue } - _, exists := server.languages.Info[value] + _, exists := lm.Languages[value] if !exists { - rb.Add(nil, client.server.name, ERR_NOLANGUAGE, client.nick, client.t("Languages are not supported by this server")) + rb.Add(nil, client.server.name, ERR_NOLANGUAGE, nick, fmt.Sprintf(client.t("Language %s is not supported by this server"), value)) return false } @@ -1456,20 +1454,16 @@ func languageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb * appliedLanguages = append(appliedLanguages, value) } - client.stateMutex.Lock() - if len(appliedLanguages) == 1 && appliedLanguages[0] == "en" { - // premature optimisation ahoy! - client.languages = []string{} - } else { - client.languages = appliedLanguages + var langsToSet []string + if !(len(appliedLanguages) == 1 && appliedLanguages[0] == "en") { + langsToSet = appliedLanguages } - client.stateMutex.Unlock() + client.SetLanguages(langsToSet) - params := []string{client.nick} - for _, lang := range appliedLanguages { - params = append(params, lang) - } - params = append(params, client.t("Language preferences have been set")) + params := make([]string, len(appliedLanguages)+2) + params[0] = nick + copy(params[1:], appliedLanguages) + params[len(params)-1] = client.t("Language preferences have been set") rb.Add(nil, client.server.name, RPL_YOURLANGUAGESARE, params...) diff --git a/irc/help.go b/irc/help.go index 34de5a31..5bbffa48 100644 --- a/irc/help.go +++ b/irc/help.go @@ -7,6 +7,7 @@ import ( "fmt" "sort" "strings" + "sync" "github.com/oragono/oragono/irc/languages" ) @@ -603,13 +604,15 @@ func modesTextGenerator(client *Client) string { return client.t(cmodeHelpText) + "\n\n" + client.t(umodeHelpText) } -// HelpIndex contains the list of all help topics for regular users. -var HelpIndex map[string]string +type HelpIndexManager struct { + sync.RWMutex // tier 1 -// HelpIndexOpers contains the list of all help topics for opers. -var HelpIndexOpers map[string]string + langToIndex map[string]string + langToOperIndex map[string]string +} // GenerateHelpIndex is used to generate HelpIndex. +// Returns: a map from language code to the help index in that language. func GenerateHelpIndex(lm *languages.Manager, forOpers bool) map[string]string { // generate the help entry lists var commands, isupport, information []string @@ -658,10 +661,7 @@ Information: newHelpIndex["en"] = fmt.Sprintf(defaultHelpIndex, commandsString, isupportString, informationString) - lm.RLock() - defer lm.RUnlock() - - for langCode := range lm.Info { + for langCode := range lm.Languages { translatedHelpIndex := lm.Translate([]string{langCode}, defaultHelpIndex) if translatedHelpIndex != defaultHelpIndex { newHelpIndex[langCode] = fmt.Sprintf(translatedHelpIndex, commandsString, isupportString, informationString) @@ -671,22 +671,16 @@ Information: return newHelpIndex } -// GenerateHelpIndices generates our help indexes and confirms we have HELP entries for every command. -func GenerateHelpIndices(lm *languages.Manager) error { - // startup check that we have HELP entries for every command - if len(HelpIndex) == 0 && len(HelpIndexOpers) == 0 { - for name := range Commands { - _, exists := Help[strings.ToLower(name)] - if !exists { - return fmt.Errorf("Help entry does not exist for command %s", name) - } - } - } - +// GenerateIndices regenerates our help indexes for each currently enabled language. +func (hm *HelpIndexManager) GenerateIndices(lm *languages.Manager) { // generate help indexes - HelpIndex = GenerateHelpIndex(lm, false) - HelpIndexOpers = GenerateHelpIndex(lm, true) - return nil + langToIndex := GenerateHelpIndex(lm, false) + langToOperIndex := GenerateHelpIndex(lm, true) + + hm.Lock() + defer hm.Unlock() + hm.langToIndex = langToIndex + hm.langToOperIndex = langToOperIndex } // sendHelp sends the client help of the given string. @@ -709,13 +703,30 @@ func (client *Client) sendHelp(name string, text string, rb *ResponseBuffer) { } // GetHelpIndex returns the help index for the given language. -func GetHelpIndex(languages []string, helpIndex map[string]string) string { +func (hm *HelpIndexManager) GetIndex(languages []string, oper bool) string { + hm.RLock() + langToIndex := hm.langToIndex + if oper { + langToIndex = hm.langToOperIndex + } + hm.RUnlock() + for _, lang := range languages { - index, exists := helpIndex[lang] + index, exists := langToIndex[lang] if exists { return index } } // 'en' always exists - return helpIndex["en"] + return langToIndex["en"] +} + +func init() { + // startup check that we have HELP entries for every command + for name := range Commands { + _, exists := Help[strings.ToLower(name)] + if !exists { + panic(fmt.Sprintf("Help entry does not exist for command %s", name)) + } + } } diff --git a/irc/languages/languages.go b/irc/languages/languages.go index 39d43e73..34a33621 100644 --- a/irc/languages/languages.go +++ b/irc/languages/languages.go @@ -4,10 +4,25 @@ package languages import ( + "encoding/json" "fmt" + "io/ioutil" + "path/filepath" "sort" + "strconv" "strings" - "sync" + + "gopkg.in/yaml.v2" +) + +const ( + // for a language (e.g., `fi-FI`) to be supported + // it must have a metadata file named, e.g., `fi-FI.lang.yaml` + metadataFileSuffix = ".lang.yaml" +) + +var ( + stringsFileSuffixes = []string{"-irc.lang.json", "-help.lang.json", "-nickserv.lang.json", "-hostserv.lang.json", "-chanserv.lang.json"} ) // LangData is the data contained in a language file. @@ -16,76 +31,144 @@ type LangData struct { Code string Contributors string Incomplete bool - Translations map[string]string } // Manager manages our languages and provides translation abilities. type Manager struct { - sync.RWMutex - Info map[string]LangData + Languages map[string]LangData translations map[string]map[string]string defaultLang string } // NewManager returns a new Manager. -func NewManager(defaultLang string, languageData map[string]LangData) *Manager { - lm := Manager{ - Info: make(map[string]LangData), +func NewManager(enabled bool, path string, defaultLang string) (lm *Manager, err error) { + lm = &Manager{ + Languages: make(map[string]LangData), translations: make(map[string]map[string]string), defaultLang: defaultLang, } // make fake "en" info - lm.Info["en"] = LangData{ + lm.Languages["en"] = LangData{ Code: "en", Name: "English", Contributors: "Oragono contributors and the IRC community", } - // load language data - for name, data := range languageData { - lm.Info[name] = data - - // make sure we don't include empty translations - lm.translations[name] = make(map[string]string) - for key, value := range data.Translations { - if strings.TrimSpace(value) == "" { - continue + if enabled { + err = lm.loadData(path) + if err == nil { + // successful load, check that defaultLang is sane + _, ok := lm.Languages[lm.defaultLang] + if !ok { + err = fmt.Errorf("Cannot find default language [%s]", lm.defaultLang) } - lm.translations[name][key] = value } + } else { + lm.defaultLang = "en" } - return &lm + return +} + +func (lm *Manager) loadData(path string) (err error) { + files, err := ioutil.ReadDir(path) + if err != nil { + return + } + + // 1. for each language that has a ${langcode}.lang.yaml in the languages path + // 2. load ${langcode}.lang.yaml + // 3. load ${langcode}-irc.lang.json and friends as the translations + for _, f := range files { + if f.IsDir() { + continue + } + // glob up *.lang.yaml in the directory + name := f.Name() + if !strings.HasSuffix(name, metadataFileSuffix) { + continue + } + prefix := strings.TrimSuffix(name, metadataFileSuffix) + + // load, e.g., `zh-CN.lang.yaml` + var data []byte + data, err = ioutil.ReadFile(filepath.Join(path, name)) + if err != nil { + return + } + var langInfo LangData + err = yaml.Unmarshal(data, &langInfo) + if err != nil { + return err + } + + if langInfo.Code == "en" { + return 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") + } + + // check for duplicate languages + _, exists := lm.Languages[strings.ToLower(langInfo.Code)] + if exists { + return fmt.Errorf("Language code [%s] defined twice", langInfo.Code) + } + + // slurp up all translation files with `prefix` into a single translation map + translations := make(map[string]string) + for _, translationSuffix := range stringsFileSuffixes { + stringsFilePath := filepath.Join(path, prefix+translationSuffix) + data, err = ioutil.ReadFile(stringsFilePath) + if err != nil { + continue // skip missing paths + } + var tlList map[string]string + err = json.Unmarshal(data, &tlList) + if err != nil { + return fmt.Errorf("invalid json for translation file %s: %s", stringsFilePath, err.Error()) + } + + for key, value := range tlList { + // because of how crowdin works, this is how we skip untranslated lines + if key == value || strings.TrimSpace(value) == "" { + continue + } + translations[key] = value + } + } + + if len(translations) == 0 { + // skip empty translations + continue + } + + // sanity check the language definition from the yaml file + if langInfo.Code == "" || langInfo.Name == "" || langInfo.Contributors == "" { + return fmt.Errorf("Code, name or contributors is empty in language file [%s]", name) + } + + key := strings.ToLower(langInfo.Code) + lm.Languages[key] = langInfo + lm.translations[key] = translations + } + + return nil } // Default returns the default languages. func (lm *Manager) Default() []string { - lm.RLock() - defer lm.RUnlock() - - if lm.defaultLang == "" { - return []string{} - } return []string{lm.defaultLang} } // Count returns how many languages we have. func (lm *Manager) Count() int { - lm.RLock() - defer lm.RUnlock() - - return len(lm.Info) + return len(lm.Languages) } // Translators returns the languages we have and the translators. func (lm *Manager) Translators() []string { - lm.RLock() - defer lm.RUnlock() - var tlist sort.StringSlice - for _, info := range lm.Info { + for _, info := range lm.Languages { if info.Code == "en" { continue } @@ -98,12 +181,9 @@ func (lm *Manager) Translators() []string { // Codes returns the proper language codes for the given casefolded language codes. func (lm *Manager) Codes(codes []string) []string { - lm.RLock() - defer lm.RUnlock() - var newCodes []string for _, code := range codes { - info, exists := lm.Info[code] + info, exists := lm.Languages[code] if exists { newCodes = append(newCodes, info.Code) } @@ -123,9 +203,6 @@ func (lm *Manager) Translate(languages []string, originalString string) string { return originalString } - lm.RLock() - defer lm.RUnlock() - for _, lang := range languages { lang = strings.ToLower(lang) if lang == "en" { @@ -149,3 +226,18 @@ func (lm *Manager) Translate(languages []string, originalString string) string { // didn't find any translation return originalString } + +func (lm *Manager) CapValue() string { + langCodes := make([]string, len(lm.Languages)+1) + langCodes[0] = strconv.Itoa(len(lm.Languages)) + i := 1 + for _, info := range lm.Languages { + codeToken := info.Code + if info.Incomplete { + codeToken = "~" + info.Code + } + langCodes[i] = codeToken + i += 1 + } + return strings.Join(langCodes, ",") +} diff --git a/irc/server.go b/irc/server.go index 23b71614..8c2c77ff 100644 --- a/irc/server.go +++ b/irc/server.go @@ -24,7 +24,6 @@ import ( "github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/connection_limits" "github.com/oragono/oragono/irc/isupport" - "github.com/oragono/oragono/irc/languages" "github.com/oragono/oragono/irc/logger" "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/sno" @@ -73,9 +72,9 @@ type Server struct { connectionThrottler *connection_limits.Throttler ctime time.Time dlines *DLineManager + helpIndexManager HelpIndexManager isupport *isupport.List klines *KLineManager - languages *languages.Manager listeners map[string]*ListenerWrapper logger *logger.Manager monitorManager *MonitorManager @@ -118,7 +117,6 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) { clients: NewClientManager(), connectionLimiter: connection_limits.NewLimiter(), connectionThrottler: connection_limits.NewThrottler(), - languages: languages.NewManager(config.Languages.Default, config.Languages.Data), listeners: make(map[string]*ListenerWrapper), logger: logger, monitorManager: NewMonitorManager(), @@ -136,11 +134,6 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) { return nil, err } - // generate help info - if err := GenerateHelpIndices(server.languages); err != nil { - return nil, err - } - // Attempt to clean up when receiving these signals. signal.Notify(server.signals, ServerExitSignals...) signal.Notify(server.rehashSignal, syscall.SIGHUP) @@ -485,11 +478,9 @@ func (server *Server) tryRegister(c *Client) { // t returns the translated version of the given string, based on the languages configured by the client. func (client *Client) t(originalString string) string { - // grab this mutex to protect client.languages - client.stateMutex.RLock() - defer client.stateMutex.RUnlock() - - return client.server.languages.Translate(client.languages, originalString) + // TODO(slingamn) investigate a fast path for this, using an atomic load to see if translation is disabled + languages := client.Languages() + return client.server.Languages().Translate(languages, originalString) } // MOTD serves the Message of the Day. @@ -553,9 +544,10 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) { rb.Add(nil, client.server.name, RPL_WHOISBOT, cnick, tnick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.Config().Network.Name))) } - if 0 < len(target.languages) { + tLanguages := target.Languages() + if 0 < len(tLanguages) { params := []string{cnick, tnick} - for _, str := range client.server.languages.Codes(target.languages) { + for _, str := range client.server.Languages().Codes(tLanguages) { params = append(params, str) } params = append(params, client.t("can speak these languages")) @@ -675,31 +667,16 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { updatedCaps := caps.NewSet() // Translations + server.logger.Debug("server", "Regenerating HELP indexes for new languages") + server.helpIndexManager.GenerateIndices(config.languageManager) + 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("server", "Languages:", newLanguageValue) - + newLanguageValue := config.languageManager.CapValue() if currentLanguageValue != newLanguageValue { updatedCaps.Add(caps.Languages) CapValues.Set(caps.Languages, newLanguageValue) } - lm := languages.NewManager(config.Languages.Default, config.Languages.Data) - - server.logger.Debug("server", "Regenerating HELP indexes for new languages") - GenerateHelpIndices(lm) - - server.languages = lm - // SASL authPreviouslyEnabled := oldConfig != nil && oldConfig.Accounts.AuthenticationEnabled if config.Accounts.AuthenticationEnabled && !authPreviouslyEnabled { diff --git a/languages/af-ZA-help.lang.json b/languages/af-ZA-help.lang.json index e034a725..4232ac73 100644 --- a/languages/af-ZA-help.lang.json +++ b/languages/af-ZA-help.lang.json @@ -1,5 +1,5 @@ { - "= Help Topics =\n\nCommands:\n%s\n\nRPL_ISUPPORT Tokens:\n%s\n\nInformation:\n%s": "", + "= Help Topics =\n\nCommands:\n%[1]s\n\nRPL_ISUPPORT Tokens:\n%[2]s\n\nInformation:\n%[3]s": "", "== Channel Modes ==\n\nOragono supports the following channel modes:\n\n +b | Client masks that are banned from the channel (e.g. *!*@127.0.0.1)\n +e | Client masks that are exempted from bans.\n +I | Client masks that are exempted from the invite-only flag.\n +i | Invite-only mode, only invited clients can join the channel.\n +k | Key required when joining the channel.\n +l | Client join limit for the channel.\n +m | Moderated mode, only privileged clients can talk on the channel.\n +n | No-outside-messages mode, only users that are on the channel can send\n | messages to it.\n +R | Only registered users can talk in the channel.\n +s | Secret mode, channel won't show up in /LIST or whois replies.\n +t | Only channel opers can modify the topic.\n\n= Prefixes =\n\n +q (~) | Founder channel mode.\n +a (&) | Admin channel mode.\n +o (@) | Operator channel mode.\n +h (%) | Halfop channel mode.\n +v (+) | Voice channel mode.": "", "== Server Notice Masks ==\n\nOragono supports the following server notice masks for operators:\n\n a | Local announcements.\n c | Local client connections.\n j | Local channel actions.\n k | Local kills.\n n | Local nick changes.\n o | Local oper actions.\n q | Local quits.\n t | Local /STATS usage.\n u | Local client account actions.\n x | Local X-lines (DLINE/KLINE/etc).\n\nTo set a snomask, do this with your nickname:\n\n /MODE +s \n\nFor instance, this would set the kill, oper, account and xline snomasks on dan:\n\n /MODE dan +s koux": "", "== User Modes ==\n\nOragono supports the following user modes:\n\n +a | User is marked as being away. This mode is set with the /AWAY command.\n +i | User is marked as invisible (their channels are hidden from whois replies).\n +o | User is an IRC operator.\n +R | User only accepts messages from other registered users. \n +s | Server Notice Masks (see help with /HELPOP snomasks).\n +Z | User is connected via TLS.": "", @@ -10,11 +10,13 @@ "AWAY [message]\n\nIf [message] is sent, marks you away. If [message] is not sent, marks you no\nlonger away.": "", "CAP [:]\n\nUsed in capability negotiation. See the IRCv3 specs for more info:\nhttp://ircv3.net/specs/core/capability-negotiation-3.1.html\nhttp://ircv3.net/specs/core/capability-negotiation-3.2.html": "", "CHANSERV [params]\n\nChanServ controls channel registrations.": "", + "CHATHISTORY [params]\n\nCHATHISTORY is an experimental history replay command. See these documents:\nhttps://github.com/MuffinMedic/ircv3-specifications/blob/chathistory/extensions/chathistory.md\nhttps://gist.github.com/DanielOaks/c104ad6e8759c01eb5c826d627caf80d": "", "CS [params]\n\nChanServ controls channel registrations.": "", "DEBUG