mirror of
				https://github.com/TeaOSLab/EdgeCommon.git
				synced 2025-11-04 05:00:24 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			466 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			466 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
 | 
						|
 | 
						|
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"github.com/TeaOSLab/EdgeCommon/pkg/langs"
 | 
						|
	"github.com/iwind/TeaGo/Tea"
 | 
						|
	_ "github.com/iwind/TeaGo/bootstrap"
 | 
						|
	"github.com/iwind/TeaGo/lists"
 | 
						|
	"github.com/iwind/TeaGo/types"
 | 
						|
	"go/format"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"regexp"
 | 
						|
	"sort"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
)
 | 
						|
 | 
						|
func main() {
 | 
						|
	var args = os.Args
 | 
						|
	if len(args) >= 2 {
 | 
						|
		switch args[1] {
 | 
						|
		case "generate":
 | 
						|
			// generate go codes from json files
 | 
						|
			runGenerate()
 | 
						|
		case "search":
 | 
						|
			// search hans from dir path
 | 
						|
			runSearch()
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		fmt.Println("Usage: langs [generate|search]")
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func runGenerate() {
 | 
						|
	var rootDir = filepath.Clean(Tea.Root + "/../pkg/langs/protos")
 | 
						|
	dir, err := os.Open(rootDir)
 | 
						|
	if err != nil {
 | 
						|
		fmt.Println("[ERROR]read dir failed: " + err.Error())
 | 
						|
		return
 | 
						|
	}
 | 
						|
	defer func() {
 | 
						|
		_ = dir.Close()
 | 
						|
	}()
 | 
						|
 | 
						|
	files, err := dir.Readdir(0)
 | 
						|
	if err != nil {
 | 
						|
		fmt.Println("[ERROR]read dir failed: " + err.Error())
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	var dirRegexp = regexp.MustCompile(`^[a-z]+-[a-z]+$`)
 | 
						|
	var jsonFileNameRegexp = regexp.MustCompile(`^([a-zA-Z0-9]+)(_([a-zA-Z0-9]+))*\.json$`)
 | 
						|
	var messageCodeRegexp = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
 | 
						|
	var jsonCommentRegexp = regexp.MustCompile(`//\s+.+`)
 | 
						|
 | 
						|
	var messageCodes = []string{}
 | 
						|
	var langMaps = map[string]*langs.Lang{} // lang => *langs.Lang
 | 
						|
	var defaultLang = langs.DefaultManager().DefaultLang()
 | 
						|
 | 
						|
	const maxMessageCodeLen = 128
 | 
						|
 | 
						|
	for _, file := range files {
 | 
						|
		var dirName = file.Name()
 | 
						|
 | 
						|
		if !file.IsDir() || !dirRegexp.MatchString(dirName) {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		var langCode = dirName
 | 
						|
		var isBaseLang = langCode == defaultLang
 | 
						|
 | 
						|
		var processOk = func() bool {
 | 
						|
			jsonFiles, err := filepath.Glob(rootDir + "/" + dirName + "/*.json")
 | 
						|
			if err != nil {
 | 
						|
				fmt.Println("[ERROR]list json files failed: " + err.Error())
 | 
						|
				return false
 | 
						|
			}
 | 
						|
 | 
						|
			for _, jsonFile := range jsonFiles {
 | 
						|
				var jsonFileName = filepath.Base(jsonFile)
 | 
						|
				if len(jsonFileName) == 0 || !jsonFileNameRegexp.MatchString(jsonFileName) {
 | 
						|
					continue
 | 
						|
				}
 | 
						|
				var module = strings.TrimSuffix(jsonFileName, ".json")
 | 
						|
 | 
						|
				data, err := os.ReadFile(jsonFile)
 | 
						|
				if err != nil {
 | 
						|
					fmt.Println("[ERROR]read json file '" + jsonFile + "' failed: " + err.Error())
 | 
						|
					return false
 | 
						|
				}
 | 
						|
 | 
						|
				// remove comments in json
 | 
						|
				data = jsonCommentRegexp.ReplaceAll(data, []byte{})
 | 
						|
 | 
						|
				var m = map[string]string{} // code => value
 | 
						|
				err = json.Unmarshal(data, &m)
 | 
						|
				if err != nil {
 | 
						|
					fmt.Println("[ERROR]decode json file '" + jsonFile + "' failed: " + err.Error())
 | 
						|
					return false
 | 
						|
				}
 | 
						|
 | 
						|
				var newM = map[string]string{}
 | 
						|
				for code, value := range m {
 | 
						|
					if !messageCodeRegexp.MatchString(code) {
 | 
						|
						fmt.Println("[ERROR]invalid message code '" + code + "'")
 | 
						|
						return false
 | 
						|
					}
 | 
						|
 | 
						|
					var fullCode = module + "@" + code
 | 
						|
 | 
						|
					if len(fullCode) > maxMessageCodeLen {
 | 
						|
						fmt.Println("[ERROR]message code '" + fullCode + "' too long, max length: " + types.String(maxMessageCodeLen))
 | 
						|
						return false
 | 
						|
					}
 | 
						|
 | 
						|
					if isBaseLang {
 | 
						|
						messageCodes = append(messageCodes, fullCode)
 | 
						|
					}
 | 
						|
					newM[fullCode] = value
 | 
						|
				}
 | 
						|
 | 
						|
				finalLang, ok := langMaps[langCode]
 | 
						|
				if !ok {
 | 
						|
					finalLang = langs.NewLang(langCode)
 | 
						|
					langMaps[langCode] = finalLang
 | 
						|
				}
 | 
						|
				for code, value := range newM {
 | 
						|
					if finalLang.Has(langs.MessageCode(code)) {
 | 
						|
						fmt.Println("[ERROR]message code '" + code + "' duplicated")
 | 
						|
						return false
 | 
						|
					}
 | 
						|
					finalLang.Set(langs.MessageCode(code), value)
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			return true
 | 
						|
		}()
 | 
						|
		if !processOk {
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// compile
 | 
						|
	for langCode, lang := range langMaps {
 | 
						|
		err = lang.Compile()
 | 
						|
		if err != nil {
 | 
						|
			fmt.Println("[ERROR]compile '" + langCode + "' failed: " + err.Error())
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// check message codes
 | 
						|
	fmt.Println("checking message codes ...")
 | 
						|
	var defaultMessageMap = map[langs.MessageCode]string{}
 | 
						|
	for langCode, messageLang := range langMaps {
 | 
						|
		if langCode == defaultLang { // only check lang not equal to 'en-us'
 | 
						|
			defaultMessageMap = messageLang.GetAll()
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		for messageCode := range messageLang.GetAll() {
 | 
						|
			if !lists.ContainsString(messageCodes, messageCode.String()) {
 | 
						|
				fmt.Println("[ERROR]message code '" + messageCode.String() + "' in lang '" + langCode + "' not exist in default lang file ('" + defaultLang + "')")
 | 
						|
				return
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	fmt.Println("found '" + types.String(len(messageCodes)) + "' message codes")
 | 
						|
 | 
						|
	// generate codes/codes.go
 | 
						|
	sort.Strings(messageCodes)
 | 
						|
	var codesSource = `
 | 
						|
// generated by run 'langs generate'
 | 
						|
 | 
						|
package codes
 | 
						|
 | 
						|
import(
 | 
						|
	"github.com/TeaOSLab/EdgeCommon/pkg/langs"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	`
 | 
						|
 | 
						|
	for index, messageCode := range messageCodes {
 | 
						|
		// add comment to message code
 | 
						|
		comment, _, _ := strings.Cut(defaultMessageMap[langs.MessageCode(messageCode)], "\n")
 | 
						|
 | 
						|
		codesSource += upperWords(messageCode) + " langs.MessageCode = " + strconv.Quote(messageCode) + " // " + comment
 | 
						|
 | 
						|
		// add NL
 | 
						|
		if index != len(messageCodes)-1 {
 | 
						|
			codesSource += "\n"
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	codesSource += `
 | 
						|
)
 | 
						|
`
 | 
						|
 | 
						|
	formattedCodesSource, err := format.Source([]byte(codesSource))
 | 
						|
	if err != nil {
 | 
						|
		fmt.Println("[ERROR]format 'codes.go' failed: " + err.Error())
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	fmt.Println("generating 'codes/codes.go' ...")
 | 
						|
	err = os.WriteFile(Tea.Root+"/../pkg/langs/codes/codes.go", formattedCodesSource, 0666)
 | 
						|
	if err != nil {
 | 
						|
		fmt.Println("[ERROR]write to 'codes.go' failed: " + err.Error())
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	// generate messages_LANG.go
 | 
						|
	for langCode, messageLang := range langMaps {
 | 
						|
		var langFile = strings.ReplaceAll("messages/messages_"+langCode+".go", "-", "_")
 | 
						|
 | 
						|
		fmt.Println("generating '" + langFile + "' ...")
 | 
						|
		var source = `
 | 
						|
// generated by run 'langs generate'
 | 
						|
 | 
						|
package messages
 | 
						|
 | 
						|
import(
 | 
						|
	"github.com/TeaOSLab/EdgeCommon/pkg/langs"
 | 
						|
)
 | 
						|
 | 
						|
func init() {
 | 
						|
	langs.Load("` + langCode + `", map[langs.MessageCode]string{
 | 
						|
	`
 | 
						|
 | 
						|
		for _, code := range messageCodes {
 | 
						|
			var value = messageLang.Get(langs.MessageCode(code))
 | 
						|
			source += strconv.Quote(code) + ": " + strconv.Quote(value) + ",\n"
 | 
						|
		}
 | 
						|
 | 
						|
		source += `
 | 
						|
	})
 | 
						|
}
 | 
						|
`
 | 
						|
 | 
						|
		formattedSource, err := format.Source([]byte(source))
 | 
						|
		if err != nil {
 | 
						|
			fmt.Println("[ERROR]format '" + langFile + "' failed: " + err.Error())
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		err = os.WriteFile(Tea.Root+"/../pkg/langs/"+langFile, formattedSource, 0666)
 | 
						|
		if err != nil {
 | 
						|
			fmt.Println("[ERROR]write file '" + langFile + "' failed: " + err.Error())
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// generate language javascript files for EdgeAdmin and EdgeUser (commercial versions)
 | 
						|
	for lang, messageLang := range langMaps {
 | 
						|
		if lang != defaultLang {
 | 
						|
			// TODO merge messageMap with default message map
 | 
						|
		}
 | 
						|
 | 
						|
		for _, component := range []string{"EdgeAdmin", "EdgeUser"} {
 | 
						|
			fmt.Println("generating '" + lang + ".js' for " + component + " ...")
 | 
						|
			var targetFile = filepath.Clean(Tea.Root + "/../../" + component + "/web/public/js/langs/" + lang + ".js")
 | 
						|
			var targetDir = filepath.Dir(targetFile)
 | 
						|
			dirStat, _ := os.Stat(targetDir)
 | 
						|
			if dirStat != nil {
 | 
						|
				var prefix = ""
 | 
						|
				switch component {
 | 
						|
				case "EdgeAdmin":
 | 
						|
					prefix = "admin_"
 | 
						|
				case "EdgeUser":
 | 
						|
					prefix = "user_"
 | 
						|
				}
 | 
						|
				if len(prefix) == 0 {
 | 
						|
					continue
 | 
						|
				}
 | 
						|
 | 
						|
				var filteredMessages = map[langs.MessageCode]string{}
 | 
						|
				for code, value := range messageLang.GetAll() {
 | 
						|
					if strings.HasPrefix(code.String(), prefix) && strings.Contains(code.String(), "@ui_") /** must contains 'ui' **/ {
 | 
						|
						filteredMessages[code] = value
 | 
						|
					}
 | 
						|
				}
 | 
						|
 | 
						|
				messageMapJSON, jsonErr := json.Marshal(filteredMessages)
 | 
						|
				if jsonErr != nil {
 | 
						|
					fmt.Println("[ERROR]marshal message map failed: " + jsonErr.Error())
 | 
						|
					return
 | 
						|
				}
 | 
						|
				err = os.WriteFile(targetFile, []byte(`// generated by 'langs generate'
 | 
						|
window.LANG_MESSAGES = `+string(messageMapJSON)+";\n"), 0666)
 | 
						|
				if err != nil {
 | 
						|
					fmt.Println("[ERROR]write file '" + targetFile + "' failed: " + err.Error())
 | 
						|
					return
 | 
						|
				}
 | 
						|
 | 
						|
				// base.js
 | 
						|
				if lang == "zh-cn" {
 | 
						|
					var baseJSFile = filepath.Dir(targetFile) + "/base.js"
 | 
						|
					err = os.WriteFile(baseJSFile, []byte(`// generated by 'langs generate'
 | 
						|
window.LANG_MESSAGES_BASE = `+string(messageMapJSON)+";\n"), 0666)
 | 
						|
					if err != nil {
 | 
						|
						fmt.Println("[ERROR]write file '" + baseJSFile + "' failed: " + err.Error())
 | 
						|
						return
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	fmt.Println("success")
 | 
						|
}
 | 
						|
 | 
						|
func upperWords(s string) string {
 | 
						|
	var pieces = strings.Split(s, "@")
 | 
						|
	for pieceIndex, piece := range pieces {
 | 
						|
		var words = strings.Split(piece, "_")
 | 
						|
		for index, word := range words {
 | 
						|
			words[index] = upperWord(word)
 | 
						|
		}
 | 
						|
		pieces[pieceIndex] = strings.Join(words, "")
 | 
						|
	}
 | 
						|
	return strings.Join(pieces, "_")
 | 
						|
}
 | 
						|
 | 
						|
func upperWord(word string) string {
 | 
						|
	var l = len(word)
 | 
						|
	if l == 0 {
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
 | 
						|
	if l == 1 {
 | 
						|
		return strings.ToUpper(word)
 | 
						|
	}
 | 
						|
 | 
						|
	// special words
 | 
						|
	switch word {
 | 
						|
	case "api", "http", "https", "tcp", "tls", "ssl", "udp", "ip", "dns", "ns",
 | 
						|
		"waf", "acme", "ssh", "toa", "http2", "http3", "uam", "cc",
 | 
						|
		"db", "isp", "sni", "ui", "soa", "ocsp", "en", "zh", "ad", "tsig",
 | 
						|
		"rpc", "dao":
 | 
						|
		return strings.ToUpper(word)
 | 
						|
	case "ipv6":
 | 
						|
		return "IPv6"
 | 
						|
	case "ddos":
 | 
						|
		return "DDoS"
 | 
						|
	case "webp":
 | 
						|
		return "WebP"
 | 
						|
	case "doh":
 | 
						|
		return "DoH"
 | 
						|
	}
 | 
						|
 | 
						|
	return strings.ToUpper(word[:1]) + word[1:]
 | 
						|
}
 | 
						|
 | 
						|
func runSearch() {
 | 
						|
	if len(os.Args) < 3 {
 | 
						|
		fmt.Println("Usage: langs search DIR")
 | 
						|
		return
 | 
						|
	}
 | 
						|
	var dir = os.Args[2]
 | 
						|
	stat, err := os.Stat(dir)
 | 
						|
	if err != nil {
 | 
						|
		fmt.Println("[ERROR]could not find dir '" + dir + "': " + err.Error())
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if !stat.IsDir() {
 | 
						|
		fmt.Println("[ERROR]could not find dir '" + dir + "'")
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	fmt.Println("searching '" + dir + "' ...")
 | 
						|
 | 
						|
	var ext = ".go"
 | 
						|
 | 
						|
	var resultFiles = []string{}
 | 
						|
	for _, pattern := range []string{
 | 
						|
		"*" + ext,
 | 
						|
		strings.Repeat("*/", 1) + "*" + ext,
 | 
						|
		strings.Repeat("*/", 2) + "*" + ext,
 | 
						|
		strings.Repeat("*/", 3) + "*" + ext,
 | 
						|
		strings.Repeat("*/", 4) + "*" + ext,
 | 
						|
		strings.Repeat("*/", 5) + "*" + ext,
 | 
						|
		strings.Repeat("*/", 6) + "*" + ext,
 | 
						|
		strings.Repeat("*/", 7) + "*" + ext,
 | 
						|
		strings.Repeat("*/", 8) + "*" + ext,
 | 
						|
		strings.Repeat("*/", 9) + "*" + ext,
 | 
						|
		strings.Repeat("*/", 10) + "*" + ext,
 | 
						|
	} {
 | 
						|
		goFiles, err := filepath.Glob(dir + "/" + pattern)
 | 
						|
		if err != nil {
 | 
						|
			fmt.Println("[ERROR]search error: " + err.Error())
 | 
						|
			return
 | 
						|
		}
 | 
						|
		resultFiles = append(resultFiles, goFiles...)
 | 
						|
	}
 | 
						|
 | 
						|
	if len(resultFiles) == 0 {
 | 
						|
		fmt.Println("no files found in the dir")
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		fmt.Println("[ERROR]search dir '" + dir + "' failed: " + err.Error())
 | 
						|
		return
 | 
						|
	}
 | 
						|
	var hansRegexp = regexp.MustCompile(`\p{Han}+`)
 | 
						|
	var countMatches = 0
 | 
						|
	for _, goFile := range resultFiles {
 | 
						|
		if strings.HasSuffix(goFile, "_test.go") ||
 | 
						|
			strings.HasSuffix(goFile, "_plus_test.go") ||
 | 
						|
			strings.Contains(goFile, "/messages/messages_") {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		data, err := os.ReadFile(goFile)
 | 
						|
		if err != nil {
 | 
						|
			fmt.Println("[ERROR]read file '" + goFile + "' failed: " + err.Error())
 | 
						|
			return
 | 
						|
		}
 | 
						|
		var matches = hansRegexp.FindAllSubmatchIndex(data, -1)
 | 
						|
		if len(matches) > 0 {
 | 
						|
			for _, match := range matches {
 | 
						|
				// ignore comment
 | 
						|
				switch ext {
 | 
						|
				case ".go":
 | 
						|
					if checkIsInGoComment(data, match[0]) {
 | 
						|
						continue
 | 
						|
					}
 | 
						|
				}
 | 
						|
 | 
						|
				countMatches++
 | 
						|
 | 
						|
				fmt.Printf("%s %s\n", goFile+":"+types.String(bytes.Count(data[:match[0]], []byte{'\n'})+1), string(data[match[0]:match[1]]))
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	fmt.Println(countMatches, "matches")
 | 
						|
}
 | 
						|
 | 
						|
func checkIsInGoComment(data []byte, start int) bool {
 | 
						|
	if start <= 1 {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	for {
 | 
						|
		start--
 | 
						|
		if start <= 1 || data[start] == '\n' {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
 | 
						|
		// 'SPACE //'
 | 
						|
		if data[start] == '/' && data[start-1] == '/' {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
 | 
						|
		// '/** SOMETHING **/'
 | 
						|
		if data[start] == '*' && data[start-1] == '/' {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 |