1package ut
2
3import (
4	"fmt"
5	"strconv"
6	"strings"
7
8	"github.com/go-playground/locales"
9)
10
11const (
12	paramZero          = "{0}"
13	paramOne           = "{1}"
14	unknownTranslation = ""
15)
16
17// Translator is universal translators
18// translator instance which is a thin wrapper
19// around locales.Translator instance providing
20// some extra functionality
21type Translator interface {
22	locales.Translator
23
24	// adds a normal translation for a particular language/locale
25	// {#} is the only replacement type accepted and are ad infinitum
26	// eg. one: '{0} day left' other: '{0} days left'
27	Add(key interface{}, text string, override bool) error
28
29	// adds a cardinal plural translation for a particular language/locale
30	// {0} is the only replacement type accepted and only one variable is accepted as
31	// multiple cannot be used for a plural rule determination, unless it is a range;
32	// see AddRange below.
33	// eg. in locale 'en' one: '{0} day left' other: '{0} days left'
34	AddCardinal(key interface{}, text string, rule locales.PluralRule, override bool) error
35
36	// adds an ordinal plural translation for a particular language/locale
37	// {0} is the only replacement type accepted and only one variable is accepted as
38	// multiple cannot be used for a plural rule determination, unless it is a range;
39	// see AddRange below.
40	// eg. in locale 'en' one: '{0}st day of spring' other: '{0}nd day of spring'
41	// - 1st, 2nd, 3rd...
42	AddOrdinal(key interface{}, text string, rule locales.PluralRule, override bool) error
43
44	// adds a range plural translation for a particular language/locale
45	// {0} and {1} are the only replacement types accepted and only these are accepted.
46	// eg. in locale 'nl' one: '{0}-{1} day left' other: '{0}-{1} days left'
47	AddRange(key interface{}, text string, rule locales.PluralRule, override bool) error
48
49	// creates the translation for the locale given the 'key' and params passed in
50	T(key interface{}, params ...string) (string, error)
51
52	// creates the cardinal translation for the locale given the 'key', 'num' and 'digit' arguments
53	//  and param passed in
54	C(key interface{}, num float64, digits uint64, param string) (string, error)
55
56	// creates the ordinal translation for the locale given the 'key', 'num' and 'digit' arguments
57	// and param passed in
58	O(key interface{}, num float64, digits uint64, param string) (string, error)
59
60	//  creates the range translation for the locale given the 'key', 'num1', 'digit1', 'num2' and
61	//  'digit2' arguments and 'param1' and 'param2' passed in
62	R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) (string, error)
63
64	// VerifyTranslations checks to ensures that no plural rules have been
65	// missed within the translations.
66	VerifyTranslations() error
67}
68
69var _ Translator = new(translator)
70var _ locales.Translator = new(translator)
71
72type translator struct {
73	locales.Translator
74	translations        map[interface{}]*transText
75	cardinalTanslations map[interface{}][]*transText // array index is mapped to locales.PluralRule index + the locales.PluralRuleUnknown
76	ordinalTanslations  map[interface{}][]*transText
77	rangeTanslations    map[interface{}][]*transText
78}
79
80type transText struct {
81	text    string
82	indexes []int
83}
84
85func newTranslator(trans locales.Translator) Translator {
86	return &translator{
87		Translator:          trans,
88		translations:        make(map[interface{}]*transText), // translation text broken up by byte index
89		cardinalTanslations: make(map[interface{}][]*transText),
90		ordinalTanslations:  make(map[interface{}][]*transText),
91		rangeTanslations:    make(map[interface{}][]*transText),
92	}
93}
94
95// Add adds a normal translation for a particular language/locale
96// {#} is the only replacement type accepted and are ad infinitum
97// eg. one: '{0} day left' other: '{0} days left'
98func (t *translator) Add(key interface{}, text string, override bool) error {
99
100	if _, ok := t.translations[key]; ok && !override {
101		return &ErrConflictingTranslation{locale: t.Locale(), key: key, text: text}
102	}
103
104	lb := strings.Count(text, "{")
105	rb := strings.Count(text, "}")
106
107	if lb != rb {
108		return &ErrMissingBracket{locale: t.Locale(), key: key, text: text}
109	}
110
111	trans := &transText{
112		text: text,
113	}
114
115	var idx int
116
117	for i := 0; i < lb; i++ {
118		s := "{" + strconv.Itoa(i) + "}"
119		idx = strings.Index(text, s)
120		if idx == -1 {
121			return &ErrBadParamSyntax{locale: t.Locale(), param: s, key: key, text: text}
122		}
123
124		trans.indexes = append(trans.indexes, idx)
125		trans.indexes = append(trans.indexes, idx+len(s))
126	}
127
128	t.translations[key] = trans
129
130	return nil
131}
132
133// AddCardinal adds a cardinal plural translation for a particular language/locale
134// {0} is the only replacement type accepted and only one variable is accepted as
135// multiple cannot be used for a plural rule determination, unless it is a range;
136// see AddRange below.
137// eg. in locale 'en' one: '{0} day left' other: '{0} days left'
138func (t *translator) AddCardinal(key interface{}, text string, rule locales.PluralRule, override bool) error {
139
140	var verified bool
141
142	// verify plural rule exists for locale
143	for _, pr := range t.PluralsCardinal() {
144		if pr == rule {
145			verified = true
146			break
147		}
148	}
149
150	if !verified {
151		return &ErrCardinalTranslation{text: fmt.Sprintf("error: cardinal plural rule '%s' does not exist for locale '%s' key: '%v' text: '%s'", rule, t.Locale(), key, text)}
152	}
153
154	tarr, ok := t.cardinalTanslations[key]
155	if ok {
156		// verify not adding a conflicting record
157		if len(tarr) > 0 && tarr[rule] != nil && !override {
158			return &ErrConflictingTranslation{locale: t.Locale(), key: key, rule: rule, text: text}
159		}
160
161	} else {
162		tarr = make([]*transText, 7, 7)
163		t.cardinalTanslations[key] = tarr
164	}
165
166	trans := &transText{
167		text:    text,
168		indexes: make([]int, 2, 2),
169	}
170
171	tarr[rule] = trans
172
173	idx := strings.Index(text, paramZero)
174	if idx == -1 {
175		tarr[rule] = nil
176		return &ErrCardinalTranslation{text: fmt.Sprintf("error: parameter '%s' not found, may want to use 'Add' instead of 'AddCardinal'. locale: '%s' key: '%v' text: '%s'", paramZero, t.Locale(), key, text)}
177	}
178
179	trans.indexes[0] = idx
180	trans.indexes[1] = idx + len(paramZero)
181
182	return nil
183}
184
185// AddOrdinal adds an ordinal plural translation for a particular language/locale
186// {0} is the only replacement type accepted and only one variable is accepted as
187// multiple cannot be used for a plural rule determination, unless it is a range;
188// see AddRange below.
189// eg. in locale 'en' one: '{0}st day of spring' other: '{0}nd day of spring' - 1st, 2nd, 3rd...
190func (t *translator) AddOrdinal(key interface{}, text string, rule locales.PluralRule, override bool) error {
191
192	var verified bool
193
194	// verify plural rule exists for locale
195	for _, pr := range t.PluralsOrdinal() {
196		if pr == rule {
197			verified = true
198			break
199		}
200	}
201
202	if !verified {
203		return &ErrOrdinalTranslation{text: fmt.Sprintf("error: ordinal plural rule '%s' does not exist for locale '%s' key: '%v' text: '%s'", rule, t.Locale(), key, text)}
204	}
205
206	tarr, ok := t.ordinalTanslations[key]
207	if ok {
208		// verify not adding a conflicting record
209		if len(tarr) > 0 && tarr[rule] != nil && !override {
210			return &ErrConflictingTranslation{locale: t.Locale(), key: key, rule: rule, text: text}
211		}
212
213	} else {
214		tarr = make([]*transText, 7, 7)
215		t.ordinalTanslations[key] = tarr
216	}
217
218	trans := &transText{
219		text:    text,
220		indexes: make([]int, 2, 2),
221	}
222
223	tarr[rule] = trans
224
225	idx := strings.Index(text, paramZero)
226	if idx == -1 {
227		tarr[rule] = nil
228		return &ErrOrdinalTranslation{text: fmt.Sprintf("error: parameter '%s' not found, may want to use 'Add' instead of 'AddOrdinal'. locale: '%s' key: '%v' text: '%s'", paramZero, t.Locale(), key, text)}
229	}
230
231	trans.indexes[0] = idx
232	trans.indexes[1] = idx + len(paramZero)
233
234	return nil
235}
236
237// AddRange adds a range plural translation for a particular language/locale
238// {0} and {1} are the only replacement types accepted and only these are accepted.
239// eg. in locale 'nl' one: '{0}-{1} day left' other: '{0}-{1} days left'
240func (t *translator) AddRange(key interface{}, text string, rule locales.PluralRule, override bool) error {
241
242	var verified bool
243
244	// verify plural rule exists for locale
245	for _, pr := range t.PluralsRange() {
246		if pr == rule {
247			verified = true
248			break
249		}
250	}
251
252	if !verified {
253		return &ErrRangeTranslation{text: fmt.Sprintf("error: range plural rule '%s' does not exist for locale '%s' key: '%v' text: '%s'", rule, t.Locale(), key, text)}
254	}
255
256	tarr, ok := t.rangeTanslations[key]
257	if ok {
258		// verify not adding a conflicting record
259		if len(tarr) > 0 && tarr[rule] != nil && !override {
260			return &ErrConflictingTranslation{locale: t.Locale(), key: key, rule: rule, text: text}
261		}
262
263	} else {
264		tarr = make([]*transText, 7, 7)
265		t.rangeTanslations[key] = tarr
266	}
267
268	trans := &transText{
269		text:    text,
270		indexes: make([]int, 4, 4),
271	}
272
273	tarr[rule] = trans
274
275	idx := strings.Index(text, paramZero)
276	if idx == -1 {
277		tarr[rule] = nil
278		return &ErrRangeTranslation{text: fmt.Sprintf("error: parameter '%s' not found, are you sure you're adding a Range Translation? locale: '%s' key: '%v' text: '%s'", paramZero, t.Locale(), key, text)}
279	}
280
281	trans.indexes[0] = idx
282	trans.indexes[1] = idx + len(paramZero)
283
284	idx = strings.Index(text, paramOne)
285	if idx == -1 {
286		tarr[rule] = nil
287		return &ErrRangeTranslation{text: fmt.Sprintf("error: parameter '%s' not found, a Range Translation requires two parameters. locale: '%s' key: '%v' text: '%s'", paramOne, t.Locale(), key, text)}
288	}
289
290	trans.indexes[2] = idx
291	trans.indexes[3] = idx + len(paramOne)
292
293	return nil
294}
295
296// T creates the translation for the locale given the 'key' and params passed in
297func (t *translator) T(key interface{}, params ...string) (string, error) {
298
299	trans, ok := t.translations[key]
300	if !ok {
301		return unknownTranslation, ErrUnknowTranslation
302	}
303
304	b := make([]byte, 0, 64)
305
306	var start, end, count int
307
308	for i := 0; i < len(trans.indexes); i++ {
309		end = trans.indexes[i]
310		b = append(b, trans.text[start:end]...)
311		b = append(b, params[count]...)
312		i++
313		start = trans.indexes[i]
314		count++
315	}
316
317	b = append(b, trans.text[start:]...)
318
319	return string(b), nil
320}
321
322// C creates the cardinal translation for the locale given the 'key', 'num' and 'digit' arguments and param passed in
323func (t *translator) C(key interface{}, num float64, digits uint64, param string) (string, error) {
324
325	tarr, ok := t.cardinalTanslations[key]
326	if !ok {
327		return unknownTranslation, ErrUnknowTranslation
328	}
329
330	rule := t.CardinalPluralRule(num, digits)
331
332	trans := tarr[rule]
333
334	b := make([]byte, 0, 64)
335	b = append(b, trans.text[:trans.indexes[0]]...)
336	b = append(b, param...)
337	b = append(b, trans.text[trans.indexes[1]:]...)
338
339	return string(b), nil
340}
341
342// O creates the ordinal translation for the locale given the 'key', 'num' and 'digit' arguments and param passed in
343func (t *translator) O(key interface{}, num float64, digits uint64, param string) (string, error) {
344
345	tarr, ok := t.ordinalTanslations[key]
346	if !ok {
347		return unknownTranslation, ErrUnknowTranslation
348	}
349
350	rule := t.OrdinalPluralRule(num, digits)
351
352	trans := tarr[rule]
353
354	b := make([]byte, 0, 64)
355	b = append(b, trans.text[:trans.indexes[0]]...)
356	b = append(b, param...)
357	b = append(b, trans.text[trans.indexes[1]:]...)
358
359	return string(b), nil
360}
361
362// R creates the range translation for the locale given the 'key', 'num1', 'digit1', 'num2' and 'digit2' arguments
363// and 'param1' and 'param2' passed in
364func (t *translator) R(key interface{}, num1 float64, digits1 uint64, num2 float64, digits2 uint64, param1, param2 string) (string, error) {
365
366	tarr, ok := t.rangeTanslations[key]
367	if !ok {
368		return unknownTranslation, ErrUnknowTranslation
369	}
370
371	rule := t.RangePluralRule(num1, digits1, num2, digits2)
372
373	trans := tarr[rule]
374
375	b := make([]byte, 0, 64)
376	b = append(b, trans.text[:trans.indexes[0]]...)
377	b = append(b, param1...)
378	b = append(b, trans.text[trans.indexes[1]:trans.indexes[2]]...)
379	b = append(b, param2...)
380	b = append(b, trans.text[trans.indexes[3]:]...)
381
382	return string(b), nil
383}
384
385// VerifyTranslations checks to ensures that no plural rules have been
386// missed within the translations.
387func (t *translator) VerifyTranslations() error {
388
389	for k, v := range t.cardinalTanslations {
390
391		for _, rule := range t.PluralsCardinal() {
392
393			if v[rule] == nil {
394				return &ErrMissingPluralTranslation{locale: t.Locale(), translationType: "plural", rule: rule, key: k}
395			}
396		}
397	}
398
399	for k, v := range t.ordinalTanslations {
400
401		for _, rule := range t.PluralsOrdinal() {
402
403			if v[rule] == nil {
404				return &ErrMissingPluralTranslation{locale: t.Locale(), translationType: "ordinal", rule: rule, key: k}
405			}
406		}
407	}
408
409	for k, v := range t.rangeTanslations {
410
411		for _, rule := range t.PluralsRange() {
412
413			if v[rule] == nil {
414				return &ErrMissingPluralTranslation{locale: t.Locale(), translationType: "range", rule: rule, key: k}
415			}
416		}
417	}
418
419	return nil
420}
421