mirror of
				https://github.com/ergochat/ergo.git
				synced 2025-10-31 05:47:22 +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, | ||||
| 		oper:      true, | ||||
| 	}, | ||||
| 	"LANGUAGE": { | ||||
| 		handler:      languageHandler, | ||||
| 		usablePreReg: true, | ||||
| 		minParams:    1, | ||||
| 	}, | ||||
| 	"LIST": { | ||||
| 		handler:   listHandler, | ||||
| 		minParams: 0, | ||||
|  | ||||
| @ -11,6 +11,7 @@ import ( | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"log" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| @ -143,6 +144,15 @@ type StackImpactConfig struct { | ||||
| 	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. | ||||
| type Config struct { | ||||
| 	Network struct { | ||||
| @ -170,6 +180,8 @@ type Config struct { | ||||
| 	Languages struct { | ||||
| 		Enabled bool | ||||
| 		Path    string | ||||
| 		Default string | ||||
| 		Data    map[string]LangData | ||||
| 	} | ||||
| 
 | ||||
| 	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()) | ||||
| 	} | ||||
| 
 | ||||
| 	// 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 | ||||
| } | ||||
|  | ||||
| @ -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 (|). | ||||
| 
 | ||||
| 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": { | ||||
| 		text: `LIST [<channel>{,<channel>}] [<elistcond>{,<elistcond>}] | ||||
|  | ||||
| @ -4,30 +4,72 @@ | ||||
| package irc | ||||
| 
 | ||||
| import ( | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| ) | ||||
| 
 | ||||
| // LanguageManager manages our languages and provides translation abilities. | ||||
| type LanguageManager struct { | ||||
| 	sync.RWMutex | ||||
| 	langMap map[string]map[string]string | ||||
| 	Info         map[string]LangData | ||||
| 	translations map[string]map[string]string | ||||
| } | ||||
| 
 | ||||
| // NewLanguageManager returns a new LanguageManager. | ||||
| func NewLanguageManager() *LanguageManager { | ||||
| func NewLanguageManager(languageData map[string]LangData) *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 | ||||
| } | ||||
| 
 | ||||
| // 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. | ||||
| func (lm *LanguageManager) Translate(languages []string, originalString string) string { | ||||
| 	// not using any special languages | ||||
| 	if len(languages) == 0 { | ||||
| 	if len(languages) == 0 || languages[0] == "en" || len(lm.translations) == 0 { | ||||
| 		return originalString | ||||
| 	} | ||||
| 
 | ||||
| @ -35,12 +77,17 @@ func (lm *LanguageManager) Translate(languages []string, originalString string) | ||||
| 	defer lm.RUnlock() | ||||
| 
 | ||||
| 	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 { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		newString, exists := langMap[originalString] | ||||
| 		newString, exists := translations[originalString] | ||||
| 		if !exists { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| @ -161,6 +161,8 @@ const ( | ||||
| 	ERR_HELPNOTFOUND                = "524" | ||||
| 	ERR_CANNOTSENDRP                = "573" | ||||
| 	RPL_WHOISSECURE                 = "671" | ||||
| 	RPL_YOURLANGUAGESARE            = "687" | ||||
| 	RPL_WHOISLANGUAGE               = "690" | ||||
| 	RPL_HELPSTART                   = "704" | ||||
| 	RPL_HELPTXT                     = "705" | ||||
| 	RPL_ENDOFHELP                   = "706" | ||||
| @ -188,4 +190,6 @@ const ( | ||||
| 	RPL_REG_VERIFICATION_REQUIRED   = "927" | ||||
| 	ERR_REG_INVALID_CRED_TYPE       = "928" | ||||
| 	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), | ||||
| 		connectionLimiter:   connection_limits.NewLimiter(), | ||||
| 		connectionThrottler: connection_limits.NewThrottler(), | ||||
| 		languages:           NewLanguageManager(), | ||||
| 		languages:           NewLanguageManager(config.Languages.Data), | ||||
| 		listeners:           make(map[string]*ListenerWrapper), | ||||
| 		logger:              logger, | ||||
| 		monitorManager:      NewMonitorManager(), | ||||
| @ -984,6 +984,9 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { | ||||
| } | ||||
| 
 | ||||
| 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) | ||||
| 
 | ||||
| 	whoischannels := client.WhoisChannelsNames(target) | ||||
| @ -1002,6 +1005,16 @@ func (client *Client) getWhoisOf(target *Client) { | ||||
| 	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) | ||||
| 	} | ||||
| 
 | ||||
| 	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) { | ||||
| 		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() | ||||
| 	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 | ||||
| 	if config.Accounts.AuthenticationEnabled && !server.accountAuthenticationEnabled { | ||||
| 		// enabling SASL | ||||
| @ -2077,6 +2109,62 @@ func userhostHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool | ||||
| 	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 ( | ||||
| 	infoString = strings.Split(`      ▄▄▄   ▄▄▄·  ▄▄ •        ▐ ▄        | ||||
| ▪     ▀▄ █·▐█ ▀█ ▐█ ▀ ▪▪     •█▌▐█▪      | ||||
|  | ||||
| @ -13,9 +13,10 @@ maintainers: "Daniel Oaks <daniel@danieloaks.net>" | ||||
| # incomplete - whether to mark this language as incomplete | ||||
| incomplete: true | ||||
| 
 | ||||
| # strings - this holds the actual replacements | ||||
| # make sure this is the last part of the file, and that the below string, "strings:", stays as-is | ||||
| # the language-update processor uses the next line to designate which part of the file to ignore and | ||||
| # which part to actually process. | ||||
| strings: | ||||
|   "Welcome to the Internet Relay Network %s": "Welcome bro to the IRN broski %s" | ||||
| # translations - this holds the actual replacements | ||||
| # make sure this is the last part of the file, and that the below string, "translations:", | ||||
| # stays as-is. the language-update processor uses the next line to designate which part of | ||||
| # the file to ignore and which part to actually process. | ||||
| translations: | ||||
|   "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: 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: | ||||
|     # nicklen is the max nick length allowed | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Daniel Oaks
						Daniel Oaks