Skip to content

Commit

Permalink
Add flatter data file structure (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
n10v authored and nicksnyder committed Mar 19, 2017
1 parent 5a40a66 commit be79775
Show file tree
Hide file tree
Showing 26 changed files with 672 additions and 44 deletions.
48 changes: 48 additions & 0 deletions README.md
Expand Up @@ -118,6 +118,54 @@ Here is an example of the default file format that go-i18n supports:

To use a different file format, write a parser for the format and add the parsed translations using [AddTranslation](https://godoc.org/github.com/nicksnyder/go-i18n/i18n#AddTranslation).

Flat Format
-------------

You can also write shorter translation files with flat format.
E.g the example above can be written in this way:

```json
{
"d_days": {
"one": "{{.Count}} day.",
"other": "{{.Count}} days."
},

"my_height_in_meters": {
"one": "I am {{.Count}} meter tall.",
"other": "I am {{.Count}} meters tall."
},

"person_greeting": {
"other": "Hello {{.Person}}"
},

"person_unread_email_count": {
"one": "{{.Person}} has {{.Count}} unread email.",
"other": "{{.Person}} has {{.Count}} unread emails."
},

"person_unread_email_count_timeframe": {
"one": "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}.",
"other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}."
},

"program_greeting": {
"other": "Hello world"
},

"your_unread_email_count": {
"one": "You have {{.Count}} unread email.",
"other": "You have {{.Count}} unread emails."
}
}
```

The logic of flat format is, what it is structure of structures
and name of substructures (ids) should be always a string.
If there is only one key in substructure and it is "other", then it's non-plural
translation, else plural.

Contributions
-------------

Expand Down
1 change: 0 additions & 1 deletion goi18n/constants_command.go
Expand Up @@ -139,7 +139,6 @@ Options:
Default: .
`)
os.Exit(1)
}

// commonInitialisms is a set of common initialisms.
Expand Down
66 changes: 39 additions & 27 deletions goi18n/merge_command.go
Expand Up @@ -5,7 +5,6 @@ import (
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"sort"
Expand All @@ -22,6 +21,7 @@ type mergeCommand struct {
sourceLanguage string
outdir string
format string
flat bool
}

func (mc *mergeCommand) execute() error {
Expand All @@ -33,11 +33,6 @@ func (mc *mergeCommand) execute() error {
return fmt.Errorf("invalid source locale: %s", mc.sourceLanguage)
}

marshal, err := newMarshalFunc(mc.format)
if err != nil {
return err
}

bundle := bundle.New()
for _, tf := range mc.translationFiles {
if err := bundle.LoadTranslationFile(tf); err != nil {
Expand All @@ -64,7 +59,7 @@ func (mc *mergeCommand) execute() error {
all := filter(localeTranslations, func(t translation.Translation) translation.Translation {
return t.Normalize(lang)
})
if err := mc.writeFile("all", all, localeID, marshal); err != nil {
if err := mc.writeFile("all", all, localeID); err != nil {
return err
}

Expand All @@ -74,7 +69,7 @@ func (mc *mergeCommand) execute() error {
}
return nil
})
if err := mc.writeFile("untranslated", untranslated, localeID, marshal); err != nil {
if err := mc.writeFile("untranslated", untranslated, localeID); err != nil {
return err
}
}
Expand All @@ -88,67 +83,81 @@ func (mc *mergeCommand) parse(arguments []string) {
sourceLanguage := flags.String("sourceLanguage", "en-us", "")
outdir := flags.String("outdir", ".", "")
format := flags.String("format", "json", "")
flat := flags.Bool("flat", true, "")

flags.Parse(arguments)

mc.translationFiles = flags.Args()
mc.sourceLanguage = *sourceLanguage
mc.outdir = *outdir
mc.format = *format
mc.flat = *flat
}

func (mc *mergeCommand) SetArgs(args []string) {
mc.translationFiles = args
}

type marshalFunc func(interface{}) ([]byte, error)

func (mc *mergeCommand) writeFile(label string, translations []translation.Translation, localeID string, marshal marshalFunc) error {
func (mc *mergeCommand) writeFile(label string, translations []translation.Translation, localeID string) error {
sort.Sort(translation.SortableByID(translations))
buf, err := marshal(marshalInterface(translations))

var convert func([]translation.Translation) interface{}
if mc.flat {
convert = marshalFlatInterface
} else {
convert = marshalInterface
}

buf, err := mc.marshal(convert(translations))
if err != nil {
return fmt.Errorf("failed to marshal %s strings to %s because %s", localeID, mc.format, err)
}

filename := filepath.Join(mc.outdir, fmt.Sprintf("%s.%s.%s", localeID, label, mc.format))

if err := ioutil.WriteFile(filename, buf, 0666); err != nil {
return fmt.Errorf("failed to write %s because %s", filename, err)
}
return nil
}

func filter(translations map[string]translation.Translation, filter func(translation.Translation) translation.Translation) []translation.Translation {
func filter(translations map[string]translation.Translation, f func(translation.Translation) translation.Translation) []translation.Translation {
filtered := make([]translation.Translation, 0, len(translations))
for _, translation := range translations {
if t := filter(translation); t != nil {
if t := f(translation); t != nil {
filtered = append(filtered, t)
}
}
return filtered

}

func newMarshalFunc(format string) (marshalFunc, error) {
switch format {
case "json":
return func(v interface{}) ([]byte, error) {
return json.MarshalIndent(v, "", " ")
}, nil
case "yaml":
return func(v interface{}) ([]byte, error) {
return yaml.Marshal(v)
}, nil
func marshalFlatInterface(translations []translation.Translation) interface{} {
mi := make(map[string]interface{}, len(translations))
for _, translation := range translations {
mi[translation.ID()] = translation.MarshalFlatInterface()
}
return nil, fmt.Errorf("unsupported format: %s\n", format)
return mi
}

func marshalInterface(translations []translation.Translation) []interface{} {
func marshalInterface(translations []translation.Translation) interface{} {
mi := make([]interface{}, len(translations))
for i, translation := range translations {
mi[i] = translation.MarshalInterface()
}
return mi
}

func (mc mergeCommand) marshal(v interface{}) ([]byte, error) {
switch mc.format {
case "json":
return json.MarshalIndent(v, "", " ")
case "yaml":
return yaml.Marshal(v)
}
return nil, fmt.Errorf("unsupported format: %s\n", mc.format)
}

func usageMerge() {
fmt.Printf(`Merge translation files.
Expand Down Expand Up @@ -201,6 +210,9 @@ Options:
Supported formats: json, yaml
Default: json
-flat
goi18n writes the output translation files in flat format.
Default: true
`)
os.Exit(1)
}
36 changes: 36 additions & 0 deletions goi18n/merge_command_flat_test.go
@@ -0,0 +1,36 @@
package main

import "testing"

func TestMergeExecuteFlatJSON(t *testing.T) {
files := []string{
"testdata/input/flat/en-us.one.json",
"testdata/input/flat/en-us.two.json",
"testdata/input/flat/fr-fr.json",
"testdata/input/flat/ar-ar.one.json",
"testdata/input/flat/ar-ar.two.json",
}
testFlatMergeExecute(t, files)
}

func testFlatMergeExecute(t *testing.T, files []string) {
resetDir(t, "testdata/output/flat")

mc := &mergeCommand{
translationFiles: files,
sourceLanguage: "en-us",
outdir: "testdata/output/flat",
format: "json",
flat: true,
}
if err := mc.execute(); err != nil {
t.Fatal(err)
}

expectEqualFiles(t, "testdata/output/flat/en-us.all.json", "testdata/expected/flat/en-us.all.json")
expectEqualFiles(t, "testdata/output/flat/ar-ar.all.json", "testdata/expected/flat/ar-ar.all.json")
expectEqualFiles(t, "testdata/output/flat/fr-fr.all.json", "testdata/expected/flat/fr-fr.all.json")
expectEqualFiles(t, "testdata/output/flat/en-us.untranslated.json", "testdata/expected/flat/en-us.untranslated.json")
expectEqualFiles(t, "testdata/output/flat/ar-ar.untranslated.json", "testdata/expected/flat/ar-ar.untranslated.json")
expectEqualFiles(t, "testdata/output/flat/fr-fr.untranslated.json", "testdata/expected/flat/fr-fr.untranslated.json")
}
3 changes: 2 additions & 1 deletion goi18n/merge_command_test.go
Expand Up @@ -37,6 +37,7 @@ func testMergeExecute(t *testing.T, files []string) {
sourceLanguage: "en-us",
outdir: "testdata/output",
format: "json",
flat: false,
}
if err := mc.execute(); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -69,6 +70,6 @@ func expectEqualFiles(t *testing.T, expectedName, actualName string) {
t.Fatal(err)
}
if !bytes.Equal(actual, expected) {
t.Fatalf("contents of files did not match: %s, %s", expectedName, actualName)
t.Errorf("contents of files did not match: %s, %s", expectedName, actualName)
}
}
34 changes: 34 additions & 0 deletions goi18n/testdata/en-us.flat.json
@@ -0,0 +1,34 @@
{
"program_greeting": {
"other": "Hello world"
},

"person_greeting": {
"other": "Hello {{.Person}}"
},

"my_height_in_meters": {
"one": "I am {{.Count}} meter tall.",
"other": "I am {{.Count}} meters tall."
},

"your_unread_email_count": {
"one": "You have {{.Count}} unread email.",
"other": "You have {{.Count}} unread emails."
},

"person_unread_email_count": {
"one": "{{.Person}} has {{.Count}} unread email.",
"other": "{{.Person}} has {{.Count}} unread emails."
},

"person_unread_email_count_timeframe": {
"one": "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}.",
"other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}."
},

"d_days": {
"one": "{{.Count}} day.",
"other": "{{.Count}} days."
}
}
25 changes: 25 additions & 0 deletions goi18n/testdata/en-us.flat.yaml
@@ -0,0 +1,25 @@
program_greeting:
other: "Hello world"

person_greeting:
other: "Hello {{.Person}}"

my_height_in_meters:
one: "I am {{.Count}} meter tall."
other: "I am {{.Count}} meters tall."

your_unread_email_count:
one: "You have {{.Count}} unread email."
other: "You have {{.Count}} unread emails."

person_unread_email_count:
one: "{{.Person}} has {{.Count}} unread email."
other: "{{.Person}} has {{.Count}} unread emails."

person_unread_email_count_timeframe:
one: "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}."
other: "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}."

d_days:
one: "{{.Count}} day"
other: "{{.Count}} days"
43 changes: 43 additions & 0 deletions goi18n/testdata/expected/flat/ar-ar.all.json
@@ -0,0 +1,43 @@
{
"d_days": {
"few": "new arabic few translation of d_days",
"many": "arabic many translation of d_days",
"one": "arabic one translation of d_days",
"other": "",
"two": "",
"zero": ""
},
"my_height_in_meters": {
"few": "",
"many": "",
"one": "",
"other": "",
"two": "",
"zero": ""
},
"person_greeting": {
"other": "new arabic translation of person_greeting"
},
"person_unread_email_count": {
"few": "arabic few translation of person_unread_email_count",
"many": "arabic many translation of person_unread_email_count",
"one": "arabic one translation of person_unread_email_count",
"other": "arabic other translation of person_unread_email_count",
"two": "arabic two translation of person_unread_email_count",
"zero": "arabic zero translation of person_unread_email_count"
},
"person_unread_email_count_timeframe": {
"other": ""
},
"program_greeting": {
"other": ""
},
"your_unread_email_count": {
"few": "",
"many": "",
"one": "",
"other": "",
"two": "",
"zero": ""
}
}

0 comments on commit be79775

Please sign in to comment.