gettext.go 7.7 KB

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