Skip to content

Commit be79775

Browse files
n10vnicksnyder
authored andcommitted
Add flatter data file structure (#65)
1 parent 5a40a66 commit be79775

26 files changed

+672
-44
lines changed

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,54 @@ Here is an example of the default file format that go-i18n supports:
118118
119119
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).
120120
121+
Flat Format
122+
-------------
123+
124+
You can also write shorter translation files with flat format.
125+
E.g the example above can be written in this way:
126+
127+
```json
128+
{
129+
"d_days": {
130+
"one": "{{.Count}} day.",
131+
"other": "{{.Count}} days."
132+
},
133+
134+
"my_height_in_meters": {
135+
"one": "I am {{.Count}} meter tall.",
136+
"other": "I am {{.Count}} meters tall."
137+
},
138+
139+
"person_greeting": {
140+
"other": "Hello {{.Person}}"
141+
},
142+
143+
"person_unread_email_count": {
144+
"one": "{{.Person}} has {{.Count}} unread email.",
145+
"other": "{{.Person}} has {{.Count}} unread emails."
146+
},
147+
148+
"person_unread_email_count_timeframe": {
149+
"one": "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}.",
150+
"other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}."
151+
},
152+
153+
"program_greeting": {
154+
"other": "Hello world"
155+
},
156+
157+
"your_unread_email_count": {
158+
"one": "You have {{.Count}} unread email.",
159+
"other": "You have {{.Count}} unread emails."
160+
}
161+
}
162+
```
163+
164+
The logic of flat format is, what it is structure of structures
165+
and name of substructures (ids) should be always a string.
166+
If there is only one key in substructure and it is "other", then it's non-plural
167+
translation, else plural.
168+
121169
Contributions
122170
-------------
123171

goi18n/constants_command.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ Options:
139139
Default: .
140140
141141
`)
142-
os.Exit(1)
143142
}
144143

145144
// commonInitialisms is a set of common initialisms.

goi18n/merge_command.go

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"flag"
66
"fmt"
77
"io/ioutil"
8-
"os"
98
"path/filepath"
109
"reflect"
1110
"sort"
@@ -22,6 +21,7 @@ type mergeCommand struct {
2221
sourceLanguage string
2322
outdir string
2423
format string
24+
flat bool
2525
}
2626

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

36-
marshal, err := newMarshalFunc(mc.format)
37-
if err != nil {
38-
return err
39-
}
40-
4136
bundle := bundle.New()
4237
for _, tf := range mc.translationFiles {
4338
if err := bundle.LoadTranslationFile(tf); err != nil {
@@ -64,7 +59,7 @@ func (mc *mergeCommand) execute() error {
6459
all := filter(localeTranslations, func(t translation.Translation) translation.Translation {
6560
return t.Normalize(lang)
6661
})
67-
if err := mc.writeFile("all", all, localeID, marshal); err != nil {
62+
if err := mc.writeFile("all", all, localeID); err != nil {
6863
return err
6964
}
7065

@@ -74,7 +69,7 @@ func (mc *mergeCommand) execute() error {
7469
}
7570
return nil
7671
})
77-
if err := mc.writeFile("untranslated", untranslated, localeID, marshal); err != nil {
72+
if err := mc.writeFile("untranslated", untranslated, localeID); err != nil {
7873
return err
7974
}
8075
}
@@ -88,67 +83,81 @@ func (mc *mergeCommand) parse(arguments []string) {
8883
sourceLanguage := flags.String("sourceLanguage", "en-us", "")
8984
outdir := flags.String("outdir", ".", "")
9085
format := flags.String("format", "json", "")
86+
flat := flags.Bool("flat", true, "")
9187

9288
flags.Parse(arguments)
9389

9490
mc.translationFiles = flags.Args()
9591
mc.sourceLanguage = *sourceLanguage
9692
mc.outdir = *outdir
9793
mc.format = *format
94+
mc.flat = *flat
9895
}
9996

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

104-
type marshalFunc func(interface{}) ([]byte, error)
105-
106-
func (mc *mergeCommand) writeFile(label string, translations []translation.Translation, localeID string, marshal marshalFunc) error {
101+
func (mc *mergeCommand) writeFile(label string, translations []translation.Translation, localeID string) error {
107102
sort.Sort(translation.SortableByID(translations))
108-
buf, err := marshal(marshalInterface(translations))
103+
104+
var convert func([]translation.Translation) interface{}
105+
if mc.flat {
106+
convert = marshalFlatInterface
107+
} else {
108+
convert = marshalInterface
109+
}
110+
111+
buf, err := mc.marshal(convert(translations))
109112
if err != nil {
110113
return fmt.Errorf("failed to marshal %s strings to %s because %s", localeID, mc.format, err)
111114
}
115+
112116
filename := filepath.Join(mc.outdir, fmt.Sprintf("%s.%s.%s", localeID, label, mc.format))
117+
113118
if err := ioutil.WriteFile(filename, buf, 0666); err != nil {
114119
return fmt.Errorf("failed to write %s because %s", filename, err)
115120
}
116121
return nil
117122
}
118123

119-
func filter(translations map[string]translation.Translation, filter func(translation.Translation) translation.Translation) []translation.Translation {
124+
func filter(translations map[string]translation.Translation, f func(translation.Translation) translation.Translation) []translation.Translation {
120125
filtered := make([]translation.Translation, 0, len(translations))
121126
for _, translation := range translations {
122-
if t := filter(translation); t != nil {
127+
if t := f(translation); t != nil {
123128
filtered = append(filtered, t)
124129
}
125130
}
126131
return filtered
127132

128133
}
129134

130-
func newMarshalFunc(format string) (marshalFunc, error) {
131-
switch format {
132-
case "json":
133-
return func(v interface{}) ([]byte, error) {
134-
return json.MarshalIndent(v, "", " ")
135-
}, nil
136-
case "yaml":
137-
return func(v interface{}) ([]byte, error) {
138-
return yaml.Marshal(v)
139-
}, nil
135+
func marshalFlatInterface(translations []translation.Translation) interface{} {
136+
mi := make(map[string]interface{}, len(translations))
137+
for _, translation := range translations {
138+
mi[translation.ID()] = translation.MarshalFlatInterface()
140139
}
141-
return nil, fmt.Errorf("unsupported format: %s\n", format)
140+
return mi
142141
}
143142

144-
func marshalInterface(translations []translation.Translation) []interface{} {
143+
func marshalInterface(translations []translation.Translation) interface{} {
145144
mi := make([]interface{}, len(translations))
146145
for i, translation := range translations {
147146
mi[i] = translation.MarshalInterface()
148147
}
149148
return mi
150149
}
151150

151+
func (mc mergeCommand) marshal(v interface{}) ([]byte, error) {
152+
switch mc.format {
153+
case "json":
154+
return json.MarshalIndent(v, "", " ")
155+
case "yaml":
156+
return yaml.Marshal(v)
157+
}
158+
return nil, fmt.Errorf("unsupported format: %s\n", mc.format)
159+
}
160+
152161
func usageMerge() {
153162
fmt.Printf(`Merge translation files.
154163
@@ -201,6 +210,9 @@ Options:
201210
Supported formats: json, yaml
202211
Default: json
203212
213+
-flat
214+
goi18n writes the output translation files in flat format.
215+
Default: true
216+
204217
`)
205-
os.Exit(1)
206218
}

goi18n/merge_command_flat_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package main
2+
3+
import "testing"
4+
5+
func TestMergeExecuteFlatJSON(t *testing.T) {
6+
files := []string{
7+
"testdata/input/flat/en-us.one.json",
8+
"testdata/input/flat/en-us.two.json",
9+
"testdata/input/flat/fr-fr.json",
10+
"testdata/input/flat/ar-ar.one.json",
11+
"testdata/input/flat/ar-ar.two.json",
12+
}
13+
testFlatMergeExecute(t, files)
14+
}
15+
16+
func testFlatMergeExecute(t *testing.T, files []string) {
17+
resetDir(t, "testdata/output/flat")
18+
19+
mc := &mergeCommand{
20+
translationFiles: files,
21+
sourceLanguage: "en-us",
22+
outdir: "testdata/output/flat",
23+
format: "json",
24+
flat: true,
25+
}
26+
if err := mc.execute(); err != nil {
27+
t.Fatal(err)
28+
}
29+
30+
expectEqualFiles(t, "testdata/output/flat/en-us.all.json", "testdata/expected/flat/en-us.all.json")
31+
expectEqualFiles(t, "testdata/output/flat/ar-ar.all.json", "testdata/expected/flat/ar-ar.all.json")
32+
expectEqualFiles(t, "testdata/output/flat/fr-fr.all.json", "testdata/expected/flat/fr-fr.all.json")
33+
expectEqualFiles(t, "testdata/output/flat/en-us.untranslated.json", "testdata/expected/flat/en-us.untranslated.json")
34+
expectEqualFiles(t, "testdata/output/flat/ar-ar.untranslated.json", "testdata/expected/flat/ar-ar.untranslated.json")
35+
expectEqualFiles(t, "testdata/output/flat/fr-fr.untranslated.json", "testdata/expected/flat/fr-fr.untranslated.json")
36+
}

goi18n/merge_command_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func testMergeExecute(t *testing.T, files []string) {
3737
sourceLanguage: "en-us",
3838
outdir: "testdata/output",
3939
format: "json",
40+
flat: false,
4041
}
4142
if err := mc.execute(); err != nil {
4243
t.Fatal(err)
@@ -69,6 +70,6 @@ func expectEqualFiles(t *testing.T, expectedName, actualName string) {
6970
t.Fatal(err)
7071
}
7172
if !bytes.Equal(actual, expected) {
72-
t.Fatalf("contents of files did not match: %s, %s", expectedName, actualName)
73+
t.Errorf("contents of files did not match: %s, %s", expectedName, actualName)
7374
}
7475
}

goi18n/testdata/en-us.flat.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"program_greeting": {
3+
"other": "Hello world"
4+
},
5+
6+
"person_greeting": {
7+
"other": "Hello {{.Person}}"
8+
},
9+
10+
"my_height_in_meters": {
11+
"one": "I am {{.Count}} meter tall.",
12+
"other": "I am {{.Count}} meters tall."
13+
},
14+
15+
"your_unread_email_count": {
16+
"one": "You have {{.Count}} unread email.",
17+
"other": "You have {{.Count}} unread emails."
18+
},
19+
20+
"person_unread_email_count": {
21+
"one": "{{.Person}} has {{.Count}} unread email.",
22+
"other": "{{.Person}} has {{.Count}} unread emails."
23+
},
24+
25+
"person_unread_email_count_timeframe": {
26+
"one": "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}.",
27+
"other": "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}."
28+
},
29+
30+
"d_days": {
31+
"one": "{{.Count}} day.",
32+
"other": "{{.Count}} days."
33+
}
34+
}

goi18n/testdata/en-us.flat.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
program_greeting:
2+
other: "Hello world"
3+
4+
person_greeting:
5+
other: "Hello {{.Person}}"
6+
7+
my_height_in_meters:
8+
one: "I am {{.Count}} meter tall."
9+
other: "I am {{.Count}} meters tall."
10+
11+
your_unread_email_count:
12+
one: "You have {{.Count}} unread email."
13+
other: "You have {{.Count}} unread emails."
14+
15+
person_unread_email_count:
16+
one: "{{.Person}} has {{.Count}} unread email."
17+
other: "{{.Person}} has {{.Count}} unread emails."
18+
19+
person_unread_email_count_timeframe:
20+
one: "{{.Person}} has {{.Count}} unread email in the past {{.Timeframe}}."
21+
other: "{{.Person}} has {{.Count}} unread emails in the past {{.Timeframe}}."
22+
23+
d_days:
24+
one: "{{.Count}} day"
25+
other: "{{.Count}} days"
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"d_days": {
3+
"few": "new arabic few translation of d_days",
4+
"many": "arabic many translation of d_days",
5+
"one": "arabic one translation of d_days",
6+
"other": "",
7+
"two": "",
8+
"zero": ""
9+
},
10+
"my_height_in_meters": {
11+
"few": "",
12+
"many": "",
13+
"one": "",
14+
"other": "",
15+
"two": "",
16+
"zero": ""
17+
},
18+
"person_greeting": {
19+
"other": "new arabic translation of person_greeting"
20+
},
21+
"person_unread_email_count": {
22+
"few": "arabic few translation of person_unread_email_count",
23+
"many": "arabic many translation of person_unread_email_count",
24+
"one": "arabic one translation of person_unread_email_count",
25+
"other": "arabic other translation of person_unread_email_count",
26+
"two": "arabic two translation of person_unread_email_count",
27+
"zero": "arabic zero translation of person_unread_email_count"
28+
},
29+
"person_unread_email_count_timeframe": {
30+
"other": ""
31+
},
32+
"program_greeting": {
33+
"other": ""
34+
},
35+
"your_unread_email_count": {
36+
"few": "",
37+
"many": "",
38+
"one": "",
39+
"other": "",
40+
"two": "",
41+
"zero": ""
42+
}
43+
}

0 commit comments

Comments
 (0)