Skip to content

Commit

Permalink
Merge pull request #1 from lucasmdrs/v2
Browse files Browse the repository at this point in the history
v2.0.0
  • Loading branch information
lucasmdrs authored Jan 16, 2020
2 parents a90a89f + bf736f7 commit 5ae8f57
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions v2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## 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.

## Features
- Multiple project management
- Automatic download new translations

Checkout some [examples](./examples).

## Basic Usage

```golang
package main

import (
"context"
"fmt"

loco "github.com/lucasmdrs/go-loco/v2"
"github.com/nicksnyder/go-i18n/i18n"
)

func main() {
g := loco.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"))
}
```
22 changes: 22 additions & 0 deletions v2/examples/multiple_projects.go
Original file line number Diff line number Diff line change
@@ -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())

}
32 changes: 32 additions & 0 deletions v2/examples/polling.go
Original file line number Diff line number Diff line change
@@ -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!")
}
}

}
5 changes: 5 additions & 0 deletions v2/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/lucasmdrs/go-loco/v2

go 1.13

require github.com/lucasmdrs/ctxpoller v1.0.0
2 changes: 2 additions & 0 deletions v2/go.sum
Original file line number Diff line number Diff line change
@@ -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=
84 changes: 84 additions & 0 deletions v2/loco.go
Original file line number Diff line number Diff line change
@@ -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
}
}
90 changes: 90 additions & 0 deletions v2/project.go
Original file line number Diff line number Diff line change
@@ -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)
}
27 changes: 27 additions & 0 deletions v2/translation.go
Original file line number Diff line number Diff line change
@@ -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"`
}

0 comments on commit 5ae8f57

Please sign in to comment.