Skip to content

Commit 8b3465d

Browse files
benoitmassonnicksnyder
authored andcommitted
Nested translation support (#157)
1 parent 7a73c96 commit 8b3465d

File tree

3 files changed

+321
-67
lines changed

3 files changed

+321
-67
lines changed

v2/internal/message.go

Lines changed: 77 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -105,60 +105,97 @@ func stringMap(v interface{}) (map[string]string, error) {
105105
case map[string]string:
106106
return value, nil
107107
case map[string]interface{}:
108-
strdata := map[string]string{}
108+
strdata := make(map[string]string, len(value))
109109
for k, v := range value {
110-
if k == "translation" {
111-
switch vt := v.(type) {
112-
case string:
113-
strdata["other"] = vt
114-
default:
115-
v1Message, err := stringMap(v)
116-
if err != nil {
117-
return nil, err
118-
}
119-
for kk, vv := range v1Message {
120-
strdata[kk] = vv
121-
}
122-
}
123-
continue
124-
}
125-
vstr, ok := v.(string)
126-
if !ok {
127-
return nil, fmt.Errorf("expected value for key %q be a string but got %#v", k, v)
110+
err := stringSubmap(k, v, strdata)
111+
if err != nil {
112+
return nil, err
128113
}
129-
strdata[k] = vstr
130114
}
131115
return strdata, nil
132116
case map[interface{}]interface{}:
133-
strdata := map[string]string{}
117+
strdata := make(map[string]string, len(value))
134118
for k, v := range value {
135119
kstr, ok := k.(string)
136120
if !ok {
137121
return nil, fmt.Errorf("expected key to be a string but got %#v", k)
138122
}
139-
if kstr == "translation" {
140-
switch vt := v.(type) {
141-
case string:
142-
strdata["other"] = vt
143-
default:
144-
v1Message, err := stringMap(v)
145-
if err != nil {
146-
return nil, err
147-
}
148-
for kk, vv := range v1Message {
149-
strdata[kk] = vv
150-
}
151-
}
152-
continue
123+
err := stringSubmap(kstr, v, strdata)
124+
if err != nil {
125+
return nil, err
153126
}
154-
vstr, ok := v.(string)
155-
if !ok {
156-
return nil, fmt.Errorf("expected value for key %q be a string but got %#v", k, v)
157-
}
158-
strdata[kstr] = vstr
159127
}
160128
return strdata, nil
161129
default:
162130
return nil, fmt.Errorf("unsupported type %#v", value)
163131
}
164132
}
133+
134+
func stringSubmap(k string, v interface{}, strdata map[string]string) error {
135+
if k == "translation" {
136+
switch vt := v.(type) {
137+
case string:
138+
strdata["other"] = vt
139+
default:
140+
v1Message, err := stringMap(v)
141+
if err != nil {
142+
return err
143+
}
144+
for kk, vv := range v1Message {
145+
strdata[kk] = vv
146+
}
147+
}
148+
return nil
149+
}
150+
vstr, ok := v.(string)
151+
if !ok {
152+
return fmt.Errorf("expected value for key %q be a string but got %#v", k, v)
153+
}
154+
strdata[k] = vstr
155+
return nil
156+
}
157+
158+
// isMessage tells whether the given data is a message, or a map containing
159+
// nested messages.
160+
// A map is assumed to be a message if it contains any of the "reserved" keys:
161+
// "id", "description", "hash", "leftdelim", "rightdelim", "zero", "one", "two", "few", "many", "other"
162+
// with a string value.
163+
// e.g.,
164+
// - {"message": {"description": "world"}} is a message
165+
// - {"message": {"description": "world", "foo": "bar"}} is a message ("foo" key is ignored)
166+
// - {"notmessage": {"description": {"hello": "world"}}} is not
167+
// - {"notmessage": {"foo": "bar"}} is not
168+
func isMessage(v interface{}) bool {
169+
reservedKeys := []string{"id", "description", "hash", "leftdelim", "rightdelim", "zero", "one", "two", "few", "many", "other"}
170+
switch data := v.(type) {
171+
case string:
172+
return true
173+
case map[string]interface{}:
174+
for _, key := range reservedKeys {
175+
val, ok := data[key]
176+
if !ok {
177+
continue
178+
}
179+
_, ok = val.(string)
180+
if !ok {
181+
continue
182+
}
183+
// v is a message if it contains a "reserved" key holding a string value
184+
return true
185+
}
186+
case map[interface{}]interface{}:
187+
for _, key := range reservedKeys {
188+
val, ok := data[key]
189+
if !ok {
190+
continue
191+
}
192+
_, ok = val.(string)
193+
if !ok {
194+
continue
195+
}
196+
// v is a message if it contains a "reserved" key holding a string value
197+
return true
198+
}
199+
}
200+
return false
201+
}

v2/internal/parse.go

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package internal
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67
"os"
78

@@ -39,49 +40,105 @@ func ParseMessageFileBytes(buf []byte, path string, unmarshalFuncs map[string]Un
3940
return nil, fmt.Errorf("no unmarshaler registered for %s", messageFile.Format)
4041
}
4142
}
43+
var err error
4244
var raw interface{}
43-
if err := unmarshalFunc(buf, &raw); err != nil {
45+
if err = unmarshalFunc(buf, &raw); err != nil {
4446
return nil, err
4547
}
48+
49+
if messageFile.Messages, err = recGetMessages(raw, isMessage(raw), true); err != nil {
50+
return nil, err
51+
}
52+
53+
return messageFile, nil
54+
}
55+
56+
const nestedSeparator = "."
57+
58+
var errInvalidTranslationFile = errors.New("invalid translation file, expected key-values, got a single value")
59+
60+
// recGetMessages looks for translation messages inside "raw" parameter,
61+
// scanning nested maps using recursion.
62+
func recGetMessages(raw interface{}, isMapMessage, isInitialCall bool) ([]*Message, error) {
63+
var messages []*Message
64+
var err error
65+
4666
switch data := raw.(type) {
67+
case string:
68+
if isInitialCall {
69+
return nil, errInvalidTranslationFile
70+
}
71+
m, err := NewMessage(data)
72+
return []*Message{m}, err
73+
4774
case map[string]interface{}:
48-
messageFile.Messages = make([]*Message, 0, len(data))
49-
for id, data := range data {
75+
if isMapMessage {
5076
m, err := NewMessage(data)
77+
return []*Message{m}, err
78+
}
79+
messages = make([]*Message, 0, len(data))
80+
for id, data := range data {
81+
// recursively scan map items
82+
messages, err = addChildMessages(id, data, messages)
5183
if err != nil {
5284
return nil, err
5385
}
54-
m.ID = id
55-
messageFile.Messages = append(messageFile.Messages, m)
5686
}
87+
5788
case map[interface{}]interface{}:
58-
messageFile.Messages = make([]*Message, 0, len(data))
89+
if isMapMessage {
90+
m, err := NewMessage(data)
91+
return []*Message{m}, err
92+
}
93+
messages = make([]*Message, 0, len(data))
5994
for id, data := range data {
6095
strid, ok := id.(string)
6196
if !ok {
6297
return nil, fmt.Errorf("expected key to be string but got %#v", id)
6398
}
64-
m, err := NewMessage(data)
99+
// recursively scan map items
100+
messages, err = addChildMessages(strid, data, messages)
65101
if err != nil {
66102
return nil, err
67103
}
68-
m.ID = strid
69-
messageFile.Messages = append(messageFile.Messages, m)
70104
}
105+
71106
case []interface{}:
72107
// Backward compatibility for v1 file format.
73-
messageFile.Messages = make([]*Message, 0, len(data))
108+
messages = make([]*Message, 0, len(data))
74109
for _, data := range data {
75-
m, err := NewMessage(data)
110+
// recursively scan slice items
111+
childMessages, err := recGetMessages(data, isMessage(data), false)
76112
if err != nil {
77113
return nil, err
78114
}
79-
messageFile.Messages = append(messageFile.Messages, m)
115+
messages = append(messages, childMessages...)
80116
}
117+
81118
default:
82119
return nil, fmt.Errorf("unsupported file format %T", raw)
83120
}
84-
return messageFile, nil
121+
122+
return messages, nil
123+
}
124+
125+
func addChildMessages(id string, data interface{}, messages []*Message) ([]*Message, error) {
126+
isChildMessage := isMessage(data)
127+
childMessages, err := recGetMessages(data, isChildMessage, false)
128+
if err != nil {
129+
return nil, err
130+
}
131+
for _, m := range childMessages {
132+
if isChildMessage {
133+
if m.ID == "" {
134+
m.ID = id // start with innermost key
135+
}
136+
} else {
137+
m.ID = id + nestedSeparator + m.ID // update ID with each nested key on the way
138+
}
139+
messages = append(messages, m)
140+
}
141+
return messages, nil
85142
}
86143

87144
func parsePath(path string) (langTag, format string) {

0 commit comments

Comments
 (0)