mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-14 07:59:31 +01:00
Make LANGUAGE support work
This commit is contained in:
parent
a7fdade41d
commit
e99f22488f
@ -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!"
|
||||||
|
12
oragono.yaml
12
oragono.yaml
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user