From 284febf5e4a58034a9e625fab62cfb5b57e0c391 Mon Sep 17 00:00:00 2001 From: daimonji <34276351+dmji@users.noreply.github.com> Date: Wed, 1 Jan 2025 22:45:50 +0000 Subject: [PATCH 1/3] feat: added resolving for consts defined in other files of package --- goi18n/extract_command.go | 97 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 6 deletions(-) diff --git a/goi18n/extract_command.go b/goi18n/extract_command.go index ff2ce5de..de9eaa8b 100644 --- a/goi18n/extract_command.go +++ b/goi18n/extract_command.go @@ -66,10 +66,41 @@ func (ec *extractCommand) parse(args []string) error { return nil } +func resolveMessageFieldWithConsts(f *string, consts []*constObj) { + s := strings.Split(*f, unresolvedConstIdentifier) + if len(s) != 2 { + return + } + + name, pkg := s[0], s[1] + for _, c := range consts { + if c.name == name && c.packageName == pkg { + *f = c.value + return + } + } + *f = name +} + +func resolveMessageWithConsts(m *i18n.Message, consts []*constObj) { + resolveMessageFieldWithConsts(&m.ID, consts) + resolveMessageFieldWithConsts(&m.Hash, consts) + resolveMessageFieldWithConsts(&m.Description, consts) + resolveMessageFieldWithConsts(&m.LeftDelim, consts) + resolveMessageFieldWithConsts(&m.RightDelim, consts) + resolveMessageFieldWithConsts(&m.Zero, consts) + resolveMessageFieldWithConsts(&m.One, consts) + resolveMessageFieldWithConsts(&m.Two, consts) + resolveMessageFieldWithConsts(&m.Few, consts) + resolveMessageFieldWithConsts(&m.Many, consts) + resolveMessageFieldWithConsts(&m.Other, consts) +} + func (ec *extractCommand) execute() error { if len(ec.paths) == 0 { ec.paths = []string{"."} } + consts := []*constObj{} messages := []*i18n.Message{} for _, path := range ec.paths { if err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error { @@ -92,18 +123,24 @@ func (ec *extractCommand) execute() error { if err != nil { return err } - msgs, err := extractMessages(buf) + msgs, cnsts, err := extractMessages(buf) if err != nil { return err } messages = append(messages, msgs...) + consts = append(consts, cnsts...) return nil }); err != nil { return err } } + messageTemplates := map[string]*i18n.MessageTemplate{} for _, m := range messages { + // resolve message consts + resolveMessageWithConsts(m, consts) + + // create template if mt := i18n.NewMessageTemplate(m); mt != nil { messageTemplates[m.ID] = mt } @@ -116,24 +153,37 @@ func (ec *extractCommand) execute() error { } // extractMessages extracts messages from the bytes of a Go source file. -func extractMessages(buf []byte) ([]*i18n.Message, error) { +func extractMessages(buf []byte) ([]*i18n.Message, []*constObj, error) { fset := token.NewFileSet() file, err := parser.ParseFile(fset, "", buf, parser.AllErrors) if err != nil { - return nil, err + return nil, nil, err } extractor := newExtractor(file) ast.Walk(extractor, file) - return extractor.messages, nil + return extractor.messages, extractor.consts, nil } func newExtractor(file *ast.File) *extractor { - return &extractor{i18nPackageName: i18nPackageName(file)} + return &extractor{ + i18nPackageName: i18nPackageName(file), + packageName: file.Name.Name, + } +} + +const unresolvedConstIdentifier = ":::unresolved:::" + +type constObj struct { + name string + value string + packageName string } type extractor struct { i18nPackageName string + packageName string messages []*i18n.Message + consts []*constObj } func (e *extractor) Visit(node ast.Node) ast.Visitor { @@ -141,7 +191,37 @@ func (e *extractor) Visit(node ast.Node) ast.Visitor { return e } +func (e *extractor) extractConsts(node ast.GenDecl) { + if node.Tok != token.CONST { + return + } + + for _, s := range node.Specs { + if vs, ok := s.(*ast.ValueSpec); ok { + for i, n := range vs.Names { + + if bl, ok := vs.Values[i].(*ast.BasicLit); ok { + v, err := strconv.Unquote(bl.Value) + if err != nil { + continue + } + e.consts = append(e.consts, &constObj{ + name: n.Name, + value: v, + packageName: e.packageName, + }) + } + } + } + } +} + func (e *extractor) extractMessages(node ast.Node) { + // Collect consts from all files to resolve nil objects recived after extract message + if gd, ok := node.(*ast.GenDecl); ok { + e.extractConsts(*gd) + } + cl, ok := node.(*ast.CompositeLit) if !ok { return @@ -221,6 +301,11 @@ func (e *extractor) extractMessage(cl *ast.CompositeLit) { } v, ok := extractStringLiteral(kve.Value) if !ok { + if len(v) > 0 { + // v might be not empty if const cannot be r (placed other file) so we should try to resolve it later + data[key.Name] = v + unresolvedConstIdentifier + e.packageName + } + continue } data[key.Name] = v @@ -260,7 +345,7 @@ func extractStringLiteral(expr ast.Expr) (string, bool) { return x + y, true case *ast.Ident: if v.Obj == nil { - return "", false + return v.Name, false } switch z := v.Obj.Decl.(type) { case *ast.ValueSpec: From 59168bba2e85bd3d137bc81b0e9caf964ef48250 Mon Sep 17 00:00:00 2001 From: daimonji <34276351+dmji@users.noreply.github.com> Date: Wed, 1 Jan 2025 23:06:18 +0000 Subject: [PATCH 2/3] fix: filter iota --- goi18n/extract_command.go | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/goi18n/extract_command.go b/goi18n/extract_command.go index de9eaa8b..74a6fb29 100644 --- a/goi18n/extract_command.go +++ b/goi18n/extract_command.go @@ -199,18 +199,27 @@ func (e *extractor) extractConsts(node ast.GenDecl) { for _, s := range node.Specs { if vs, ok := s.(*ast.ValueSpec); ok { for i, n := range vs.Names { + if len(vs.Values) <= i { + break + } + bl, ok := vs.Values[i].(*ast.BasicLit) + if !ok { + break + } - if bl, ok := vs.Values[i].(*ast.BasicLit); ok { - v, err := strconv.Unquote(bl.Value) - if err != nil { - continue - } - e.consts = append(e.consts, &constObj{ - name: n.Name, - value: v, - packageName: e.packageName, - }) + v, err := strconv.Unquote(bl.Value) + if err != nil { + continue } + if v == "iota" { + break + } + e.consts = append(e.consts, &constObj{ + name: n.Name, + value: v, + packageName: e.packageName, + }) + } } } From 7d0eed858cca456a30be00c54f24c37693ae7b44 Mon Sep 17 00:00:00 2001 From: daimonji <34276351+dmji@users.noreply.github.com> Date: Wed, 1 Jan 2025 23:06:18 +0000 Subject: [PATCH 3/3] test: added testdata for extracting consts from different file --- goi18n/testdata/ids.go | 9 +++++++ goi18n/testdata/some_enum.go | 49 ++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 goi18n/testdata/ids.go create mode 100644 goi18n/testdata/some_enum.go diff --git a/goi18n/testdata/ids.go b/goi18n/testdata/ids.go new file mode 100644 index 00000000..734a37f2 --- /dev/null +++ b/goi18n/testdata/ids.go @@ -0,0 +1,9 @@ +package main + +const ( + ID1 = "HelloPersonConst" + ID2 = "MyUnreadEmailsConst" + ID3 = "PersonUnreadEmailsConst" + + MSG_HELLO = "Hello {{.Name}}" +) diff --git a/goi18n/testdata/some_enum.go b/goi18n/testdata/some_enum.go new file mode 100644 index 00000000..bd8e22db --- /dev/null +++ b/goi18n/testdata/some_enum.go @@ -0,0 +1,49 @@ +package main + +import "github.com/nicksnyder/go-i18n/v2/i18n" + +type MessagList struct { + HelloPerson string + MyUnreadEmails string + PersonUnreadEmails string +} + +func FillLocalizer(localizer *i18n.Localizer, unreadEmailCount, name string) *MessagList { + ml := &MessagList{} + + ml.HelloPerson = localizer.MustLocalize(&i18n.LocalizeConfig{ + DefaultMessage: &i18n.Message{ + ID: ID1, + Other: MSG_HELLO, + }, + TemplateData: map[string]string{ + "Name": name, + }, + }) + + ml.MyUnreadEmails = localizer.MustLocalize(&i18n.LocalizeConfig{ + DefaultMessage: &i18n.Message{ + ID: ID2, + Description: "The number of unread emails I have", + One: "I have {{.PluralCount}} unread email.", + Other: "I have {{.PluralCount}} unread emails.", + }, + PluralCount: unreadEmailCount, + }) + + ml.PersonUnreadEmails = localizer.MustLocalize(&i18n.LocalizeConfig{ + DefaultMessage: &i18n.Message{ + ID: ID3, + Description: "The number of unread emails a person has", + One: "{{.Name}} has {{.UnreadEmailCount}} unread email.", + Other: "{{.Name}} has {{.UnreadEmailCount}} unread emails.", + }, + PluralCount: unreadEmailCount, + TemplateData: map[string]interface{}{ + "Name": name, + "UnreadEmailCount": unreadEmailCount, + }, + }) + + return ml +}