generate_translations.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. package main
  2. import (
  3. "io/ioutil"
  4. "log"
  5. "os"
  6. "path/filepath"
  7. "regexp"
  8. "strings"
  9. "text/template"
  10. "time"
  11. "code.osinet.fr/fgm/kurz/translations"
  12. "github.com/nicksnyder/go-i18n/v2/i18n"
  13. "github.com/spf13/cobra"
  14. "golang.org/x/text/language"
  15. "gopkg.in/yaml.v2"
  16. )
  17. var cmdGenerateTranslations = &cobra.Command{
  18. Args: cobra.NoArgs,
  19. Long: "Generate the translations package",
  20. Run: generateTranslationsHandler,
  21. Short: "Converts the messages.*.yaml files to a compilable i18n messages embed package. Only useful during Kurz development",
  22. Use: "generate-translations",
  23. }
  24. func init() {
  25. cmd.AddCommand(cmdGenerateTranslations)
  26. }
  27. /*
  28. dumpMessages is a debug helper, logging a messageMap in YAML format for readability.
  29. */
  30. func dumpMessages(messageMap translations.MessageMap) {
  31. out, err := yaml.Marshal(messageMap)
  32. if err != nil {
  33. panic(err)
  34. }
  35. log.Println("Messages:\n" + string(out))
  36. }
  37. func generateTranslationsHandler(cmd *cobra.Command, args []string) {
  38. cwd, err := os.Getwd()
  39. if err != nil {
  40. panic(err)
  41. }
  42. t0 := time.Now()
  43. log.Printf("Finding message catalogs below %s\n", cwd)
  44. tags := []string{
  45. language.English.String(),
  46. language.French.String(),
  47. }
  48. messageMap := make(translations.MessageMap)
  49. for _, tag := range tags {
  50. messageMap[language.Make(tag)] = [](*i18n.Message){}
  51. }
  52. // Build the regex matching message file names.
  53. re := regexp.MustCompile("messages\\.(?:" + strings.Join(tags, "|") + ")\\.yaml")
  54. scanned := 0
  55. catalogCount := 0
  56. walkErr := filepath.Walk(cwd, scanCatalog(messageMap, re, &scanned, &catalogCount))
  57. if walkErr != nil {
  58. panic(walkErr)
  59. }
  60. t1 := time.Now()
  61. diff := t1.Sub(t0)
  62. log.Printf("%d items scanned, %d catalogs loaded in %d msec\n", scanned, catalogCount, diff/time.Millisecond)
  63. // dumpMessages(messageMap)
  64. pwd, err := os.Getwd()
  65. if err != nil {
  66. panic(err)
  67. }
  68. println(pwd)
  69. translationsDir := pwd + "/translations"
  70. tpl := template.Must(template.ParseFiles(translationsDir + "/template.go.gotext"))
  71. gen, err := os.Create(translationsDir + "/generated.go")
  72. if err != nil {
  73. panic(err)
  74. }
  75. err = tpl.ExecuteTemplate(gen, "template.go.gotext", messageMap)
  76. if err != nil {
  77. panic(err)
  78. }
  79. }
  80. /*
  81. parseMessageFileBytes merges the messages found in "buf" into the messageMap.
  82. See scanCatalog().
  83. */
  84. func parseMessages(messagesMap translations.MessageMap, buf []byte, tag language.Tag) error {
  85. if len(buf) == 0 {
  86. return nil
  87. }
  88. raw := make(map[string]interface{})
  89. if err := yaml.Unmarshal(buf, &raw); err != nil {
  90. return err
  91. }
  92. for id, item := range raw {
  93. switch item.(type) {
  94. // Degenerate case: "key: one"
  95. case string:
  96. message := &i18n.Message{
  97. ID: id,
  98. Other: item.(string),
  99. }
  100. messagesMap[tag] = append(messagesMap[tag], message)
  101. // Normal case: key: { ID: ..., one: ... }.
  102. case map[interface{}]interface{}:
  103. serial, err := yaml.Marshal(item)
  104. if err != nil {
  105. panic(err)
  106. }
  107. message := &i18n.Message{
  108. ID: id,
  109. }
  110. err = yaml.Unmarshal(serial, message)
  111. if err != nil {
  112. panic(err)
  113. }
  114. messagesMap[tag] = append(messagesMap[tag], message)
  115. default:
  116. log.Fatalf("Unsupported message format %#v", item)
  117. }
  118. }
  119. return nil
  120. }
  121. // parsePath is a copy of go-i18n.internal.parsePath
  122. //
  123. // Used under its MIT license.
  124. func parsePath(path string) (langTag, format string) {
  125. formatStartIdx := -1
  126. for i := len(path) - 1; i >= 0; i-- {
  127. c := path[i]
  128. if os.IsPathSeparator(c) {
  129. if formatStartIdx != -1 {
  130. langTag = path[i+1 : formatStartIdx]
  131. }
  132. return
  133. }
  134. if path[i] == '.' {
  135. if formatStartIdx != -1 {
  136. langTag = path[i+1 : formatStartIdx]
  137. return
  138. }
  139. if formatStartIdx == -1 {
  140. format = path[i+1:]
  141. formatStartIdx = i
  142. }
  143. }
  144. }
  145. if formatStartIdx != -1 {
  146. langTag = path[:formatStartIdx]
  147. }
  148. return
  149. }
  150. /*
  151. scanCatalog builds a WalkFn callback parsing each message file as it is found, and
  152. merging its values into messageMap.
  153. See generateTranslationsHandler
  154. */
  155. func scanCatalog(messageMap translations.MessageMap, re *regexp.Regexp, scanned *int, catalogCount *int) filepath.WalkFunc {
  156. return func(path string, info os.FileInfo, err error) error {
  157. *scanned++
  158. if err != nil {
  159. panic(err)
  160. }
  161. if info.IsDir() {
  162. return nil
  163. }
  164. if !re.MatchString(info.Name()) {
  165. return nil
  166. }
  167. langid, _ := parsePath(path)
  168. tag := language.Make(langid)
  169. _, ok := messageMap[tag]
  170. if !ok {
  171. log.Printf("Found message catalog %s in unexpected language %s, skipping.",
  172. path, tag.String())
  173. return nil
  174. }
  175. buf, err := ioutil.ReadFile(path)
  176. if err != nil {
  177. panic(err)
  178. }
  179. err = parseMessages(messageMap, buf, tag)
  180. *catalogCount++
  181. return err
  182. }
  183. }