gettext.go 8.1 KB

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