/ lokalize2android.go
lokalize2android.go
  1  package main
  2  
  3  import (
  4  	"bufio"
  5  	"bytes"
  6  	"encoding/json"
  7  	"encoding/xml"
  8  	"fmt"
  9  	"io"
 10  	"log"
 11  	"os"
 12  	"strings"
 13  	"text/scanner"
 14  )
 15  
 16  type Resources struct {
 17  	Strings      []String      `xml:"string"`
 18  	StringArrays []StringArray `xml:"string-array"`
 19  	Plurals      []Plurals     `xml:"plural"`
 20  
 21  	XMLName struct{} `xml:"resources"`
 22  }
 23  
 24  type String struct {
 25  	Name  string `xml:"name,attr"`
 26  	Value string `xml:",innerxml"`
 27  }
 28  
 29  type StringArray struct {
 30  	Name  string   `xml:"name,attr"`
 31  	Items []string `xml:"item"`
 32  }
 33  
 34  type Plurals struct {
 35  	Name  string       `xml:"name,attr"`
 36  	Items []PluralItem `xml:"item"`
 37  }
 38  
 39  type PluralItem struct {
 40  	Quantity string `xml:"quantity,attr"`
 41  	Value    string `xml:",innerxml"`
 42  }
 43  
 44  var pluralTypes = []string{"zero", "one", "two", "few", "many", "other"}
 45  
 46  func (l *Resources) UnmarshalJSON(b []byte) error {
 47  	var ts map[string]interface{}
 48  	if err := json.Unmarshal(b, &ts); err != nil {
 49  		return err
 50  	}
 51  
 52  	for k, v := range ts {
 53  		switch v.(type) {
 54  		case string:
 55  			str := String{Name: processKey(k), Value: processTranslation(v.(string))}
 56  			l.Strings = append(l.Strings, str)
 57  		case []interface{}:
 58  			strs := StringArray{Name: processKey(k)}
 59  			for _, str := range v.([]interface{}) {
 60  				strs.Items = append(strs.Items, processTranslation(str.(string)))
 61  			}
 62  			l.StringArrays = append(l.StringArrays, strs)
 63  		case map[string]interface{}:
 64  			pl := Plurals{Name: processKey(k)}
 65  			for _, pt := range pluralTypes {
 66  				if str, ok := v.(map[string]interface{})[pt]; ok {
 67  					pl.Items = append(pl.Items, PluralItem{Quantity: pt, Value: processTranslation(str.(string))})
 68  				}
 69  			}
 70  			l.Plurals = append(l.Plurals, pl)
 71  		default:
 72  			return fmt.Errorf("can't handle %q: %q", k, v)
 73  		}
 74  	}
 75  
 76  	return nil
 77  }
 78  
 79  func processKey(k string) string {
 80  	return strings.ReplaceAll(k, "-", "_")
 81  }
 82  
 83  // processTranslation transforms {{ and }} into XML surrounding tags for placeholders.
 84  func processTranslation(v string) string {
 85  	var (
 86  		s  scanner.Scanner
 87  		b  bytes.Buffer
 88  		ph bytes.Buffer
 89  	)
 90  
 91  	s.Init(strings.NewReader(v))
 92  	s.Filename = "translation"
 93  	s.Whitespace = 0
 94  	s.Mode = scanner.ScanStrings
 95  
 96  	insideph := false
 97  	lastOpen := -1
 98  	lastClosed := -1
 99  	for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
100  		switch tok {
101  		case '{':
102  			if 1 == s.Pos().Column - lastOpen{
103  				insideph = true
104  			}
105  			lastOpen = s.Pos().Column
106  		case '}':
107  			if 1 == s.Pos().Column - lastClosed {
108  				b.WriteString(fmt.Sprintf(`<xliff:g id="%s" />`, ph.String()))
109  				ph.Reset()
110  				insideph = false
111  			}
112  			lastClosed = s.Pos().Column
113  		default:
114  			if !insideph {
115  				b.WriteRune(tok)
116  			} else {
117  				ph.WriteRune(tok)
118  			}
119  		}
120  	}
121  	return b.String()
122  }
123  
124  func main() {
125  	l := log.New(os.Stderr, "", log.Flags())
126  
127  	var r io.Reader
128  	if len(os.Args) == 2 {
129  		f, err := os.Open(os.Args[1])
130  		if err != nil {
131  			l.Fatalf("error reading file: %q", err)
132  		}
133  
134  		defer func() {
135  			err = f.Close()
136  			if err != nil {
137  				l.Fatalf("error closing file: %q", err)
138  			}
139  		}()
140  
141  		r = f
142  	} else {
143  		r = bufio.NewReader(os.Stdin)
144  	}
145  
146  	var rs Resources
147  	if err := json.NewDecoder(r).Decode(&rs); err != nil {
148  		l.Fatalf("error parsing json: %q", err)
149  	}
150  
151  	b, err := xml.MarshalIndent(rs, "", "\t")
152  	if err != nil {
153  		l.Fatalf("error serializing xml: %q", err)
154  	}
155  	fmt.Println(string(b))
156  }