Browse Source

Make LANGUAGE support work

tags/v0.11.0-alpha
Daniel Oaks 6 years ago
parent
commit
e99f22488f
8 changed files with 255 additions and 14 deletions
  1. 5
    0
      irc/commands.go
  2. 79
    0
      irc/config.go
  3. 5
    0
      irc/help.go
  4. 54
    7
      irc/languages.go
  5. 4
    0
      irc/numerics.go
  6. 89
    1
      irc/server.go
  7. 7
    6
      languages/example.lang.yaml
  8. 12
    0
      oragono.yaml

+ 5
- 0
irc/commands.go View File

131
 		minParams: 1,
131
 		minParams: 1,
132
 		oper:      true,
132
 		oper:      true,
133
 	},
133
 	},
134
+	"LANGUAGE": {
135
+		handler:      languageHandler,
136
+		usablePreReg: true,
137
+		minParams:    1,
138
+	},
134
 	"LIST": {
139
 	"LIST": {
135
 		handler:   listHandler,
140
 		handler:   listHandler,
136
 		minParams: 0,
141
 		minParams: 0,

+ 79
- 0
irc/config.go View File

11
 	"fmt"
11
 	"fmt"
12
 	"io/ioutil"
12
 	"io/ioutil"
13
 	"log"
13
 	"log"
14
+	"path/filepath"
14
 	"strings"
15
 	"strings"
15
 	"time"
16
 	"time"
16
 
17
 
143
 	AppName  string `yaml:"app-name"`
144
 	AppName  string `yaml:"app-name"`
144
 }
145
 }
145
 
146
 
147
+// LangData is the data contained in a language file.
148
+type LangData struct {
149
+	Name         string
150
+	Code         string
151
+	Maintainers  string
152
+	Incomplete   bool
153
+	Translations map[string]string
154
+}
155
+
146
 // Config defines the overall configuration.
156
 // Config defines the overall configuration.
147
 type Config struct {
157
 type Config struct {
148
 	Network struct {
158
 	Network struct {
170
 	Languages struct {
180
 	Languages struct {
171
 		Enabled bool
181
 		Enabled bool
172
 		Path    string
182
 		Path    string
183
+		Default string
184
+		Data    map[string]LangData
173
 	}
185
 	}
174
 
186
 
175
 	Datastore struct {
187
 	Datastore struct {
470
 		return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())
482
 		return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())
471
 	}
483
 	}
472
 
484
 
485
+	// get language files
486
+	config.Languages.Data = make(map[string]LangData)
487
+	if config.Languages.Enabled {
488
+		files, err := ioutil.ReadDir(config.Languages.Path)
489
+		if err != nil {
490
+			return nil, fmt.Errorf("Could not load language files: %s", err.Error())
491
+		}
492
+
493
+		for _, f := range files {
494
+			// skip dirs
495
+			if f.IsDir() {
496
+				continue
497
+			}
498
+
499
+			// only load .lang.yaml files
500
+			name := f.Name()
501
+			if !strings.HasSuffix(strings.ToLower(name), ".lang.yaml") {
502
+				continue
503
+			}
504
+
505
+			data, err = ioutil.ReadFile(filepath.Join(config.Languages.Path, name))
506
+			if err != nil {
507
+				return nil, fmt.Errorf("Could not load language file [%s]: %s", name, err.Error())
508
+			}
509
+
510
+			var langInfo LangData
511
+			err = yaml.Unmarshal(data, &langInfo)
512
+			if err != nil {
513
+				return nil, fmt.Errorf("Could not parse language file [%s]: %s", name, err.Error())
514
+			}
515
+
516
+			// confirm that values are correct
517
+			if langInfo.Code == "en" {
518
+				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")
519
+			}
520
+
521
+			if langInfo.Code == "" || langInfo.Name == "" || langInfo.Maintainers == "" {
522
+				return nil, fmt.Errorf("Code, name or maintainers is empty in language file [%s]", name)
523
+			}
524
+
525
+			if len(langInfo.Translations) == 0 {
526
+				return nil, fmt.Errorf("Language file [%s] contains no translations", name)
527
+			}
528
+
529
+			// check for duplicate languages
530
+			_, exists := config.Languages.Data[strings.ToLower(langInfo.Code)]
531
+			if exists {
532
+				return nil, fmt.Errorf("Language code [%s] defined twice", langInfo.Code)
533
+			}
534
+
535
+			// and insert into lang info
536
+			config.Languages.Data[strings.ToLower(langInfo.Code)] = langInfo
537
+		}
538
+
539
+		// confirm that default language exists
540
+		if config.Languages.Default == "" {
541
+			config.Languages.Default = "en"
542
+		} else {
543
+			config.Languages.Default = strings.ToLower(config.Languages.Default)
544
+		}
545
+
546
+		_, exists := config.Languages.Data[config.Languages.Default]
547
+		if config.Languages.Default != "en" && !exists {
548
+			return nil, fmt.Errorf("Cannot find default language [%s]", config.Languages.Default)
549
+		}
550
+	}
551
+
473
 	return config, nil
552
 	return config, nil
474
 }
553
 }

+ 5
- 0
irc/help.go View File

249
 [reason] and [oper reason], if they exist, are separated by a vertical bar (|).
249
 [reason] and [oper reason], if they exist, are separated by a vertical bar (|).
250
 
250
 
251
 If "KLINE LIST" is sent, the server sends back a list of our current KLINEs.`,
251
 If "KLINE LIST" is sent, the server sends back a list of our current KLINEs.`,
252
+	},
253
+	"language": {
254
+		text: `LANGUAGE <code>{ <code>}
255
+
256
+Sets your preferred languages to the given ones.`,
252
 	},
257
 	},
253
 	"list": {
258
 	"list": {
254
 		text: `LIST [<channel>{,<channel>}] [<elistcond>{,<elistcond>}]
259
 		text: `LIST [<channel>{,<channel>}] [<elistcond>{,<elistcond>}]

+ 54
- 7
irc/languages.go View File

4
 package irc
4
 package irc
5
 
5
 
6
 import (
6
 import (
7
+	"strings"
7
 	"sync"
8
 	"sync"
8
 )
9
 )
9
 
10
 
10
 // LanguageManager manages our languages and provides translation abilities.
11
 // LanguageManager manages our languages and provides translation abilities.
11
 type LanguageManager struct {
12
 type LanguageManager struct {
12
 	sync.RWMutex
13
 	sync.RWMutex
13
-	langMap map[string]map[string]string
14
+	Info         map[string]LangData
15
+	translations map[string]map[string]string
14
 }
16
 }
15
 
17
 
16
 // NewLanguageManager returns a new LanguageManager.
18
 // NewLanguageManager returns a new LanguageManager.
17
-func NewLanguageManager() *LanguageManager {
19
+func NewLanguageManager(languageData map[string]LangData) *LanguageManager {
18
 	lm := LanguageManager{
20
 	lm := LanguageManager{
19
-		langMap: make(map[string]map[string]string),
21
+		Info:         make(map[string]LangData),
22
+		translations: make(map[string]map[string]string),
20
 	}
23
 	}
21
 
24
 
22
-	//TODO(dan): load language files here
25
+	// make fake "en" info
26
+	lm.Info["en"] = LangData{
27
+		Code:        "en",
28
+		Name:        "English",
29
+		Maintainers: "Oragono contributors and the IRC community",
30
+	}
31
+
32
+	// load language data
33
+	for name, data := range languageData {
34
+		lm.Info[name] = data
35
+		lm.translations[name] = data.Translations
36
+	}
23
 
37
 
24
 	return &lm
38
 	return &lm
25
 }
39
 }
26
 
40
 
41
+// Count returns how many languages we have.
42
+func (lm *LanguageManager) Count() int {
43
+	lm.RLock()
44
+	defer lm.RUnlock()
45
+
46
+	return len(lm.Info)
47
+}
48
+
49
+// Codes returns the proper language codes for the given casefolded language codes.
50
+func (lm *LanguageManager) Codes(codes []string) []string {
51
+	lm.RLock()
52
+	defer lm.RUnlock()
53
+
54
+	var newCodes []string
55
+	for _, code := range codes {
56
+		info, exists := lm.Info[code]
57
+		if exists {
58
+			newCodes = append(newCodes, info.Code)
59
+		}
60
+	}
61
+
62
+	if len(newCodes) == 0 {
63
+		newCodes = []string{"en"}
64
+	}
65
+
66
+	return newCodes
67
+}
68
+
27
 // Translate returns the given string, translated into the given language.
69
 // Translate returns the given string, translated into the given language.
28
 func (lm *LanguageManager) Translate(languages []string, originalString string) string {
70
 func (lm *LanguageManager) Translate(languages []string, originalString string) string {
29
 	// not using any special languages
71
 	// not using any special languages
30
-	if len(languages) == 0 {
72
+	if len(languages) == 0 || languages[0] == "en" || len(lm.translations) == 0 {
31
 		return originalString
73
 		return originalString
32
 	}
74
 	}
33
 
75
 
35
 	defer lm.RUnlock()
77
 	defer lm.RUnlock()
36
 
78
 
37
 	for _, lang := range languages {
79
 	for _, lang := range languages {
38
-		langMap, exists := lm.langMap[lang]
80
+		lang = strings.ToLower(lang)
81
+		if lang == "en" {
82
+			return originalString
83
+		}
84
+
85
+		translations, exists := lm.translations[lang]
39
 		if !exists {
86
 		if !exists {
40
 			continue
87
 			continue
41
 		}
88
 		}
42
 
89
 
43
-		newString, exists := langMap[originalString]
90
+		newString, exists := translations[originalString]
44
 		if !exists {
91
 		if !exists {
45
 			continue
92
 			continue
46
 		}
93
 		}

+ 4
- 0
irc/numerics.go View File

161
 	ERR_HELPNOTFOUND                = "524"
161
 	ERR_HELPNOTFOUND                = "524"
162
 	ERR_CANNOTSENDRP                = "573"
162
 	ERR_CANNOTSENDRP                = "573"
163
 	RPL_WHOISSECURE                 = "671"
163
 	RPL_WHOISSECURE                 = "671"
164
+	RPL_YOURLANGUAGESARE            = "687"
165
+	RPL_WHOISLANGUAGE               = "690"
164
 	RPL_HELPSTART                   = "704"
166
 	RPL_HELPSTART                   = "704"
165
 	RPL_HELPTXT                     = "705"
167
 	RPL_HELPTXT                     = "705"
166
 	RPL_ENDOFHELP                   = "706"
168
 	RPL_ENDOFHELP                   = "706"
188
 	RPL_REG_VERIFICATION_REQUIRED   = "927"
190
 	RPL_REG_VERIFICATION_REQUIRED   = "927"
189
 	ERR_REG_INVALID_CRED_TYPE       = "928"
191
 	ERR_REG_INVALID_CRED_TYPE       = "928"
190
 	ERR_REG_INVALID_CALLBACK        = "929"
192
 	ERR_REG_INVALID_CALLBACK        = "929"
193
+	ERR_TOOMANYLANGUAGES            = "981"
194
+	ERR_NOLANGUAGE                  = "982"
191
 )
195
 )

+ 89
- 1
irc/server.go View File

154
 		commands:            make(chan Command),
154
 		commands:            make(chan Command),
155
 		connectionLimiter:   connection_limits.NewLimiter(),
155
 		connectionLimiter:   connection_limits.NewLimiter(),
156
 		connectionThrottler: connection_limits.NewThrottler(),
156
 		connectionThrottler: connection_limits.NewThrottler(),
157
-		languages:           NewLanguageManager(),
157
+		languages:           NewLanguageManager(config.Languages.Data),
158
 		listeners:           make(map[string]*ListenerWrapper),
158
 		listeners:           make(map[string]*ListenerWrapper),
159
 		logger:              logger,
159
 		logger:              logger,
160
 		monitorManager:      NewMonitorManager(),
160
 		monitorManager:      NewMonitorManager(),
984
 }
984
 }
985
 
985
 
986
 func (client *Client) getWhoisOf(target *Client) {
986
 func (client *Client) getWhoisOf(target *Client) {
987
+	target.stateMutex.RLock()
988
+	defer target.stateMutex.RUnlock()
989
+
987
 	client.Send(nil, client.server.name, RPL_WHOISUSER, client.nick, target.nick, target.username, target.hostname, "*", target.realname)
990
 	client.Send(nil, client.server.name, RPL_WHOISUSER, client.nick, target.nick, target.username, target.hostname, "*", target.realname)
988
 
991
 
989
 	whoischannels := client.WhoisChannelsNames(target)
992
 	whoischannels := client.WhoisChannelsNames(target)
1002
 	if target.flags[Bot] {
1005
 	if target.flags[Bot] {
1003
 		client.Send(nil, client.server.name, RPL_WHOISBOT, client.nick, target.nick, ircfmt.Unescape("is a $bBot$b on ")+client.server.networkName)
1006
 		client.Send(nil, client.server.name, RPL_WHOISBOT, client.nick, target.nick, ircfmt.Unescape("is a $bBot$b on ")+client.server.networkName)
1004
 	}
1007
 	}
1008
+
1009
+	if 0 < len(target.languages) {
1010
+		params := []string{client.nick, target.nick}
1011
+		for _, str := range client.server.languages.Codes(target.languages) {
1012
+			params = append(params, str)
1013
+		}
1014
+		params = append(params, "can speak these languages")
1015
+		client.Send(nil, client.server.name, RPL_WHOISLANGUAGE, params...)
1016
+	}
1017
+
1005
 	if target.certfp != "" && (client.flags[Operator] || client == target) {
1018
 	if target.certfp != "" && (client.flags[Operator] || client == target) {
1006
 		client.Send(nil, client.server.name, RPL_WHOISCERTFP, client.nick, target.nick, fmt.Sprintf("has client certificate fingerprint %s", target.certfp))
1019
 		client.Send(nil, client.server.name, RPL_WHOISCERTFP, client.nick, target.nick, fmt.Sprintf("has client certificate fingerprint %s", target.certfp))
1007
 	}
1020
 	}
1237
 	removedCaps := caps.NewSet()
1250
 	removedCaps := caps.NewSet()
1238
 	updatedCaps := caps.NewSet()
1251
 	updatedCaps := caps.NewSet()
1239
 
1252
 
1253
+	// Translations
1254
+	currentLanguageValue, _ := CapValues.Get(caps.Languages)
1255
+
1256
+	langCodes := []string{strconv.Itoa(len(config.Languages.Data) + 1), "en"}
1257
+	for _, info := range config.Languages.Data {
1258
+		if info.Incomplete {
1259
+			langCodes = append(langCodes, "~"+info.Code)
1260
+		} else {
1261
+			langCodes = append(langCodes, info.Code)
1262
+		}
1263
+	}
1264
+	newLanguageValue := strings.Join(langCodes, ",")
1265
+	server.logger.Debug("rehash", "Languages:", newLanguageValue)
1266
+
1267
+	if currentLanguageValue != newLanguageValue {
1268
+		updatedCaps.Add(caps.Languages)
1269
+		CapValues.Set(caps.Languages, newLanguageValue)
1270
+	}
1271
+
1240
 	// SASL
1272
 	// SASL
1241
 	if config.Accounts.AuthenticationEnabled && !server.accountAuthenticationEnabled {
1273
 	if config.Accounts.AuthenticationEnabled && !server.accountAuthenticationEnabled {
1242
 		// enabling SASL
1274
 		// enabling SASL
2077
 	return false
2109
 	return false
2078
 }
2110
 }
2079
 
2111
 
2112
+// LANGUAGE <code>{ <code>}
2113
+func languageHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
2114
+	alreadyDoneLanguages := make(map[string]bool)
2115
+	var appliedLanguages []string
2116
+
2117
+	supportedLanguagesCount := server.languages.Count()
2118
+	if supportedLanguagesCount < len(msg.Params) {
2119
+		client.Send(nil, client.server.name, ERR_TOOMANYLANGUAGES, client.nick, strconv.Itoa(supportedLanguagesCount), "You specified too many languages")
2120
+		return false
2121
+	}
2122
+
2123
+	for _, value := range msg.Params {
2124
+		value = strings.ToLower(value)
2125
+		// strip ~ from the language if it has it
2126
+		value = strings.TrimPrefix(value, "~")
2127
+
2128
+		// silently ignore empty languages or those with spaces in them
2129
+		if len(value) == 0 || strings.Contains(value, " ") {
2130
+			continue
2131
+		}
2132
+
2133
+		_, exists := server.languages.Info[value]
2134
+		if !exists {
2135
+			client.Send(nil, client.server.name, ERR_NOLANGUAGE, client.nick, "Languages are not supported by this server")
2136
+			return false
2137
+		}
2138
+
2139
+		// if we've already applied the given language, skip it
2140
+		_, exists = alreadyDoneLanguages[value]
2141
+		if exists {
2142
+			continue
2143
+		}
2144
+
2145
+		appliedLanguages = append(appliedLanguages, value)
2146
+	}
2147
+
2148
+	client.stateMutex.Lock()
2149
+	if len(appliedLanguages) == 1 && appliedLanguages[0] == "en" {
2150
+		// premature optimisation ahoy!
2151
+		client.languages = []string{}
2152
+	} else {
2153
+		client.languages = appliedLanguages
2154
+	}
2155
+	client.stateMutex.Unlock()
2156
+
2157
+	params := []string{client.nick}
2158
+	for _, lang := range appliedLanguages {
2159
+		params = append(params, lang)
2160
+	}
2161
+	params = append(params, client.t("Language preferences have been set"))
2162
+
2163
+	client.Send(nil, client.server.name, RPL_YOURLANGUAGESARE, params...)
2164
+
2165
+	return false
2166
+}
2167
+
2080
 var (
2168
 var (
2081
 	infoString = strings.Split(`      ▄▄▄   ▄▄▄·  ▄▄ •        ▐ ▄       
2169
 	infoString = strings.Split(`      ▄▄▄   ▄▄▄·  ▄▄ •        ▐ ▄       
2082
 ▪     ▀▄ █·▐█ ▀█ ▐█ ▀ ▪▪     •█▌▐█▪     
2170
 ▪     ▀▄ █·▐█ ▀█ ▐█ ▀ ▪▪     •█▌▐█▪     

+ 7
- 6
languages/example.lang.yaml View File

13
 # incomplete - whether to mark this language as incomplete
13
 # incomplete - whether to mark this language as incomplete
14
 incomplete: true
14
 incomplete: true
15
 
15
 
16
-# strings - this holds the actual replacements
17
-# make sure this is the last part of the file, and that the below string, "strings:", stays as-is
18
-# the language-update processor uses the next line to designate which part of the file to ignore and
19
-# which part to actually process.
20
-strings:
21
-  "Welcome to the Internet Relay Network %s": "Welcome bro to the IRN broski %s"
16
+# translations - this holds the actual replacements
17
+# make sure this is the last part of the file, and that the below string, "translations:",
18
+# stays as-is. the language-update processor uses the next line to designate which part of
19
+# the file to ignore and which part to actually process.
20
+translations:
21
+  "Welcome to the Internet Relay Network %s": "Welcome braaaah to the IRN broski %s"
22
+  "Language preferences have been set": "You've set your languages man, wicked!"

+ 12
- 0
oragono.yaml View File

291
     # path to the datastore
291
     # path to the datastore
292
     path: ircd.db
292
     path: ircd.db
293
 
293
 
294
+# languages config
295
+languages:
296
+    # whether to load languages
297
+    enabled: true
298
+
299
+    # default language to use for new clients
300
+    # 'en' is the default English language in the code
301
+    default: en
302
+
303
+    # which directory contains our language files
304
+    path: languages
305
+
294
 # limits - these need to be the same across the network
306
 # limits - these need to be the same across the network
295
 limits:
307
 limits:
296
     # nicklen is the max nick length allowed
308
     # nicklen is the max nick length allowed

Loading…
Cancel
Save