diff --git a/README.md b/README.md index 05c5270b..aa7da9ac 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Oragono is a fork of the [Ergonomadic](https://github.com/edmund-huber/ergonomad [![Build Status](https://travis-ci.org/oragono/oragono.svg?branch=master)](https://travis-ci.org/oragono/oragono) [![Download Latest Release](https://img.shields.io/badge/downloads-latest%20release-green.svg)](https://github.com/oragono/oragono/releases/latest) [![Freenode #oragono](https://img.shields.io/badge/Freenode-%23oragono-1e72ff.svg?style=flat)](https://www.irccloud.com/invite?channel=%23oragono&hostname=irc.freenode.net&port=6697&ssl=1) +[![Crowdin](https://d322cqt584bo4o.cloudfront.net/oragono/localized.svg)](https://crowdin.com/project/oragono) [darwin.network](https://irc.darwin.network/) is running Oragono in production if you want to take a look. diff --git a/irc/config.go b/irc/config.go index e2c24124..755c7717 100644 --- a/irc/config.go +++ b/irc/config.go @@ -7,6 +7,7 @@ package irc import ( "crypto/tls" + "encoding/json" "errors" "fmt" "io/ioutil" @@ -496,16 +497,18 @@ func LoadConfig(filename string) (config *Config, err error) { continue } - // only load .lang.yaml files + // only load core .lang.yaml files, and ignore help/irc files name := f.Name() - if !strings.HasSuffix(strings.ToLower(name), ".lang.yaml") { + lowerName := strings.ToLower(name) + if !strings.HasSuffix(lowerName, ".lang.yaml") { continue } - // don't load our example file in practice - if strings.ToLower(name) == "example.lang.yaml" { + // 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()) @@ -516,6 +519,52 @@ func LoadConfig(filename string) (config *Config, err error) { 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 { + return nil, fmt.Errorf("Could not load language's irc file [%s]: %s", ircName, err.Error()) + } + + 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 { + 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 { + return nil, fmt.Errorf("Could not load language's help file [%s]: %s", helpName, err.Error()) + } + + 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 { + continue + } + langInfo.Translations[key] = value + } // confirm that values are correct if langInfo.Code == "en" { @@ -527,7 +576,7 @@ func LoadConfig(filename string) (config *Config, err error) { } if len(langInfo.Translations) == 0 { - return nil, fmt.Errorf("Language file [%s] contains no translations", name) + return nil, fmt.Errorf("Language [%s / %s] contains no translations", langInfo.Code, langInfo.Name) } // check for duplicate languages diff --git a/irc/dline.go b/irc/dline.go index 5e34ebab..a39cc313 100644 --- a/irc/dline.go +++ b/irc/dline.go @@ -372,10 +372,10 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { var snoDescription string if durationIsUsed { - client.Notice(fmt.Sprintf("Added temporary (%s) D-Line for %s", duration.String(), hostString)) + client.Notice(fmt.Sprintf(client.t("Added temporary (%s) D-Line for %s"), duration.String(), hostString)) snoDescription = fmt.Sprintf(ircfmt.Unescape("%s [%s]$r added temporary (%s) D-Line for %s"), client.nick, operName, duration.String(), hostString) } else { - client.Notice(fmt.Sprintf("Added D-Line for %s", hostString)) + client.Notice(fmt.Sprintf(client.t("Added D-Line for %s"), hostString)) snoDescription = fmt.Sprintf(ircfmt.Unescape("%s [%s]$r added D-Line for %s"), client.nick, operName, hostString) } server.snomasks.Send(sno.LocalXline, snoDescription) diff --git a/irc/languages.go b/irc/languages.go index 29cf4784..a3bc736d 100644 --- a/irc/languages.go +++ b/irc/languages.go @@ -34,7 +34,15 @@ func NewLanguageManager(defaultLang string, languageData map[string]LangData) *L // load language data for name, data := range languageData { lm.Info[name] = data - lm.translations[name] = data.Translations + + // 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 + } + lm.translations[name][key] = value + } } return &lm diff --git a/irc/server.go b/irc/server.go index a33d8ddf..fce040a7 100644 --- a/irc/server.go +++ b/irc/server.go @@ -2235,7 +2235,7 @@ func languageHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool } var ( - infoString = strings.Split(` ▄▄▄ ▄▄▄· ▄▄ • ▐ ▄ + infoString1 = strings.Split(` ▄▄▄ ▄▄▄· ▄▄ • ▐ ▄ ▪ ▀▄ █·▐█ ▀█ ▐█ ▀ ▪▪ •█▌▐█▪ ▄█▀▄ ▐▀▀▄ ▄█▀▀█ ▄█ ▀█▄ ▄█▀▄▪▐█▐▐▌ ▄█▀▄ ▐█▌.▐▌▐█•█▌▐█ ▪▐▌▐█▄▪▐█▐█▌ ▐▌██▐█▌▐█▌.▐▌ @@ -2243,17 +2243,11 @@ var ( https://oragono.io/ https://github.com/oragono/oragono - -Oragono is released under the MIT license. - -Thanks to Jeremy Latt for founding Ergonomadic, the project this is based on <3 - -Core Developers: - Daniel Oakley, DanielOaks, +`, "\n") + infoString2 = strings.Split(` Daniel Oakley, DanielOaks, Shivaram Lingamneni, slingamn, - -Contributors and Former Developers: - 3onyc +`, "\n") + infoString3 = strings.Split(` 3onyc Edmund Huber Euan Kemp (euank) Jeremy Latt @@ -2267,7 +2261,21 @@ Contributors and Former Developers: ) func infoHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { - for _, line := range infoString { + // we do the below so that the human-readable lines in info can be translated. + for _, line := range infoString1 { + client.Send(nil, server.name, RPL_INFO, client.nick, line) + } + client.Send(nil, server.name, RPL_INFO, client.nick, client.t("Oragono is released under the MIT license.")) + client.Send(nil, server.name, RPL_INFO, client.nick, "") + client.Send(nil, server.name, RPL_INFO, client.nick, client.t("Thanks to Jeremy Latt for founding Ergonomadic, the project this is based on <3")) + client.Send(nil, server.name, RPL_INFO, client.nick, "") + client.Send(nil, server.name, RPL_INFO, client.nick, client.t("Core Developers:")) + for _, line := range infoString2 { + client.Send(nil, server.name, RPL_INFO, client.nick, line) + } + client.Send(nil, server.name, RPL_INFO, client.nick, "") + client.Send(nil, server.name, RPL_INFO, client.nick, client.t("Contributors and Former Developers:")) + for _, line := range infoString3 { client.Send(nil, server.name, RPL_INFO, client.nick, line) } client.Send(nil, server.name, RPL_ENDOFINFO, client.nick, "End of /INFO") diff --git a/languages/example-help.lang.json b/languages/example-help.lang.json new file mode 100644 index 00000000..9d155c5a --- /dev/null +++ b/languages/example-help.lang.json @@ -0,0 +1,65 @@ +{ + "= Help Topics =\n\nCommands:\n%s\n\nRPL_ISUPPORT Tokens:\n%s\n\nInformation:\n%s": "= Help Topics =\n\nCommands:\n%s\n\nRPL_ISUPPORT Tokens:\n%s\n\nInformation:\n%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.": "== 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": "== 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.": "== 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.", + "@+client-only-tags TAGMSG {,}\n\nSends the given client-only tags to the given targets as a TAGMSG. See the IRCv3\nspecs for more info: http://ircv3.net/specs/core/message-tags-3.3.html": "@+client-only-tags TAGMSG {,}\n\nSends the given client-only tags to the given targets as a TAGMSG. See the IRCv3\nspecs for more info: http://ircv3.net/specs/core/message-tags-3.3.html", + "ACC REGISTER [callback_namespace:] [cred_type] :\nACC VERIFY \n\nUsed in account registration. See the relevant specs for more info:\nhttps://oragono.io/specs.html": "ACC REGISTER [callback_namespace:] [cred_type] :\nACC VERIFY \n\nUsed in account registration. See the relevant specs for more info:\nhttps://oragono.io/specs.html", + "AMBIANCE \n\nThe AMBIANCE command is used to send a scene notification to the given target.": "AMBIANCE \n\nThe AMBIANCE command is used to send a scene notification to the given target.", + "AUTHENTICATE\n\nUsed during SASL authentication. See the IRCv3 specs for more info:\nhttp://ircv3.net/specs/extensions/sasl-3.1.html": "AUTHENTICATE\n\nUsed during SASL authentication. See the IRCv3 specs for more info:\nhttp://ircv3.net/specs/extensions/sasl-3.1.html", + "AWAY [message]\n\nIf [message] is sent, marks you away. If [message] is not sent, marks you no\nlonger away.": "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": "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.": "CHANSERV [params]\n\nChanServ controls channel registrations.", + "CS [params]\n\nChanServ controls channel registrations.": "CS [params]\n\nChanServ controls channel registrations.", + "DEBUG