gettext.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. //go:generate go run .
  2. package main
  3. import (
  4. "bufio"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "regexp"
  9. "runtime"
  10. "sort"
  11. "strings"
  12. "github.com/uozi-tech/cosy/logger"
  13. )
  14. // Directories to exclude
  15. var excludeDirs = []string{
  16. ".devcontainer", ".github", ".idea", ".pnpm-store",
  17. ".vscode", "app", "query", "tmp", "cmd",
  18. }
  19. // Regular expression to match import statements for translation package
  20. var importRegex = regexp.MustCompile(`import\s+\(\s*((?:.|\n)*?)\s*\)|\s*import\s+(.*?)\s+".*?(?:internal/translation|github\.com/0xJacky/Nginx-UI/internal/translation)"`)
  21. var singleImportRegex = regexp.MustCompile(`\s*(?:(\w+)\s+)?".*?(?:internal/translation|github\.com/0xJacky/Nginx-UI/internal/translation)"`)
  22. func main() {
  23. logger.Init("release")
  24. // Start scanning from the project root
  25. _, file, _, ok := runtime.Caller(0)
  26. if !ok {
  27. logger.Error("Unable to get the current file")
  28. return
  29. }
  30. root := filepath.Join(filepath.Dir(file), "../../")
  31. calls := make(map[string]bool)
  32. // Scan all Go files
  33. err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
  34. if err != nil {
  35. return err
  36. }
  37. // Skip excluded directories
  38. for _, dir := range excludeDirs {
  39. if strings.Contains(path, dir) {
  40. if info.IsDir() {
  41. return filepath.SkipDir
  42. }
  43. return nil
  44. }
  45. }
  46. // Only process Go files
  47. if !info.IsDir() && strings.HasSuffix(path, ".go") {
  48. findTranslationC(path, calls)
  49. }
  50. return nil
  51. })
  52. if err != nil {
  53. logger.Errorf("Error walking the path: %v\n", err)
  54. return
  55. }
  56. // Generate a single TS file
  57. generateSingleTSFile(root, calls)
  58. logger.Infof("Found %d translation messages\n", len(calls))
  59. }
  60. // findTranslationC finds all translation.C calls in a file and adds them to the calls map
  61. func findTranslationC(filePath string, calls map[string]bool) {
  62. // Read the entire file content
  63. content, err := os.ReadFile(filePath)
  64. if err != nil {
  65. logger.Errorf("Error reading file %s: %v\n", filePath, err)
  66. return
  67. }
  68. fileContent := string(content)
  69. // Find the translation package alias from import statements
  70. alias := findTranslationAlias(fileContent)
  71. if alias == "" {
  72. // No translation package imported, skip this file
  73. return
  74. }
  75. // First pre-process the file content to handle multi-line string concatenation
  76. // Replace newlines and spaces between string concatenation to make them easier to parse
  77. preprocessed := regexp.MustCompile(`"\s*\+\s*(\r?\n)?\s*"`).ReplaceAllString(fileContent, "")
  78. // Create regex pattern for translation.C calls
  79. pattern := fmt.Sprintf(`%s\.C\(\s*"([^"]+)"`, alias)
  80. cCallRegex := regexp.MustCompile(pattern)
  81. // Find all matches
  82. matches := cCallRegex.FindAllStringSubmatch(preprocessed, -1)
  83. for _, match := range matches {
  84. if len(match) >= 2 {
  85. message := match[1]
  86. // Clean up the message (remove escaped quotes, etc.)
  87. message = strings.ReplaceAll(message, "\\\"", "\"")
  88. message = strings.ReplaceAll(message, "\\'", "'")
  89. // Add to the map if not already present
  90. if _, exists := calls[message]; !exists {
  91. calls[message] = true
  92. }
  93. }
  94. }
  95. // Handle backtick strings separately (multi-line strings)
  96. backtickPattern := fmt.Sprintf(`%s\.C\(\s*\x60([^\x60]*)\x60`, alias)
  97. backtickRegex := regexp.MustCompile(backtickPattern)
  98. // Find all matches with backticks
  99. backtickMatches := backtickRegex.FindAllStringSubmatch(fileContent, -1)
  100. for _, match := range backtickMatches {
  101. if len(match) >= 2 {
  102. message := match[1]
  103. // Add to the map if not already present
  104. if _, exists := calls[message]; !exists {
  105. calls[message] = true
  106. }
  107. }
  108. }
  109. // Use a more direct approach to handle multi-line string concatenation
  110. // This regex finds translation.C calls with string concatenation
  111. // concatPattern := fmt.Sprintf(`%s\.C\(\s*"(.*?)"\s*(?:\+\s*"(.*?)")+\s*\)`, alias)
  112. // concatRegex := regexp.MustCompile(concatPattern)
  113. // We need to handle this specifically by manually parsing the file
  114. translationStart := fmt.Sprintf(`%s\.C\(`, alias)
  115. lines := strings.Split(fileContent, "\n")
  116. for i := 0; i < len(lines); i++ {
  117. if strings.Contains(lines[i], translationStart) && strings.Contains(lines[i], `"`) && strings.Contains(lines[i], `+`) {
  118. // Potential multi-line concatenated string found
  119. // startLine := i
  120. var concatenatedParts []string
  121. currentLine := lines[i]
  122. // Extract the first part
  123. firstPartMatch := regexp.MustCompile(`C\(\s*"([^"]*)"`)
  124. fMatches := firstPartMatch.FindStringSubmatch(currentLine)
  125. if len(fMatches) >= 2 {
  126. concatenatedParts = append(concatenatedParts, fMatches[1])
  127. }
  128. // Look for continuation lines with string parts
  129. for j := i + 1; j < len(lines) && j < i+10; j++ { // Limit to 10 lines
  130. if strings.Contains(lines[j], `"`) && !strings.Contains(lines[j], translationStart) {
  131. // Extract string parts
  132. partMatch := regexp.MustCompile(`"([^"]*)"`)
  133. pMatches := partMatch.FindAllStringSubmatch(lines[j], -1)
  134. for _, pm := range pMatches {
  135. if len(pm) >= 2 {
  136. concatenatedParts = append(concatenatedParts, pm[1])
  137. }
  138. }
  139. // If we find a closing parenthesis, we've reached the end
  140. if strings.Contains(lines[j], `)`) {
  141. break
  142. }
  143. } else if !strings.Contains(lines[j], `+`) {
  144. // If the line doesn't contain a +, we've likely reached the end
  145. break
  146. }
  147. }
  148. // Combine all parts
  149. if len(concatenatedParts) > 0 {
  150. message := strings.Join(concatenatedParts, "")
  151. if _, exists := calls[message]; !exists {
  152. calls[message] = true
  153. }
  154. }
  155. }
  156. }
  157. }
  158. // findTranslationAlias finds the alias for the translation package in import statements
  159. func findTranslationAlias(fileContent string) string {
  160. // Default alias
  161. alias := "translation"
  162. // Find import blocks
  163. matches := importRegex.FindAllStringSubmatch(fileContent, -1)
  164. for _, match := range matches {
  165. if len(match) >= 3 && match[1] != "" {
  166. // This is a block import, search inside it
  167. imports := match[1]
  168. singleMatches := singleImportRegex.FindAllStringSubmatch(imports, -1)
  169. for _, singleMatch := range singleMatches {
  170. if len(singleMatch) >= 2 && singleMatch[1] != "" {
  171. // Custom alias found
  172. return singleMatch[1]
  173. }
  174. }
  175. } else if len(match) >= 3 && match[2] != "" {
  176. // This is a single-line import
  177. singleMatch := singleImportRegex.FindAllStringSubmatch(match[2], -1)
  178. if len(singleMatch) > 0 && len(singleMatch[0]) >= 2 && singleMatch[0][1] != "" {
  179. // Custom alias found
  180. return singleMatch[0][1]
  181. }
  182. }
  183. }
  184. return alias
  185. }
  186. // generateSingleTSFile generates a single TS file with all translation messages
  187. func generateSingleTSFile(root string, calls map[string]bool) {
  188. outputPath := filepath.Join(root, "app/src/language/generate.ts")
  189. // Create the directory if it doesn't exist
  190. err := os.MkdirAll(filepath.Dir(outputPath), 0755)
  191. if err != nil {
  192. logger.Errorf("Error creating directory: %v\n", err)
  193. return
  194. }
  195. // Create the output file
  196. file, err := os.Create(outputPath)
  197. if err != nil {
  198. logger.Errorf("Error creating file: %v\n", err)
  199. return
  200. }
  201. defer file.Close()
  202. writer := bufio.NewWriter(file)
  203. // Write the header
  204. writer.WriteString("// This file is auto-generated. DO NOT EDIT MANUALLY.\n\n")
  205. writer.WriteString("export const msg = [\n")
  206. // Extract and sort the translation messages to ensure stable output
  207. var messages []string
  208. for message := range calls {
  209. messages = append(messages, message)
  210. }
  211. sort.Strings(messages)
  212. // Write each translation message in sorted order
  213. for _, message := range messages {
  214. // Escape single quotes and handle newlines in the message for JavaScript
  215. escapedMessage := strings.ReplaceAll(message, "'", "\\'")
  216. // Replace newlines with space to ensure proper formatting in the generated TS file
  217. escapedMessage = strings.ReplaceAll(escapedMessage, "\n", " ")
  218. escapedMessage = strings.ReplaceAll(escapedMessage, "\r", "")
  219. writer.WriteString(fmt.Sprintf(" $gettext('%s'),\n", escapedMessage))
  220. }
  221. writer.WriteString("]\n")
  222. writer.Flush()
  223. logger.Infof("Generated TS file at %s\n", outputPath)
  224. }