mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
tools/mmgotool: move into monorepo (#24010)
This commit is contained in:
parent
8ee38ee644
commit
38fd8cd6aa
@ -332,11 +332,11 @@ app-layers: ## Extract interface from App struct
|
||||
$(GO) run ./channels/app/layer_generators -in ./channels/app/app_iface.go -out ./channels/app/opentracing/opentracing_layer.go -template ./channels/app/layer_generators/opentracing_layer.go.tmpl
|
||||
|
||||
i18n-extract: ## Extract strings for translation from the source code
|
||||
$(GO) install github.com/mattermost/mattermost-utilities/mmgotool@mono-repo
|
||||
cd ../tools/mmgotool && $(GO) install .
|
||||
$(GOBIN)/mmgotool i18n extract --portal-dir=""
|
||||
|
||||
i18n-check: ## Exit on empty translation strings and translation source strings
|
||||
$(GO) install github.com/mattermost/mattermost-utilities/mmgotool@mono-repo
|
||||
cd ../tools/mmgotool && $(GO) install .
|
||||
$(GOBIN)/mmgotool i18n clean-empty --portal-dir="" --check
|
||||
$(GOBIN)/mmgotool i18n check-empty-src --portal-dir=""
|
||||
|
||||
|
22
tools/README.md
Normal file
22
tools/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Tools
|
||||
|
||||
This directory aims to provide a set of tools that simplify and enhance various development tasks. This README file serves as a guide to help you understand the directory, features of these tools, and how to get started using it. This is a collection of utilities and scripts designed to streamline common development tasks for Mattermost. These tools aim to help automate repetitive tasks and improve productivity.
|
||||
|
||||
## Included tools
|
||||
|
||||
* **mmgotool**: is a CLI to help with i18n related checks for the mattermost/server development.
|
||||
|
||||
## Installation & Usage
|
||||
|
||||
### mmgotool
|
||||
|
||||
To install `mmgotool`, simply run the following command: `go install github.com/mattermost/mattermost/tools/mmgotool`
|
||||
|
||||
Make sure you have the necessary prerequisites such as [Go](https://go.dev/) compiler.
|
||||
|
||||
`mmgotool i18n` has following subcommands described below:
|
||||
|
||||
* `check`: Check translations
|
||||
* `check-empty-src`: Check for empty translation source strings
|
||||
* `clean-empty`: Clean empty translations
|
||||
* `extract`: Extract translations
|
734
tools/mmgotool/commands/i18n.go
Normal file
734
tools/mmgotool/commands/i18n.go
Normal file
@ -0,0 +1,734 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const enterpriseKeyPrefix = "ent."
|
||||
const untranslatedKey = "<untranslated>"
|
||||
|
||||
type Translation struct {
|
||||
Id string `json:"id"`
|
||||
Translation interface{} `json:"translation"`
|
||||
}
|
||||
|
||||
type Item struct {
|
||||
ID string `json:"id"`
|
||||
Translation json.RawMessage `json:"translation"`
|
||||
}
|
||||
|
||||
var I18nCmd = &cobra.Command{
|
||||
Use: "i18n",
|
||||
Short: "Management of Mattermost translations",
|
||||
}
|
||||
|
||||
var ExtractCmd = &cobra.Command{
|
||||
Use: "extract",
|
||||
Short: "Extract translations",
|
||||
Long: "Extract translations from the source code and put them into the i18n/en.json file",
|
||||
Example: " i18n extract",
|
||||
RunE: extractCmdF,
|
||||
}
|
||||
|
||||
var CheckCmd = &cobra.Command{
|
||||
Use: "check",
|
||||
Short: "Check translations",
|
||||
Long: "Check translations existing in the source code and compare it to the i18n/en.json file",
|
||||
Example: " i18n check",
|
||||
RunE: checkCmdF,
|
||||
}
|
||||
|
||||
var CheckEmptySrcCmd = &cobra.Command{
|
||||
Use: "check-empty-src",
|
||||
Short: "Check for empty translation source strings",
|
||||
Long: "Check the en.json file for empty translation source strings",
|
||||
Example: " i18n check-empty-src",
|
||||
RunE: checkEmptySrcCmdF,
|
||||
}
|
||||
|
||||
var CleanEmptyCmd = &cobra.Command{
|
||||
Use: "clean-empty",
|
||||
Short: "Clean empty translations",
|
||||
Long: "Clean empty translations in translation files other than i18n/en.json base file",
|
||||
Example: " i18n clean-empty",
|
||||
RunE: cleanEmptyCmdF,
|
||||
}
|
||||
|
||||
func init() {
|
||||
ExtractCmd.Flags().Bool("skip-dynamic", false, "Whether to skip dynamically added translations")
|
||||
ExtractCmd.Flags().String("portal-dir", "../customer-web-server", "Path to folder with the Mattermost Customer Portal source code")
|
||||
ExtractCmd.Flags().String("enterprise-dir", "../../enterprise", "Path to folder with the Mattermost enterprise source code")
|
||||
ExtractCmd.Flags().String("server-dir", "./", "Path to folder with the Mattermost server source code")
|
||||
ExtractCmd.Flags().String("model-dir", "../model", "Path to folder with the Mattermost model package source code")
|
||||
ExtractCmd.Flags().String("plugin-dir", "../plugin", "Path to folder with the Mattermost plugin package source code")
|
||||
ExtractCmd.Flags().Bool("contributor", false, "Allows contributors safely extract translations from source code without removing enterprise messages keys")
|
||||
|
||||
CheckCmd.Flags().Bool("skip-dynamic", false, "Whether to skip dynamically added translations")
|
||||
CheckCmd.Flags().String("portal-dir", "../customer-web-server", "Path to folder with the Mattermost Customer Portal source code")
|
||||
CheckCmd.Flags().String("enterprise-dir", "../../enterprise", "Path to folder with the Mattermost enterprise source code")
|
||||
CheckCmd.Flags().String("server-dir", "./", "Path to folder with the Mattermost server source code")
|
||||
CheckCmd.Flags().String("model-dir", "../model", "Path to folder with the Mattermost model package source code")
|
||||
CheckCmd.Flags().String("plugin-dir", "../plugin", "Path to folder with the Mattermost plugin package source code")
|
||||
|
||||
CheckEmptySrcCmd.Flags().String("portal-dir", "../customer-web-server", "Path to folder with the Mattermost Customer Portal source code")
|
||||
CheckEmptySrcCmd.Flags().String("enterprise-dir", "../../enterprise", "Path to folder with the Mattermost enterprise source code")
|
||||
CheckEmptySrcCmd.Flags().String("server-dir", "./", "Path to folder with the Mattermost server source code")
|
||||
|
||||
CleanEmptyCmd.Flags().Bool("dry-run", false, "Run without applying changes")
|
||||
CleanEmptyCmd.Flags().Bool("check", false, "Throw exit code on empty translation strings")
|
||||
CleanEmptyCmd.Flags().String("portal-dir", "../customer-web-server", "Path to folder with the Mattermost Customer Portal source code")
|
||||
CleanEmptyCmd.Flags().String("enterprise-dir", "../../enterprise", "Path to folder with the Mattermost enterprise source code")
|
||||
CleanEmptyCmd.Flags().String("server-dir", "./", "Path to folder with the Mattermost server source code")
|
||||
|
||||
I18nCmd.AddCommand(
|
||||
ExtractCmd,
|
||||
CheckCmd,
|
||||
CheckEmptySrcCmd,
|
||||
CleanEmptyCmd,
|
||||
)
|
||||
RootCmd.AddCommand(I18nCmd)
|
||||
}
|
||||
|
||||
func getBaseFileSrcStrings(mattermostDir string) ([]Translation, error) {
|
||||
jsonFile, err := ioutil.ReadFile(path.Join(mattermostDir, "i18n", "en.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var translations []Translation
|
||||
_ = json.Unmarshal(jsonFile, &translations)
|
||||
return translations, nil
|
||||
}
|
||||
|
||||
func extractSrcStrings(enterpriseDir, mattermostDir, modelDir, pluginDir, portalDir string) map[string]bool {
|
||||
i18nStrings := map[string]bool{}
|
||||
walkFunc := func(p string, info os.FileInfo, err error) error {
|
||||
if strings.HasPrefix(p, path.Join(mattermostDir, "vendor")) {
|
||||
return nil
|
||||
}
|
||||
return extractFromPath(p, info, err, i18nStrings)
|
||||
}
|
||||
if portalDir != "" {
|
||||
_ = filepath.Walk(portalDir, walkFunc)
|
||||
} else {
|
||||
_ = filepath.Walk(mattermostDir, walkFunc)
|
||||
_ = filepath.Walk(enterpriseDir, walkFunc)
|
||||
_ = filepath.Walk(modelDir, walkFunc)
|
||||
_ = filepath.Walk(pluginDir, walkFunc)
|
||||
}
|
||||
return i18nStrings
|
||||
}
|
||||
|
||||
func extractCmdF(command *cobra.Command, args []string) error {
|
||||
skipDynamic, err := command.Flags().GetBool("skip-dynamic")
|
||||
if err != nil {
|
||||
return errors.New("invalid skip-dynamic parameter")
|
||||
}
|
||||
enterpriseDir, err := command.Flags().GetString("enterprise-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid enterprise-dir parameter")
|
||||
}
|
||||
mattermostDir, err := command.Flags().GetString("server-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid server-dir parameter")
|
||||
}
|
||||
contributorMode, err := command.Flags().GetBool("contributor")
|
||||
if err != nil {
|
||||
return errors.New("invalid contributor parameter")
|
||||
}
|
||||
portalDir, err := command.Flags().GetString("portal-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid portal-dir parameter")
|
||||
}
|
||||
modelDir, err := command.Flags().GetString("model-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid model-dir parameter")
|
||||
}
|
||||
pluginDir, err := command.Flags().GetString("plugin-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid plugin-dir parameter")
|
||||
}
|
||||
translationDir := mattermostDir
|
||||
if portalDir != "" {
|
||||
if enterpriseDir != "" || mattermostDir != "" {
|
||||
return errors.New("please specify EITHER portal-dir or enterprise-dir/server-dir")
|
||||
}
|
||||
skipDynamic = true // dynamics are not needed for portal
|
||||
translationDir = portalDir
|
||||
}
|
||||
i18nStrings := extractSrcStrings(enterpriseDir, mattermostDir, modelDir, pluginDir, portalDir)
|
||||
if !skipDynamic {
|
||||
addDynamicallyGeneratedStrings(i18nStrings)
|
||||
}
|
||||
// Delete any untranslated keys
|
||||
delete(i18nStrings, untranslatedKey)
|
||||
var i18nStringsList []string
|
||||
for id := range i18nStrings {
|
||||
i18nStringsList = append(i18nStringsList, id)
|
||||
}
|
||||
sort.Strings(i18nStringsList)
|
||||
|
||||
sourceStrings, err := getBaseFileSrcStrings(translationDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var baseFileList []string
|
||||
idx := map[string]bool{}
|
||||
resultMap := map[string]Translation{}
|
||||
for _, t := range sourceStrings {
|
||||
idx[t.Id] = true
|
||||
baseFileList = append(baseFileList, t.Id)
|
||||
resultMap[t.Id] = t
|
||||
}
|
||||
sort.Strings(baseFileList)
|
||||
|
||||
for _, translationKey := range i18nStringsList {
|
||||
if _, hasKey := idx[translationKey]; !hasKey {
|
||||
resultMap[translationKey] = Translation{Id: translationKey, Translation: ""}
|
||||
}
|
||||
}
|
||||
|
||||
for _, translationKey := range baseFileList {
|
||||
if _, hasKey := i18nStrings[translationKey]; !hasKey {
|
||||
if contributorMode && strings.HasPrefix(translationKey, enterpriseKeyPrefix) {
|
||||
continue
|
||||
}
|
||||
delete(resultMap, translationKey)
|
||||
}
|
||||
}
|
||||
|
||||
var result []Translation
|
||||
for _, t := range resultMap {
|
||||
result = append(result, t)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool { return result[i].Id < result[j].Id })
|
||||
|
||||
f, err := os.Create(path.Join(mattermostDir, "i18n", "en.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
encoder := json.NewEncoder(f)
|
||||
encoder.SetIndent("", " ")
|
||||
encoder.SetEscapeHTML(false)
|
||||
err = encoder.Encode(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkCmdF(command *cobra.Command, args []string) error {
|
||||
skipDynamic, err := command.Flags().GetBool("skip-dynamic")
|
||||
if err != nil {
|
||||
return errors.New("invalid skip-dynamic parameter")
|
||||
}
|
||||
enterpriseDir, err := command.Flags().GetString("enterprise-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid enterprise-dir parameter")
|
||||
}
|
||||
mattermostDir, err := command.Flags().GetString("server-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid server-dir parameter")
|
||||
}
|
||||
portalDir, err := command.Flags().GetString("portal-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid portal-dir parameter")
|
||||
}
|
||||
modelDir, err := command.Flags().GetString("model-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid model-dir parameter")
|
||||
}
|
||||
pluginDir, err := command.Flags().GetString("plugin-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid plugin-dir parameter")
|
||||
}
|
||||
translationDir := mattermostDir
|
||||
if portalDir != "" {
|
||||
if enterpriseDir != "" || mattermostDir != "" {
|
||||
return errors.New("please specify EITHER portal-dir or enterprise-dir/server-dir")
|
||||
}
|
||||
translationDir = portalDir
|
||||
skipDynamic = true // dynamics are not needed for portal
|
||||
}
|
||||
extractedSrcStrings := extractSrcStrings(enterpriseDir, mattermostDir, modelDir, pluginDir, portalDir)
|
||||
if !skipDynamic {
|
||||
addDynamicallyGeneratedStrings(extractedSrcStrings)
|
||||
}
|
||||
// Delete any untranslated keys
|
||||
delete(extractedSrcStrings, untranslatedKey)
|
||||
var extractedList []string
|
||||
for id := range extractedSrcStrings {
|
||||
extractedList = append(extractedList, id)
|
||||
}
|
||||
sort.Strings(extractedList)
|
||||
|
||||
srcStrings, err := getBaseFileSrcStrings(translationDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var baseFileList []string
|
||||
idx := map[string]bool{}
|
||||
for _, t := range srcStrings {
|
||||
idx[t.Id] = true
|
||||
baseFileList = append(baseFileList, t.Id)
|
||||
}
|
||||
sort.Strings(baseFileList)
|
||||
|
||||
changed := false
|
||||
for _, translationKey := range extractedList {
|
||||
if _, hasKey := idx[translationKey]; !hasKey {
|
||||
fmt.Println("Added:", translationKey)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, translationKey := range baseFileList {
|
||||
if _, hasKey := extractedSrcStrings[translationKey]; !hasKey {
|
||||
fmt.Println("Removed:", translationKey)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
command.SilenceUsage = true
|
||||
return errors.New("translation source strings file out of date")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addDynamicallyGeneratedStrings(i18nStrings map[string]bool) {
|
||||
i18nStrings["model.user.is_valid.pwd.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_lowercase.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_lowercase_number.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_lowercase_number_symbol.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_lowercase_symbol.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_lowercase_uppercase.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_lowercase_uppercase_number.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_lowercase_uppercase_number_symbol.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_lowercase_uppercase_symbol.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_number.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_number_symbol.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_symbol.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_uppercase.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_uppercase_number.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_uppercase_number_symbol.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.pwd_uppercase_symbol.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.id.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.create_at.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.update_at.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.username.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.email.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.nickname.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.position.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.first_name.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.last_name.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.auth_data.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.auth_data_type.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.auth_data_pwd.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.password_limit.app_error"] = true
|
||||
i18nStrings["model.user.is_valid.locale.app_error"] = true
|
||||
i18nStrings["January"] = true
|
||||
i18nStrings["February"] = true
|
||||
i18nStrings["March"] = true
|
||||
i18nStrings["April"] = true
|
||||
i18nStrings["May"] = true
|
||||
i18nStrings["June"] = true
|
||||
i18nStrings["July"] = true
|
||||
i18nStrings["August"] = true
|
||||
i18nStrings["September"] = true
|
||||
i18nStrings["October"] = true
|
||||
i18nStrings["November"] = true
|
||||
i18nStrings["December"] = true
|
||||
}
|
||||
|
||||
func extractByFuncName(name string, args []ast.Expr) *string {
|
||||
if name == "T" {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
key, ok := args[0].(*ast.BasicLit)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &key.Value
|
||||
} else if name == "NewAppError" {
|
||||
if len(args) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
key, ok := args[1].(*ast.BasicLit)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &key.Value
|
||||
} else if name == "newAppError" {
|
||||
if len(args) < 1 {
|
||||
return nil
|
||||
}
|
||||
key, ok := args[0].(*ast.BasicLit)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &key.Value
|
||||
} else if name == "NewUserFacingError" {
|
||||
if len(args) < 1 {
|
||||
return nil
|
||||
}
|
||||
key, ok := args[0].(*ast.BasicLit)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &key.Value
|
||||
} else if name == "translateFunc" {
|
||||
if len(args) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
key, ok := args[0].(*ast.BasicLit)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &key.Value
|
||||
} else if name == "TranslateAsHTML" || name == "TranslateAsHtml" {
|
||||
if len(args) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
key, ok := args[1].(*ast.BasicLit)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &key.Value
|
||||
} else if name == "userLocale" {
|
||||
if len(args) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
key, ok := args[0].(*ast.BasicLit)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &key.Value
|
||||
} else if name == "localT" {
|
||||
if len(args) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
key, ok := args[0].(*ast.BasicLit)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &key.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractForConstants(name string, valueNode ast.Expr) *string {
|
||||
validConstants := map[string]bool{
|
||||
"MISSING_CHANNEL_ERROR": true,
|
||||
"MISSING_CHANNEL_MEMBER_ERROR": true,
|
||||
"CHANNEL_EXISTS_ERROR": true,
|
||||
"MISSING_STATUS_ERROR": true,
|
||||
"TEAM_MEMBER_EXISTS_ERROR": true,
|
||||
"MISSING_AUTH_ACCOUNT_ERROR": true,
|
||||
"MISSING_ACCOUNT_ERROR": true,
|
||||
"EXPIRED_LICENSE_ERROR": true,
|
||||
"INVALID_LICENSE_ERROR": true,
|
||||
"MissingChannelError": true,
|
||||
"MissingChannelMemberError": true,
|
||||
"ChannelExistsError": true,
|
||||
"MissingStatusError": true,
|
||||
"TeamMemberExistsError": true,
|
||||
"MissingAuthAccountError": true,
|
||||
"MissingAccountError": true,
|
||||
"ExpiredLicenseError": true,
|
||||
"InvalidLicenseError": true,
|
||||
"NoTranslation": true,
|
||||
}
|
||||
|
||||
if _, ok := validConstants[name]; !ok {
|
||||
return nil
|
||||
}
|
||||
value, ok := valueNode.(*ast.BasicLit)
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &value.Value
|
||||
|
||||
}
|
||||
|
||||
func extractFromPath(path string, info os.FileInfo, err error, i18nStrings map[string]bool) error {
|
||||
if strings.HasSuffix(path, "model/client4.go") {
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".go") {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(path, ".git/") || strings.HasPrefix(path, ".git/") {
|
||||
return nil
|
||||
}
|
||||
|
||||
src, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "", src, 0)
|
||||
if err != nil {
|
||||
fmt.Printf("error parsing source: %s\n", path)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
var id *string = nil
|
||||
|
||||
switch expr := n.(type) {
|
||||
case *ast.CallExpr:
|
||||
switch fun := expr.Fun.(type) {
|
||||
case *ast.SelectorExpr:
|
||||
id = extractByFuncName(fun.Sel.Name, expr.Args)
|
||||
if id == nil {
|
||||
return true
|
||||
}
|
||||
break
|
||||
case *ast.Ident:
|
||||
id = extractByFuncName(fun.Name, expr.Args)
|
||||
break
|
||||
default:
|
||||
return true
|
||||
}
|
||||
break
|
||||
case *ast.GenDecl:
|
||||
if expr.Tok == token.CONST {
|
||||
for _, spec := range expr.Specs {
|
||||
valueSpec, ok := spec.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if len(valueSpec.Names) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(valueSpec.Values) == 0 {
|
||||
continue
|
||||
}
|
||||
id = extractForConstants(valueSpec.Names[0].Name, valueSpec.Values[0])
|
||||
if id == nil {
|
||||
continue
|
||||
}
|
||||
i18nStrings[strings.Trim(*id, "\"")] = true
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
||||
if id != nil {
|
||||
i18nStrings[strings.Trim(*id, "\"")] = true
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkEmptySrcCmdF(command *cobra.Command, args []string) error {
|
||||
enterpriseDir, err := command.Flags().GetString("enterprise-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid enterprise-dir parameter")
|
||||
}
|
||||
mattermostDir, err := command.Flags().GetString("server-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid server-dir parameter")
|
||||
}
|
||||
portalDir, err := command.Flags().GetString("portal-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid portal-dir parameter")
|
||||
}
|
||||
translationDir := path.Join(mattermostDir, "i18n")
|
||||
if portalDir != "" {
|
||||
if enterpriseDir != "" || mattermostDir != "" {
|
||||
return errors.New("please specify EITHER portal-dir or enterprise-dir/server-dir")
|
||||
}
|
||||
translationDir = portalDir
|
||||
}
|
||||
srcJSON, err := ioutil.ReadFile(path.Join(translationDir, "en.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var items []Item
|
||||
if err = json.Unmarshal(srcJSON, &items); err != nil {
|
||||
return err
|
||||
}
|
||||
err = countEmptyItems(items)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func countEmptyItems(items []Item) error {
|
||||
hasError := false
|
||||
for _, t := range items {
|
||||
str := string(t.Translation)
|
||||
if !strings.HasPrefix(str, "\"") {
|
||||
continue
|
||||
}
|
||||
unquoted, err := strconv.Unquote(str)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unquoting translation for %s, %v", t.ID, err)
|
||||
}
|
||||
if strings.TrimSpace(unquoted) == "" {
|
||||
log.Printf("Empty translation for %s. Please fix it.\n", t.ID)
|
||||
hasError = true
|
||||
}
|
||||
}
|
||||
if hasError {
|
||||
return errors.New("empty translations found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanEmptyCmdF(command *cobra.Command, args []string) error {
|
||||
dryRun, err := command.Flags().GetBool("dry-run")
|
||||
if err != nil {
|
||||
return errors.New("invalid dry-run parameter")
|
||||
}
|
||||
check, err := command.Flags().GetBool("check")
|
||||
if err != nil {
|
||||
return errors.New("invalid check parameter")
|
||||
}
|
||||
enterpriseDir, err := command.Flags().GetString("enterprise-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid enterprise-dir parameter")
|
||||
}
|
||||
mattermostDir, err := command.Flags().GetString("server-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid server-dir parameter")
|
||||
}
|
||||
portalDir, err := command.Flags().GetString("portal-dir")
|
||||
if err != nil {
|
||||
return errors.New("invalid portal-dir parameter")
|
||||
}
|
||||
translationDir := path.Join(mattermostDir, "i18n")
|
||||
if portalDir != "" {
|
||||
if enterpriseDir != "" || mattermostDir != "" {
|
||||
return errors.New("please specify EITHER portal-dir or enterprise-dir/server-dir")
|
||||
}
|
||||
translationDir = portalDir
|
||||
}
|
||||
|
||||
var shippedFiles []string
|
||||
files, err := ioutil.ReadDir(translationDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range files {
|
||||
if !file.IsDir() && filepath.Ext(file.Name()) == ".json" && file.Name() != "en.json" {
|
||||
shippedFiles = append(shippedFiles, file.Name())
|
||||
}
|
||||
}
|
||||
|
||||
results := ""
|
||||
for _, file := range shippedFiles {
|
||||
result, err2 := clean(translationDir, file, dryRun, check)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
results += *result
|
||||
}
|
||||
if results == "" {
|
||||
return nil
|
||||
}
|
||||
fmt.Print("\n" + results)
|
||||
if check {
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func clean(translationDir string, file string, dryRun bool, check bool) (*string, error) {
|
||||
oldJSON, err := ioutil.ReadFile(path.Join(translationDir, file))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var oldList []Item
|
||||
if err = json.Unmarshal(oldJSON, &oldList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newList, count := removeEmptyTranslations(oldList)
|
||||
result := ""
|
||||
if count == 0 {
|
||||
return &result, nil
|
||||
}
|
||||
result = fmt.Sprintf("%v has %v empty translations\n", file, count)
|
||||
if dryRun || check {
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
newJSON, err := JSONMarshal(newList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filename := path.Join(translationDir, file)
|
||||
fileInfo, err := os.Lstat(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = ioutil.WriteFile(filename, newJSON, fileInfo.Mode().Perm()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func removeEmptyTranslations(oldList []Item) ([]Item, int) {
|
||||
var count int
|
||||
var newList []Item
|
||||
for i, t := range oldList {
|
||||
if string(t.Translation) != "\"\"" {
|
||||
newList = append(newList, oldList[i])
|
||||
} else {
|
||||
count++
|
||||
}
|
||||
|
||||
}
|
||||
return newList, count
|
||||
}
|
||||
|
||||
func JSONMarshal(t interface{}) ([]byte, error) {
|
||||
buffer := &bytes.Buffer{}
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetEscapeHTML(false)
|
||||
encoder.SetIndent("", " ")
|
||||
err := encoder.Encode(t)
|
||||
return buffer.Bytes(), err
|
||||
}
|
21
tools/mmgotool/commands/root.go
Normal file
21
tools/mmgotool/commands/root.go
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type Command = cobra.Command
|
||||
|
||||
func Run(args []string) error {
|
||||
RootCmd.SetArgs(args)
|
||||
return RootCmd.Execute()
|
||||
}
|
||||
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "mmgotool",
|
||||
Short: "Mattermost dev utils cli",
|
||||
Long: `Mattermost cli to help in the development process`,
|
||||
}
|
10
tools/mmgotool/go.mod
Normal file
10
tools/mmgotool/go.mod
Normal file
@ -0,0 +1,10 @@
|
||||
module github.com/mattermost/mattermost/tools/mmgotool
|
||||
|
||||
go 1.19
|
||||
|
||||
require github.com/spf13/cobra v1.7.0
|
||||
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
)
|
10
tools/mmgotool/go.sum
Normal file
10
tools/mmgotool/go.sum
Normal file
@ -0,0 +1,10 @@
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
16
tools/mmgotool/main.go
Normal file
16
tools/mmgotool/main.go
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/mattermost/mattermost/tools/mmgotool/commands"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := commands.Run(os.Args[1:]); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user