From 0a921fb4441e475102bf91afcecb1d67c9e4d0f6 Mon Sep 17 00:00:00 2001 From: Lucas Medeiros Date: Thu, 16 Jan 2020 17:53:21 -0300 Subject: [PATCH 1/2] refactor to v2 --- README.md | 2 + v2/README.md | 34 ++++++++++++ v2/examples/multiple_projects.go | 22 ++++++++ v2/examples/polling.go | 32 ++++++++++++ v2/go.mod | 5 ++ v2/go.sum | 2 + v2/loco.go | 84 +++++++++++++++++++++++++++++ v2/project.go | 90 ++++++++++++++++++++++++++++++++ v2/translation.go | 27 ++++++++++ 9 files changed, 298 insertions(+) create mode 100644 v2/README.md create mode 100644 v2/examples/multiple_projects.go create mode 100644 v2/examples/polling.go create mode 100644 v2/go.mod create mode 100644 v2/go.sum create mode 100644 v2/loco.go create mode 100644 v2/project.go create mode 100644 v2/translation.go diff --git a/README.md b/README.md index 066d207..16185e5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A simple lib to fetch translation files from [Localise](https://localise.biz/) and save it in a [go-i18n](https://github.com/nicksnyder/go-i18n) compatible format. +Checkout the [V2](./v2) for multiple projects and automatic assets update. + ## Usage ```golang diff --git a/v2/README.md b/v2/README.md new file mode 100644 index 0000000..4c2e0f8 --- /dev/null +++ b/v2/README.md @@ -0,0 +1,34 @@ +## go-loco v2 + +A simple lib to fetch translation files from [Localise](https://localise.biz/) and save it in a [go-i18n](https://github.com/nicksnyder/go-i18n) compatible format. + +## Usage + +```golang +package main + +import ( + "context" + "fmt" + + loco "github.com/lucasmdrs/go-loco" + "github.com/nicksnyder/go-i18n/i18n" +) + +func main() { + g := Init() + + if err := g.AddProject("MY_LOCALISE_KEY", "./"); err != nil { + panic(err) + } + + g.FetchTranslations(context.TODO()) + + i18n.MustLoadTranslationFile("en-US.json") + i18n.MustLoadTranslationFile("pt-BR.json") + + T, _ := i18n.Tfunc("pt-BR") + + fmt.Println(T("hello")) +} +``` \ No newline at end of file diff --git a/v2/examples/multiple_projects.go b/v2/examples/multiple_projects.go new file mode 100644 index 0000000..9d8621c --- /dev/null +++ b/v2/examples/multiple_projects.go @@ -0,0 +1,22 @@ +package main + +import ( + "context" + "log" + + loco "github.com/lucasmdrs/go-loco/v2" +) + +func main() { + g := loco.Init() + + if err := g.AddProject("KEY_1", "project1"); err != nil { + log.Fatalln(err.Error()) + } + + if err := g.AddProject("KEY_2", "project2"); err != nil { + log.Fatalln(err.Error()) + } + g.FetchTranslations(context.TODO()) + +} diff --git a/v2/examples/polling.go b/v2/examples/polling.go new file mode 100644 index 0000000..b0163d3 --- /dev/null +++ b/v2/examples/polling.go @@ -0,0 +1,32 @@ +package main + +import ( + "log" + + loco "github.com/lucasmdrs/go-loco/v2" +) + +func main() { + g := loco.Init() + + if err := g.AddProject("KEY_1", "project1"); err != nil { + log.Fatalln(err.Error()) + } + + if err := g.AddProject("KEY_2", "project2"); err != nil { + log.Fatalln(err.Error()) + } + + ch, err := g.StartPoller() + if err != nil { + log.Fatalln(err.Error()) + } + + for { + select { + case <-ch: + log.Println("changed!") + } + } + +} diff --git a/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..a408b9a --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,5 @@ +module github.com/lucasmdrs/go-loco/v2 + +go 1.13 + +require github.com/lucasmdrs/ctxpoller v1.0.0 diff --git a/v2/go.sum b/v2/go.sum new file mode 100644 index 0000000..6ffac9b --- /dev/null +++ b/v2/go.sum @@ -0,0 +1,2 @@ +github.com/lucasmdrs/ctxpoller v1.0.0 h1:0/tGTbahfyjMu2FgzoAnaAijPlEGQta6sMyQyzDF4lY= +github.com/lucasmdrs/ctxpoller v1.0.0/go.mod h1:9xiEKn7CgFaY6hvpVxQxctYAShigf9LKKNH4uZuEogc= diff --git a/v2/loco.go b/v2/loco.go new file mode 100644 index 0000000..15e9a55 --- /dev/null +++ b/v2/loco.go @@ -0,0 +1,84 @@ +package loco + +import ( + "context" + "errors" + "log" + + "github.com/lucasmdrs/ctxpoller" +) + +const baseURL = "https://localise.biz/api/" +const authEndpoint = "auth/verify" +const filenameTemplate = "%s.json" +const endpointTemplate = "export/" + filenameTemplate +const authParameter = "?key=%s" + +// goloco is a structure that holds the required information and implements the GoLoco service interface +type goloco struct { + notifier chan interface{} + poller ctxpoller.Poller + projects map[uint]*project +} + +// GoLoco defines the service interface +type GoLoco interface { + AddProject(key, assetsPath string) error + StartPoller() (chan interface{}, error) + StopPoller() + FetchTranslations(context.Context) +} + +// Init initializes the service +func Init() GoLoco { + return &goloco{ + notifier: make(chan interface{}), + projects: make(map[uint]*project, 0), + } +} + +// AddProject includes a new Loco project from a API Key +// and sets the assets destination. +func (g *goloco) AddProject(key string, assetsDestinationPath string) error { + p, err := getProjectInformation(key) + if err != nil { + return err + } + if _, exists := g.projects[p.ID]; exists { + return errors.New("Duplicated project") + } + p.assetsPath = assetsDestinationPath + g.projects[p.ID] = &p + g.notifier = make(chan interface{}, len(g.projects)) + return nil +} + +// StartPoller keep a poller for any changes in the projects translations +// notifying it in the returned channel whenever the assets changed +func (g *goloco) StartPoller() (chan interface{}, error) { + g.poller = ctxpoller.DefaultPoller(g.FetchTranslations) + return g.notifier, g.poller.Start() +} + +// StopPoller stops looking for changes in the translations +func (g *goloco) StopPoller() { + g.poller.Stop() +} + +// FetchTranslations fetches the translations from Loco and save it assets +func (g *goloco) FetchTranslations(context.Context) { + for _, p := range g.projects { + if err := p.fetchProjectTranslations(g.notifier); err != nil { + log.Fatalf("Failed to retrieve translations for %s: %s", p.Name, err.Error()) + } + } + if g.poller == nil || !g.poller.IsActive() { + emptyChannel(g.notifier) + } +} + +func emptyChannel(ch chan interface{}) { + for len(ch) > 0 { + <-ch + } +} diff --git a/v2/project.go b/v2/project.go new file mode 100644 index 0000000..98042e6 --- /dev/null +++ b/v2/project.go @@ -0,0 +1,90 @@ +package loco + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "log" + "net/http" + "path" + "reflect" +) + +type authVerifyResponse struct { + project `json:"project"` +} +type project struct { + key string + ID uint `json:"id"` + Name string `json:"name"` + assetsPath string + lastResult translationResponse +} + +func getProjectInformation(key string) (project, error) { + res, err := http.Get(fmt.Sprintf(baseURL+authEndpoint+authParameter, key)) + if err != nil { + return project{}, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return project{}, errors.New("Invalid Key") + } + var data authVerifyResponse + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return project{}, err + } + data.project.key = key + data.project.lastResult = make(translationResponse) + return data.project, nil +} + +func (p *project) fetchProjectTranslations(notifier chan interface{}) error { + uri := fmt.Sprintf(baseURL+endpointTemplate+authParameter, "all", p.key) + res, err := http.Get(uri) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return errors.New("Failed to request project translations") + } + + var locoData translationResponse + if err := json.NewDecoder(res.Body).Decode(&locoData); err != nil { + return err + } + + if reflect.DeepEqual(locoData, p.lastResult) { + return nil + } + + for lang, keys := range locoData { + if reflect.DeepEqual(keys, p.lastResult[lang]) { + continue + } + + json, err := keys.toJSONTranslationList() + if err != nil { + return err + } + + if err := writeToFile(json, p.assetsPath, lang); err != nil { + return err + } + + p.lastResult[lang] = keys + log.Println(p.Name, ":", lang) + } + + notifier <- nil + return nil +} + +func writeToFile(content []byte, destination, lang string) error { + destination = path.Join(destination, "%s.json") + return ioutil.WriteFile(fmt.Sprintf(destination, lang), content, 0644) +} diff --git a/v2/translation.go b/v2/translation.go new file mode 100644 index 0000000..a673395 --- /dev/null +++ b/v2/translation.go @@ -0,0 +1,27 @@ +package loco + +import "encoding/json" + +// translation represents a translation from Loco +// as structure that is compatible with go-i18n + +type keyValue map[string]string + +type translationResponse map[string]keyValue + +func (kv *keyValue) toTranslationList() []translation { + list := make([]translation, 0) + for id, t := range *kv { + list = append(list, translation{ID: id, Translation: t}) + } + return list +} + +func (kv *keyValue) toJSONTranslationList() ([]byte, error) { + return json.Marshal(kv.toTranslationList()) +} + +type translation struct { + ID string `json:"id"` + Translation string `json:"translation"` +} From bf736f7b945e311ad2882b9fd102fb0b33c745a2 Mon Sep 17 00:00:00 2001 From: Lucas Medeiros Date: Thu, 16 Jan 2020 17:58:20 -0300 Subject: [PATCH 2/2] fix docs --- v2/README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/v2/README.md b/v2/README.md index 4c2e0f8..a2281c2 100644 --- a/v2/README.md +++ b/v2/README.md @@ -2,7 +2,13 @@ A simple lib to fetch translation files from [Localise](https://localise.biz/) and save it in a [go-i18n](https://github.com/nicksnyder/go-i18n) compatible format. -## Usage +## Features +- Multiple project management +- Automatic download new translations + +Checkout some [examples](./examples). + +## Basic Usage ```golang package main @@ -11,12 +17,12 @@ import ( "context" "fmt" - loco "github.com/lucasmdrs/go-loco" + loco "github.com/lucasmdrs/go-loco/v2" "github.com/nicksnyder/go-i18n/i18n" ) func main() { - g := Init() + g := loco.Init() if err := g.AddProject("MY_LOCALISE_KEY", "./"); err != nil { panic(err)