Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

languages.go 6.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. // Copyright (c) 2018 Daniel Oaks <daniel@danieloaks.net>
  2. // released under the MIT license
  3. package languages
  4. import (
  5. "encoding/json"
  6. "fmt"
  7. "os"
  8. "path/filepath"
  9. "sort"
  10. "strconv"
  11. "strings"
  12. "gopkg.in/yaml.v2"
  13. )
  14. const (
  15. // for a language (e.g., `fi-FI`) to be supported
  16. // it must have a metadata file named, e.g., `fi-FI.lang.yaml`
  17. metadataFileSuffix = ".lang.yaml"
  18. )
  19. var (
  20. stringsFileSuffixes = []string{"-irc.lang.json", "-help.lang.json", "-nickserv.lang.json", "-hostserv.lang.json", "-chanserv.lang.json"}
  21. )
  22. // LangData is the data contained in a language file.
  23. type LangData struct {
  24. Name string
  25. Code string
  26. Contributors string
  27. Incomplete bool
  28. }
  29. // Manager manages our languages and provides translation abilities.
  30. type Manager struct {
  31. Languages map[string]LangData
  32. translations map[string]map[string]string
  33. defaultLang string
  34. }
  35. // NewManager returns a new Manager.
  36. func NewManager(enabled bool, path string, defaultLang string) (lm *Manager, err error) {
  37. lm = &Manager{
  38. Languages: make(map[string]LangData),
  39. translations: make(map[string]map[string]string),
  40. defaultLang: defaultLang,
  41. }
  42. // make fake "en" info
  43. lm.Languages["en"] = LangData{
  44. Code: "en",
  45. Name: "English",
  46. Contributors: "Oragono contributors and the IRC community",
  47. }
  48. if enabled {
  49. err = lm.loadData(path)
  50. if err == nil {
  51. // successful load, check that defaultLang is sane
  52. _, ok := lm.Languages[lm.defaultLang]
  53. if !ok {
  54. err = fmt.Errorf("Cannot find default language [%s]", lm.defaultLang)
  55. }
  56. }
  57. } else {
  58. lm.defaultLang = "en"
  59. }
  60. return
  61. }
  62. func (lm *Manager) loadData(path string) (err error) {
  63. files, err := os.ReadDir(path)
  64. if err != nil {
  65. return
  66. }
  67. // 1. for each language that has a ${langcode}.lang.yaml in the languages path
  68. // 2. load ${langcode}.lang.yaml
  69. // 3. load ${langcode}-irc.lang.json and friends as the translations
  70. for _, f := range files {
  71. if f.IsDir() {
  72. continue
  73. }
  74. // glob up *.lang.yaml in the directory
  75. name := f.Name()
  76. if !strings.HasSuffix(name, metadataFileSuffix) {
  77. continue
  78. }
  79. prefix := strings.TrimSuffix(name, metadataFileSuffix)
  80. // load, e.g., `zh-CN.lang.yaml`
  81. var data []byte
  82. data, err = os.ReadFile(filepath.Join(path, name))
  83. if err != nil {
  84. return
  85. }
  86. var langInfo LangData
  87. err = yaml.Unmarshal(data, &langInfo)
  88. if err != nil {
  89. return err
  90. }
  91. if langInfo.Code == "en" {
  92. 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")
  93. }
  94. // check for duplicate languages
  95. _, exists := lm.Languages[strings.ToLower(langInfo.Code)]
  96. if exists {
  97. return fmt.Errorf("Language code [%s] defined twice", langInfo.Code)
  98. }
  99. // slurp up all translation files with `prefix` into a single translation map
  100. translations := make(map[string]string)
  101. for _, translationSuffix := range stringsFileSuffixes {
  102. stringsFilePath := filepath.Join(path, prefix+translationSuffix)
  103. data, err = os.ReadFile(stringsFilePath)
  104. if err != nil {
  105. continue // skip missing paths
  106. }
  107. var tlList map[string]string
  108. err = json.Unmarshal(data, &tlList)
  109. if err != nil {
  110. return fmt.Errorf("invalid json for translation file %s: %s", stringsFilePath, err.Error())
  111. }
  112. for key, value := range tlList {
  113. // because of how crowdin works, this is how we skip untranslated lines
  114. if key == value || strings.TrimSpace(value) == "" {
  115. continue
  116. }
  117. translations[key] = value
  118. }
  119. }
  120. if len(translations) == 0 {
  121. // skip empty translations
  122. continue
  123. }
  124. // sanity check the language definition from the yaml file
  125. if langInfo.Code == "" || langInfo.Name == "" || langInfo.Contributors == "" {
  126. return fmt.Errorf("Code, name or contributors is empty in language file [%s]", name)
  127. }
  128. key := strings.ToLower(langInfo.Code)
  129. lm.Languages[key] = langInfo
  130. lm.translations[key] = translations
  131. }
  132. return nil
  133. }
  134. // Default returns the default languages.
  135. func (lm *Manager) Default() []string {
  136. return []string{lm.defaultLang}
  137. }
  138. // Count returns how many languages we have.
  139. func (lm *Manager) Count() int {
  140. return len(lm.Languages)
  141. }
  142. // Enabled returns whether translation is enabled.
  143. func (lm *Manager) Enabled() bool {
  144. return len(lm.translations) != 0
  145. }
  146. // Translators returns the languages we have and the translators.
  147. func (lm *Manager) Translators() []string {
  148. var tlist sort.StringSlice
  149. for _, info := range lm.Languages {
  150. if info.Code == "en" {
  151. continue
  152. }
  153. tlist = append(tlist, fmt.Sprintf("%s (%s): %s", info.Name, info.Code, info.Contributors))
  154. }
  155. tlist.Sort()
  156. return tlist
  157. }
  158. // Codes returns the proper language codes for the given casefolded language codes.
  159. func (lm *Manager) Codes(codes []string) []string {
  160. var newCodes []string
  161. for _, code := range codes {
  162. info, exists := lm.Languages[code]
  163. if exists {
  164. newCodes = append(newCodes, info.Code)
  165. }
  166. }
  167. if len(newCodes) == 0 {
  168. newCodes = []string{"en"}
  169. }
  170. return newCodes
  171. }
  172. // Translate returns the given string, translated into the given language.
  173. func (lm *Manager) Translate(languages []string, originalString string) string {
  174. // not using any special languages
  175. if len(languages) == 0 || languages[0] == "en" || len(lm.translations) == 0 {
  176. return originalString
  177. }
  178. for _, lang := range languages {
  179. lang = strings.ToLower(lang)
  180. if lang == "en" {
  181. return originalString
  182. }
  183. translations, exists := lm.translations[lang]
  184. if !exists {
  185. continue
  186. }
  187. newString, exists := translations[originalString]
  188. if !exists {
  189. continue
  190. }
  191. // found a valid translation!
  192. return newString
  193. }
  194. // didn't find any translation
  195. return originalString
  196. }
  197. func (lm *Manager) CapValue() string {
  198. langCodes := make(sort.StringSlice, len(lm.Languages)+1)
  199. langCodes[0] = strconv.Itoa(len(lm.Languages))
  200. i := 1
  201. for _, info := range lm.Languages {
  202. codeToken := info.Code
  203. if info.Incomplete {
  204. codeToken = "~" + info.Code
  205. }
  206. langCodes[i] = codeToken
  207. i += 1
  208. }
  209. langCodes.Sort()
  210. return strings.Join(langCodes, ",")
  211. }