Add API Framework Revel Source Files
[iec.git] / src / foundation / api / revel / i18n.go
1 // Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
2 // Revel Framework source code and usage is governed by a MIT style
3 // license that can be found in the LICENSE file.
4
5 package revel
6
7 import (
8         "fmt"
9         "html/template"
10         "os"
11         "path/filepath"
12         "regexp"
13         "strings"
14
15         "github.com/revel/config"
16 )
17
18 const (
19         // CurrentLocaleViewArg the key for the current locale view arg value
20         CurrentLocaleViewArg = "currentLocale"
21
22         messageFilesDirectory  = "messages"
23         messageFilePattern     = `^\w+\.[a-zA-Z]{2}$`
24         defaultUnknownFormat   = "??? %s ???"
25         unknownFormatConfigKey = "i18n.unknown_format"
26         defaultLanguageOption  = "i18n.default_language"
27         localeCookieConfigKey  = "i18n.cookie"
28 )
29
30 var (
31         // All currently loaded message configs.
32         messages            map[string]*config.Config
33         localeParameterName string
34         i18nLog             = RevelLog.New("section", "i18n")
35 )
36
37 // MessageFunc allows you to override the translation interface.
38 //
39 // Set this to your own function that translates to the current locale.
40 // This allows you to set up your own loading and logging of translated texts.
41 //
42 // See Message(...) in i18n.go for example of function.
43 var MessageFunc = Message
44
45 // MessageLanguages returns all currently loaded message languages.
46 func MessageLanguages() []string {
47         languages := make([]string, len(messages))
48         i := 0
49         for language := range messages {
50                 languages[i] = language
51                 i++
52         }
53         return languages
54 }
55
56 // Message performs a message look-up for the given locale and message using the given arguments.
57 //
58 // When either an unknown locale or message is detected, a specially formatted string is returned.
59 func Message(locale, message string, args ...interface{}) string {
60         language, region := parseLocale(locale)
61         unknownValueFormat := getUnknownValueFormat()
62
63         messageConfig, knownLanguage := messages[language]
64         if !knownLanguage {
65                 i18nLog.Debugf("Unsupported language for locale '%s' and message '%s', trying default language", locale, message)
66
67                 if defaultLanguage, found := Config.String(defaultLanguageOption); found {
68                         i18nLog.Debugf("Using default language '%s'", defaultLanguage)
69
70                         messageConfig, knownLanguage = messages[defaultLanguage]
71                         if !knownLanguage {
72                                 i18nLog.Debugf("Unsupported default language for locale '%s' and message '%s'", defaultLanguage, message)
73                                 return fmt.Sprintf(unknownValueFormat, message)
74                         }
75                 } else {
76                         i18nLog.Warnf("Unable to find default language option (%s); messages for unsupported locales will never be translated", defaultLanguageOption)
77                         return fmt.Sprintf(unknownValueFormat, message)
78                 }
79         }
80
81         // This works because unlike the goconfig documentation suggests it will actually
82         // try to resolve message in DEFAULT if it did not find it in the given section.
83         value, err := messageConfig.String(region, message)
84         if err != nil {
85                 i18nLog.Warnf("Unknown message '%s' for locale '%s'", message, locale)
86                 return fmt.Sprintf(unknownValueFormat, message)
87         }
88
89         if len(args) > 0 {
90                 i18nLog.Debugf("Arguments detected, formatting '%s' with %v", value, args)
91                 safeArgs := make([]interface{}, 0, len(args))
92                 for _, arg := range args {
93                         switch a := arg.(type) {
94                         case template.HTML:
95                                 safeArgs = append(safeArgs, a)
96                         case string:
97                                 safeArgs = append(safeArgs, template.HTML(template.HTMLEscapeString(a)))
98                         default:
99                                 safeArgs = append(safeArgs, a)
100                         }
101                 }
102                 value = fmt.Sprintf(value, safeArgs...)
103         }
104
105         return value
106 }
107
108 func parseLocale(locale string) (language, region string) {
109         if strings.Contains(locale, "-") {
110                 languageAndRegion := strings.Split(locale, "-")
111                 return languageAndRegion[0], languageAndRegion[1]
112         }
113
114         return locale, ""
115 }
116
117 // Retrieve message format or default format when i18n message is missing.
118 func getUnknownValueFormat() string {
119         return Config.StringDefault(unknownFormatConfigKey, defaultUnknownFormat)
120 }
121
122 // Recursively read and cache all available messages from all message files on the given path.
123 func loadMessages(path string) {
124         messages = make(map[string]*config.Config)
125
126         // Read in messages from the modules. Load the module messges first,
127         // so that it can be override in parent application
128         for _, module := range Modules {
129                 i18nLog.Debug("Importing messages from module:", "importpath", module.ImportPath)
130                 if err := Walk(filepath.Join(module.Path, messageFilesDirectory), loadMessageFile); err != nil &&
131                         !os.IsNotExist(err) {
132                         i18nLog.Error("Error reading messages files from module:", "error", err)
133                 }
134         }
135
136         if err := Walk(path, loadMessageFile); err != nil && !os.IsNotExist(err) {
137                 i18nLog.Error("Error reading messages files:", "error", err)
138         }
139 }
140
141 // Load a single message file
142 func loadMessageFile(path string, info os.FileInfo, osError error) error {
143         if osError != nil {
144                 return osError
145         }
146         if info.IsDir() {
147                 return nil
148         }
149
150         if matched, _ := regexp.MatchString(messageFilePattern, info.Name()); matched {
151                 messageConfig, err := parseMessagesFile(path)
152                 if err != nil {
153                         return err
154                 }
155                 locale := parseLocaleFromFileName(info.Name())
156
157                 // If we have already parsed a message file for this locale, merge both
158                 if _, exists := messages[locale]; exists {
159                         messages[locale].Merge(messageConfig)
160                         i18nLog.Debugf("Successfully merged messages for locale '%s'", locale)
161                 } else {
162                         messages[locale] = messageConfig
163                 }
164
165                 i18nLog.Debug("Successfully loaded messages from file", "file", info.Name())
166         } else {
167                 i18nLog.Warn("Ignoring file because it did not have a valid extension", "file", info.Name())
168         }
169
170         return nil
171 }
172
173 func parseMessagesFile(path string) (messageConfig *config.Config, err error) {
174         messageConfig, err = config.ReadDefault(path)
175         return
176 }
177
178 func parseLocaleFromFileName(file string) string {
179         extension := filepath.Ext(file)[1:]
180         return strings.ToLower(extension)
181 }
182
183 func init() {
184         OnAppStart(func() {
185                 loadMessages(filepath.Join(BasePath, messageFilesDirectory))
186                 localeParameterName = Config.StringDefault("i18n.locale.parameter", "")
187         }, 0)
188 }
189
190 func I18nFilter(c *Controller, fc []Filter) {
191         foundLocale := false
192         // Search for a parameter first
193         if localeParameterName != "" {
194                 if locale, found := c.Params.Values[localeParameterName]; found && len(locale[0]) > 0 {
195                         setCurrentLocaleControllerArguments(c, locale[0])
196                         foundLocale = true
197                         i18nLog.Debug("Found locale parameter value: ", "locale", locale[0])
198                 }
199         }
200         if !foundLocale {
201                 if foundCookie, cookieValue := hasLocaleCookie(c.Request); foundCookie {
202                         i18nLog.Debug("Found locale cookie value: ", "cookie", cookieValue)
203                         setCurrentLocaleControllerArguments(c, cookieValue)
204                 } else if foundHeader, headerValue := hasAcceptLanguageHeader(c.Request); foundHeader {
205                         i18nLog.Debug("Found Accept-Language header value: ", "header", headerValue)
206                         setCurrentLocaleControllerArguments(c, headerValue)
207                 } else {
208                         i18nLog.Debug("Unable to find locale in cookie or header, using empty string")
209                         setCurrentLocaleControllerArguments(c, "")
210                 }
211         }
212         fc[0](c, fc[1:])
213 }
214
215 // Set the current locale controller argument (CurrentLocaleControllerArg) with the given locale.
216 func setCurrentLocaleControllerArguments(c *Controller, locale string) {
217         c.Request.Locale = locale
218         c.ViewArgs[CurrentLocaleViewArg] = locale
219 }
220
221 // Determine whether the given request has valid Accept-Language value.
222 //
223 // Assumes that the accept languages stored in the request are sorted according to quality, with top
224 // quality first in the slice.
225 func hasAcceptLanguageHeader(request *Request) (bool, string) {
226         if request.AcceptLanguages != nil && len(request.AcceptLanguages) > 0 {
227                 return true, request.AcceptLanguages[0].Language
228         }
229
230         return false, ""
231 }
232
233 // Determine whether the given request has a valid language cookie value.
234 func hasLocaleCookie(request *Request) (bool, string) {
235         if request != nil {
236                 name := Config.StringDefault(localeCookieConfigKey, CookiePrefix+"_LANG")
237                 cookie, err := request.Cookie(name)
238                 if err == nil {
239                         return true, cookie.GetValue()
240                 }
241                 i18nLog.Debug("Unable to read locale cookie ", "name", name, "error", err)
242         }
243
244         return false, ""
245 }