gettext.go 8.0 KB

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