translate.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. // Copyright (C) 2014 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. //go:build ignore
  7. // +build ignore
  8. package main
  9. import (
  10. "bufio"
  11. "encoding/json"
  12. "log"
  13. "os"
  14. "path/filepath"
  15. "regexp"
  16. "strings"
  17. "golang.org/x/net/html"
  18. )
  19. var trans = make(map[string]string)
  20. var attrRe = regexp.MustCompile(`\{\{\s*'([^']+)'\s+\|\s+translate\s*\}\}`)
  21. var attrReCond = regexp.MustCompile(`\{\{.+\s+\?\s+'([^']+)'\s+:\s+'([^']+)'\s+\|\s+translate\s*\}\}`)
  22. // Find both $translate.instant("…") and $translate.instant("…",…) in JS.
  23. // Consider single quote variants too.
  24. var jsRe = []*regexp.Regexp{
  25. regexp.MustCompile(`\$translate\.instant\(\s*"(.+?)"(,.*|\s*)\)`),
  26. regexp.MustCompile(`\$translate\.instant\(\s*'(.+?)'(,.*|\s*)\)`),
  27. }
  28. // exceptions to the untranslated text warning
  29. var noStringRe = regexp.MustCompile(
  30. `^((\W*\{\{.*?\}\} ?.?\/?.?(bps)?\W*)+(\.stignore)?|[^a-zA-Z]+.?[^a-zA-Z]*|[kMGT]?B|Twitter|JS\W?|DEV|https?://\S+|TechUi)$`)
  31. // exceptions to the untranslated text warning specific to aboutModalView.html
  32. var aboutRe = regexp.MustCompile(`^([^/]+/[^/]+|(The Go Pro|Font Awesome ).+|Build \{\{.+\}\}|Copyright .+ the Syncthing Authors\.)$`)
  33. func generalNode(n *html.Node, filename string) {
  34. translate := false
  35. if n.Type == html.ElementNode {
  36. if n.Data == "translate" { // for <translate>Text</translate>
  37. translate = true
  38. } else if n.Data == "style" || n.Data == "noscript" {
  39. return
  40. } else {
  41. for _, a := range n.Attr {
  42. if a.Key == "translate" {
  43. translate = true
  44. } else if a.Key == "id" && (a.Val == "contributor-list" ||
  45. a.Val == "copyright-notices") {
  46. // Don't translate a list of names and
  47. // copyright notices of other projects
  48. return
  49. } else {
  50. for _, matches := range attrRe.FindAllStringSubmatch(a.Val, -1) {
  51. translation(matches[1])
  52. }
  53. for _, matches := range attrReCond.FindAllStringSubmatch(a.Val, -1) {
  54. translation(matches[1])
  55. translation(matches[2])
  56. }
  57. if a.Key == "data-content" &&
  58. !noStringRe.MatchString(a.Val) {
  59. log.Println("Untranslated data-content string (" + filename + "):")
  60. log.Print("\t" + a.Val)
  61. }
  62. }
  63. }
  64. }
  65. } else if n.Type == html.TextNode {
  66. v := strings.TrimSpace(n.Data)
  67. if len(v) > 1 && !noStringRe.MatchString(v) &&
  68. !(filename == "aboutModalView.html" && aboutRe.MatchString(v)) &&
  69. !(filename == "logbar.html" && (v == "warn" || v == "errors")) {
  70. log.Println("Untranslated text node (" + filename + "):")
  71. log.Print("\t" + v)
  72. }
  73. }
  74. for c := n.FirstChild; c != nil; c = c.NextSibling {
  75. if translate {
  76. inTranslate(c, filename)
  77. } else {
  78. generalNode(c, filename)
  79. }
  80. }
  81. }
  82. func inTranslate(n *html.Node, filename string) {
  83. if n.Type == html.TextNode {
  84. translation(n.Data)
  85. } else {
  86. log.Println("translate node with non-text child < (" + filename + ")")
  87. log.Println(n)
  88. }
  89. if n.FirstChild != nil {
  90. log.Println("translate node has children (" + filename + "):")
  91. log.Println(n.Data)
  92. }
  93. }
  94. func translation(v string) {
  95. v = strings.TrimSpace(v)
  96. if _, ok := trans[v]; !ok {
  97. av := strings.Replace(v, "{%", "{{", -1)
  98. av = strings.Replace(av, "%}", "}}", -1)
  99. trans[v] = av
  100. }
  101. }
  102. func walkerFor(basePath string) filepath.WalkFunc {
  103. return func(name string, info os.FileInfo, err error) error {
  104. if err != nil {
  105. return err
  106. }
  107. if !info.Mode().IsRegular() {
  108. return nil
  109. }
  110. fd, err := os.Open(name)
  111. if err != nil {
  112. log.Fatal(err)
  113. }
  114. defer fd.Close()
  115. switch filepath.Ext(name) {
  116. case ".html":
  117. doc, err := html.Parse(fd)
  118. if err != nil {
  119. log.Fatal(err)
  120. }
  121. generalNode(doc, filepath.Base(name))
  122. case ".js":
  123. for s := bufio.NewScanner(fd); s.Scan(); {
  124. for _, re := range jsRe {
  125. for _, matches := range re.FindAllStringSubmatch(s.Text(), -1) {
  126. translation(matches[1])
  127. }
  128. }
  129. }
  130. }
  131. return nil
  132. }
  133. }
  134. func collectThemes(basePath string) {
  135. files, err := os.ReadDir(basePath)
  136. if err != nil {
  137. log.Fatal(err)
  138. }
  139. for _, f := range files {
  140. if f.IsDir() {
  141. key := "theme-name-" + f.Name()
  142. if _, ok := trans[key]; !ok {
  143. name := strings.Title(f.Name())
  144. trans[key] = name
  145. }
  146. }
  147. }
  148. }
  149. func main() {
  150. fd, err := os.Open(os.Args[1])
  151. if err != nil {
  152. log.Fatal(err)
  153. }
  154. err = json.NewDecoder(fd).Decode(&trans)
  155. if err != nil {
  156. log.Fatal(err)
  157. }
  158. fd.Close()
  159. var guiDir = os.Args[2]
  160. filepath.Walk(guiDir, walkerFor(guiDir))
  161. collectThemes(guiDir)
  162. bs, err := json.MarshalIndent(trans, "", " ")
  163. if err != nil {
  164. log.Fatal(err)
  165. }
  166. os.Stdout.Write(bs)
  167. os.Stdout.WriteString("\n")
  168. }