Skip to content

Commit

Permalink
Nested translation support (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitmasson authored and nicksnyder committed Apr 12, 2019
1 parent 7a73c96 commit 8b3465d
Show file tree
Hide file tree
Showing 3 changed files with 321 additions and 67 deletions.
117 changes: 77 additions & 40 deletions v2/internal/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,60 +105,97 @@ func stringMap(v interface{}) (map[string]string, error) {
case map[string]string:
return value, nil
case map[string]interface{}:
strdata := map[string]string{}
strdata := make(map[string]string, len(value))
for k, v := range value {
if k == "translation" {
switch vt := v.(type) {
case string:
strdata["other"] = vt
default:
v1Message, err := stringMap(v)
if err != nil {
return nil, err
}
for kk, vv := range v1Message {
strdata[kk] = vv
}
}
continue
}
vstr, ok := v.(string)
if !ok {
return nil, fmt.Errorf("expected value for key %q be a string but got %#v", k, v)
err := stringSubmap(k, v, strdata)
if err != nil {
return nil, err
}
strdata[k] = vstr
}
return strdata, nil
case map[interface{}]interface{}:
strdata := map[string]string{}
strdata := make(map[string]string, len(value))
for k, v := range value {
kstr, ok := k.(string)
if !ok {
return nil, fmt.Errorf("expected key to be a string but got %#v", k)
}
if kstr == "translation" {
switch vt := v.(type) {
case string:
strdata["other"] = vt
default:
v1Message, err := stringMap(v)
if err != nil {
return nil, err
}
for kk, vv := range v1Message {
strdata[kk] = vv
}
}
continue
err := stringSubmap(kstr, v, strdata)
if err != nil {
return nil, err
}
vstr, ok := v.(string)
if !ok {
return nil, fmt.Errorf("expected value for key %q be a string but got %#v", k, v)
}
strdata[kstr] = vstr
}
return strdata, nil
default:
return nil, fmt.Errorf("unsupported type %#v", value)
}
}

func stringSubmap(k string, v interface{}, strdata map[string]string) error {
if k == "translation" {
switch vt := v.(type) {
case string:
strdata["other"] = vt
default:
v1Message, err := stringMap(v)
if err != nil {
return err
}
for kk, vv := range v1Message {
strdata[kk] = vv
}
}
return nil
}
vstr, ok := v.(string)
if !ok {
return fmt.Errorf("expected value for key %q be a string but got %#v", k, v)
}
strdata[k] = vstr
return nil
}

// isMessage tells whether the given data is a message, or a map containing
// nested messages.
// A map is assumed to be a message if it contains any of the "reserved" keys:
// "id", "description", "hash", "leftdelim", "rightdelim", "zero", "one", "two", "few", "many", "other"
// with a string value.
// e.g.,
// - {"message": {"description": "world"}} is a message
// - {"message": {"description": "world", "foo": "bar"}} is a message ("foo" key is ignored)
// - {"notmessage": {"description": {"hello": "world"}}} is not
// - {"notmessage": {"foo": "bar"}} is not
func isMessage(v interface{}) bool {
reservedKeys := []string{"id", "description", "hash", "leftdelim", "rightdelim", "zero", "one", "two", "few", "many", "other"}
switch data := v.(type) {
case string:
return true
case map[string]interface{}:
for _, key := range reservedKeys {
val, ok := data[key]
if !ok {
continue
}
_, ok = val.(string)
if !ok {
continue
}
// v is a message if it contains a "reserved" key holding a string value
return true
}
case map[interface{}]interface{}:
for _, key := range reservedKeys {
val, ok := data[key]
if !ok {
continue
}
_, ok = val.(string)
if !ok {
continue
}
// v is a message if it contains a "reserved" key holding a string value
return true
}
}
return false
}
83 changes: 70 additions & 13 deletions v2/internal/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package internal

import (
"encoding/json"
"errors"
"fmt"
"os"

Expand Down Expand Up @@ -39,49 +40,105 @@ func ParseMessageFileBytes(buf []byte, path string, unmarshalFuncs map[string]Un
return nil, fmt.Errorf("no unmarshaler registered for %s", messageFile.Format)
}
}
var err error
var raw interface{}
if err := unmarshalFunc(buf, &raw); err != nil {
if err = unmarshalFunc(buf, &raw); err != nil {
return nil, err
}

if messageFile.Messages, err = recGetMessages(raw, isMessage(raw), true); err != nil {
return nil, err
}

return messageFile, nil
}

const nestedSeparator = "."

var errInvalidTranslationFile = errors.New("invalid translation file, expected key-values, got a single value")

// recGetMessages looks for translation messages inside "raw" parameter,
// scanning nested maps using recursion.
func recGetMessages(raw interface{}, isMapMessage, isInitialCall bool) ([]*Message, error) {
var messages []*Message
var err error

switch data := raw.(type) {
case string:
if isInitialCall {
return nil, errInvalidTranslationFile
}
m, err := NewMessage(data)
return []*Message{m}, err

case map[string]interface{}:
messageFile.Messages = make([]*Message, 0, len(data))
for id, data := range data {
if isMapMessage {
m, err := NewMessage(data)
return []*Message{m}, err
}
messages = make([]*Message, 0, len(data))
for id, data := range data {
// recursively scan map items
messages, err = addChildMessages(id, data, messages)
if err != nil {
return nil, err
}
m.ID = id
messageFile.Messages = append(messageFile.Messages, m)
}

case map[interface{}]interface{}:
messageFile.Messages = make([]*Message, 0, len(data))
if isMapMessage {
m, err := NewMessage(data)
return []*Message{m}, err
}
messages = make([]*Message, 0, len(data))
for id, data := range data {
strid, ok := id.(string)
if !ok {
return nil, fmt.Errorf("expected key to be string but got %#v", id)
}
m, err := NewMessage(data)
// recursively scan map items
messages, err = addChildMessages(strid, data, messages)
if err != nil {
return nil, err
}
m.ID = strid
messageFile.Messages = append(messageFile.Messages, m)
}

case []interface{}:
// Backward compatibility for v1 file format.
messageFile.Messages = make([]*Message, 0, len(data))
messages = make([]*Message, 0, len(data))
for _, data := range data {
m, err := NewMessage(data)
// recursively scan slice items
childMessages, err := recGetMessages(data, isMessage(data), false)
if err != nil {
return nil, err
}
messageFile.Messages = append(messageFile.Messages, m)
messages = append(messages, childMessages...)
}

default:
return nil, fmt.Errorf("unsupported file format %T", raw)
}
return messageFile, nil

return messages, nil
}

func addChildMessages(id string, data interface{}, messages []*Message) ([]*Message, error) {
isChildMessage := isMessage(data)
childMessages, err := recGetMessages(data, isChildMessage, false)
if err != nil {
return nil, err
}
for _, m := range childMessages {
if isChildMessage {
if m.ID == "" {
m.ID = id // start with innermost key
}
} else {
m.ID = id + nestedSeparator + m.ID // update ID with each nested key on the way
}
messages = append(messages, m)
}
return messages, nil
}

func parsePath(path string) (langTag, format string) {
Expand Down
Loading

0 comments on commit 8b3465d

Please sign in to comment.