i18n.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. // i18n is a simple package that translates strings using a language map.
  2. // It mimicks some functionality of the vue-i18n library so that the same JSON
  3. // language map may be used in the JS frontent and the Go backend.
  4. package i18n
  5. import (
  6. "encoding/json"
  7. "errors"
  8. "regexp"
  9. "strings"
  10. )
  11. // I18n offers translation functions over a language map.
  12. type I18n struct {
  13. code string `json:"code"`
  14. name string `json:"name"`
  15. langMap map[string]string
  16. }
  17. var reParam = regexp.MustCompile(`(?i)\{([a-z0-9-.]+)\}`)
  18. // New returns an I18n instance.
  19. func New(b []byte) (*I18n, error) {
  20. var l map[string]string
  21. if err := json.Unmarshal(b, &l); err != nil {
  22. return nil, err
  23. }
  24. code, ok := l["_.code"]
  25. if !ok {
  26. return nil, errors.New("missing _.code field in language file")
  27. }
  28. name, ok := l["_.name"]
  29. if !ok {
  30. return nil, errors.New("missing _.name field in language file")
  31. }
  32. return &I18n{
  33. langMap: l,
  34. code: code,
  35. name: name,
  36. }, nil
  37. }
  38. // Load loads a JSON language map into the instance overwriting
  39. // existing keys that conflict.
  40. func (i *I18n) Load(b []byte) error {
  41. var l map[string]string
  42. if err := json.Unmarshal(b, &l); err != nil {
  43. return err
  44. }
  45. for k, v := range l {
  46. i.langMap[k] = v
  47. }
  48. return nil
  49. }
  50. // Name returns the canonical name of the language.
  51. func (i *I18n) Name() string {
  52. return i.name
  53. }
  54. // Code returns the ISO code of the language.
  55. func (i *I18n) Code() string {
  56. return i.code
  57. }
  58. // JSON returns the languagemap as raw JSON.
  59. func (i *I18n) JSON() []byte {
  60. b, _ := json.Marshal(i.langMap)
  61. return b
  62. }
  63. // T returns the translation for the given key similar to vue i18n's t().
  64. func (i *I18n) T(key string) string {
  65. s, ok := i.langMap[key]
  66. if !ok {
  67. return key
  68. }
  69. return i.getSingular(s)
  70. }
  71. // Ts returns the translation for the given key similar to vue i18n's t()
  72. // and subsitutes the params in the given map in the translated value.
  73. // In the language values, the substitutions are represented as: {key}
  74. // The params and values are received as a pairs of succeeding strings.
  75. // That is, the number of these arguments should be an even number.
  76. // eg: Ts("globals.message.notFound",
  77. // "name", "campaigns",
  78. // "error", err)
  79. func (i *I18n) Ts(key string, params ...string) string {
  80. if len(params)%2 != 0 {
  81. return key + `: Invalid arguments`
  82. }
  83. s, ok := i.langMap[key]
  84. if !ok {
  85. return key
  86. }
  87. s = i.getSingular(s)
  88. for n := 0; n < len(params); n += 2 {
  89. // If there are {params} in the param values, substitute them.
  90. val := i.subAllParams(params[n+1])
  91. s = strings.ReplaceAll(s, `{`+params[n]+`}`, val)
  92. }
  93. return s
  94. }
  95. // Tc returns the translation for the given key similar to vue i18n's tc().
  96. // It expects the language string in the map to be of the form `Singular | Plural` and
  97. // returns `Plural` if n > 1, or `Singular` otherwise.
  98. func (i *I18n) Tc(key string, n int) string {
  99. s, ok := i.langMap[key]
  100. if !ok {
  101. return key
  102. }
  103. // Plural.
  104. if n > 1 {
  105. return i.getPlural(s)
  106. }
  107. return i.getSingular(s)
  108. }
  109. // getSingular returns the singular term from the vuei18n pipe separated value.
  110. // singular term | plural term
  111. func (i *I18n) getSingular(s string) string {
  112. if !strings.Contains(s, "|") {
  113. return s
  114. }
  115. return strings.TrimSpace(strings.Split(s, "|")[0])
  116. }
  117. // getSingular returns the plural term from the vuei18n pipe separated value.
  118. // singular term | plural term
  119. func (i *I18n) getPlural(s string) string {
  120. if !strings.Contains(s, "|") {
  121. return s
  122. }
  123. chunks := strings.Split(s, "|")
  124. if len(chunks) == 2 {
  125. return strings.TrimSpace(chunks[1])
  126. }
  127. return strings.TrimSpace(chunks[0])
  128. }
  129. // subAllParams recursively resolves and replaces all {params} in a string.
  130. func (i *I18n) subAllParams(s string) string {
  131. if !strings.Contains(s, `{`) {
  132. return s
  133. }
  134. parts := reParam.FindAllStringSubmatch(s, -1)
  135. if len(parts) < 1 {
  136. return s
  137. }
  138. for _, p := range parts {
  139. s = strings.ReplaceAll(s, p[0], i.T(p[1]))
  140. }
  141. return i.subAllParams(s)
  142. }