catalog_test.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. // Copyright 2017 The Go Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. package catalog
  5. import (
  6. "bytes"
  7. "path"
  8. "reflect"
  9. "strings"
  10. "testing"
  11. "golang.org/x/text/internal/catmsg"
  12. "golang.org/x/text/language"
  13. )
  14. type entry struct {
  15. tag, key string
  16. msg interface{}
  17. }
  18. func langs(s string) []language.Tag {
  19. t, _, _ := language.ParseAcceptLanguage(s)
  20. return t
  21. }
  22. type testCase struct {
  23. desc string
  24. cat []entry
  25. lookup []entry
  26. fallback string
  27. match []string
  28. tags []language.Tag
  29. }
  30. var testCases = []testCase{{
  31. desc: "empty catalog",
  32. lookup: []entry{
  33. {"en", "key", ""},
  34. {"en", "", ""},
  35. {"nl", "", ""},
  36. },
  37. match: []string{
  38. "gr -> und",
  39. "en-US -> und",
  40. "af -> und",
  41. },
  42. tags: nil, // not an empty list.
  43. }, {
  44. desc: "one entry",
  45. cat: []entry{
  46. {"en", "hello", "Hello!"},
  47. },
  48. lookup: []entry{
  49. {"und", "hello", ""},
  50. {"nl", "hello", ""},
  51. {"en", "hello", "Hello!"},
  52. {"en-US", "hello", "Hello!"},
  53. {"en-GB", "hello", "Hello!"},
  54. {"en-oxendict", "hello", "Hello!"},
  55. {"en-oxendict-u-ms-metric", "hello", "Hello!"},
  56. },
  57. match: []string{
  58. "gr -> en",
  59. "en-US -> en-u-rg-uszzzz",
  60. },
  61. tags: langs("en"),
  62. }, {
  63. desc: "hierarchical languages",
  64. cat: []entry{
  65. {"en", "hello", "Hello!"},
  66. {"en-GB", "hello", "Hellø!"},
  67. {"en-US", "hello", "Howdy!"},
  68. {"en", "greetings", "Greetings!"},
  69. {"gsw", "hello", "Grüetzi!"},
  70. },
  71. lookup: []entry{
  72. {"und", "hello", ""},
  73. {"nl", "hello", ""},
  74. {"en", "hello", "Hello!"},
  75. {"en-US", "hello", "Howdy!"},
  76. {"en-GB", "hello", "Hellø!"},
  77. {"en-oxendict", "hello", "Hello!"},
  78. {"en-US-oxendict-u-ms-metric", "hello", "Howdy!"},
  79. {"und", "greetings", ""},
  80. {"nl", "greetings", ""},
  81. {"en", "greetings", "Greetings!"},
  82. {"en-US", "greetings", "Greetings!"},
  83. {"en-GB", "greetings", "Greetings!"},
  84. {"en-oxendict", "greetings", "Greetings!"},
  85. {"en-US-oxendict-u-ms-metric", "greetings", "Greetings!"},
  86. },
  87. fallback: "gsw",
  88. match: []string{
  89. "gr -> gsw",
  90. "en-US -> en-US",
  91. },
  92. tags: langs("gsw, en, en-GB, en-US"),
  93. }, {
  94. desc: "variables",
  95. cat: []entry{
  96. {"en", "hello %s", []Message{
  97. Var("person", String("Jane")),
  98. String("Hello ${person}!"),
  99. }},
  100. {"en", "hello error", []Message{
  101. Var("person", String("Jane")),
  102. noMatchMessage{}, // trigger sequence path.
  103. String("Hello ${person."),
  104. }},
  105. {"en", "fallback to var value", []Message{
  106. Var("you", noMatchMessage{}, noMatchMessage{}),
  107. String("Hello ${you}."),
  108. }},
  109. {"en", "scopes", []Message{
  110. Var("person1", String("Mark")),
  111. Var("person2", String("Jane")),
  112. Var("couple",
  113. Var("person1", String("Joe")),
  114. String("${person1} and ${person2}")),
  115. String("Hello ${couple}."),
  116. }},
  117. {"en", "missing var", String("Hello ${missing}.")},
  118. },
  119. lookup: []entry{
  120. {"en", "hello %s", "Hello Jane!"},
  121. {"en", "hello error", "Hello $!(MISSINGBRACE)"},
  122. {"en", "fallback to var value", "Hello you."},
  123. {"en", "scopes", "Hello Joe and Jane."},
  124. {"en", "missing var", "Hello missing."},
  125. },
  126. tags: langs("en"),
  127. }, {
  128. desc: "macros",
  129. cat: []entry{
  130. {"en", "macro1", String("Hello ${macro1(1)}.")},
  131. {"en", "macro2", String("Hello ${ macro1(2) }!")},
  132. {"en", "macroWS", String("Hello ${ macro1( 2 ) }!")},
  133. {"en", "missing", String("Hello ${ missing(1 }.")},
  134. {"en", "badnum", String("Hello ${ badnum(1b) }.")},
  135. {"en", "undefined", String("Hello ${ undefined(1) }.")},
  136. {"en", "macroU", String("Hello ${ macroU(2) }!")},
  137. },
  138. lookup: []entry{
  139. {"en", "macro1", "Hello Joe."},
  140. {"en", "macro2", "Hello Joe!"},
  141. {"en-US", "macroWS", "Hello Joe!"},
  142. {"en-NL", "missing", "Hello $!(MISSINGPAREN)."},
  143. {"en", "badnum", "Hello $!(BADNUM)."},
  144. {"en", "undefined", "Hello undefined."},
  145. {"en", "macroU", "Hello macroU!"},
  146. },
  147. tags: langs("en"),
  148. }}
  149. func setMacros(b *Builder) {
  150. b.SetMacro(language.English, "macro1", String("Joe"))
  151. b.SetMacro(language.Und, "macro2", String("${macro1(1)}"))
  152. b.SetMacro(language.English, "macroU", noMatchMessage{})
  153. }
  154. type buildFunc func(t *testing.T, tc testCase) Catalog
  155. func initBuilder(t *testing.T, tc testCase) Catalog {
  156. options := []Option{}
  157. if tc.fallback != "" {
  158. options = append(options, Fallback(language.MustParse(tc.fallback)))
  159. }
  160. cat := NewBuilder(options...)
  161. for _, e := range tc.cat {
  162. tag := language.MustParse(e.tag)
  163. switch msg := e.msg.(type) {
  164. case string:
  165. cat.SetString(tag, e.key, msg)
  166. case Message:
  167. cat.Set(tag, e.key, msg)
  168. case []Message:
  169. cat.Set(tag, e.key, msg...)
  170. }
  171. }
  172. setMacros(cat)
  173. return cat
  174. }
  175. type dictionary map[string]string
  176. func (d dictionary) Lookup(key string) (data string, ok bool) {
  177. data, ok = d[key]
  178. return data, ok
  179. }
  180. func initCatalog(t *testing.T, tc testCase) Catalog {
  181. m := map[string]Dictionary{}
  182. for _, e := range tc.cat {
  183. m[e.tag] = dictionary{}
  184. }
  185. for _, e := range tc.cat {
  186. var msg Message
  187. switch x := e.msg.(type) {
  188. case string:
  189. msg = String(x)
  190. case Message:
  191. msg = x
  192. case []Message:
  193. msg = firstInSequence(x)
  194. }
  195. data, _ := catmsg.Compile(language.MustParse(e.tag), nil, msg)
  196. m[e.tag].(dictionary)[e.key] = data
  197. }
  198. options := []Option{}
  199. if tc.fallback != "" {
  200. options = append(options, Fallback(language.MustParse(tc.fallback)))
  201. }
  202. c, err := NewFromMap(m, options...)
  203. if err != nil {
  204. t.Fatal(err)
  205. }
  206. // TODO: implement macros for fixed catalogs.
  207. b := NewBuilder()
  208. setMacros(b)
  209. c.(*catalog).macros.index = b.macros.index
  210. return c
  211. }
  212. func TestMatcher(t *testing.T) {
  213. test := func(t *testing.T, init buildFunc) {
  214. for _, tc := range testCases {
  215. for _, s := range tc.match {
  216. a := strings.Split(s, "->")
  217. t.Run(path.Join(tc.desc, a[0]), func(t *testing.T) {
  218. cat := init(t, tc)
  219. got, _ := language.MatchStrings(cat.Matcher(), a[0])
  220. want := language.MustParse(strings.TrimSpace(a[1]))
  221. if got != want {
  222. t.Errorf("got %q; want %q", got, want)
  223. }
  224. })
  225. }
  226. }
  227. }
  228. t.Run("Builder", func(t *testing.T) { test(t, initBuilder) })
  229. t.Run("Catalog", func(t *testing.T) { test(t, initCatalog) })
  230. }
  231. func TestCatalog(t *testing.T) {
  232. test := func(t *testing.T, init buildFunc) {
  233. for _, tc := range testCases {
  234. cat := init(t, tc)
  235. wantTags := tc.tags
  236. if got := cat.Languages(); !reflect.DeepEqual(got, wantTags) {
  237. t.Errorf("%s:Languages: got %v; want %v", tc.desc, got, wantTags)
  238. }
  239. for _, e := range tc.lookup {
  240. t.Run(path.Join(tc.desc, e.tag, e.key), func(t *testing.T) {
  241. tag := language.MustParse(e.tag)
  242. buf := testRenderer{}
  243. ctx := cat.Context(tag, &buf)
  244. want := e.msg.(string)
  245. err := ctx.Execute(e.key)
  246. gotFound := err != ErrNotFound
  247. wantFound := want != ""
  248. if gotFound != wantFound {
  249. t.Fatalf("err: got %v (%v); want %v", gotFound, err, wantFound)
  250. }
  251. if got := buf.buf.String(); got != want {
  252. t.Errorf("Lookup:\ngot %q\nwant %q", got, want)
  253. }
  254. })
  255. }
  256. }
  257. }
  258. t.Run("Builder", func(t *testing.T) { test(t, initBuilder) })
  259. t.Run("Catalog", func(t *testing.T) { test(t, initCatalog) })
  260. }
  261. type testRenderer struct {
  262. buf bytes.Buffer
  263. }
  264. func (f *testRenderer) Arg(i int) interface{} { return nil }
  265. func (f *testRenderer) Render(s string) { f.buf.WriteString(s) }
  266. var msgNoMatch = catmsg.Register("no match", func(d *catmsg.Decoder) bool {
  267. return false // no match
  268. })
  269. type noMatchMessage struct{}
  270. func (noMatchMessage) Compile(e *catmsg.Encoder) error {
  271. e.EncodeMessageType(msgNoMatch)
  272. return catmsg.ErrIncomplete
  273. }