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.
15 "github.com/revel/config"
19 // CurrentLocaleViewArg the key for the current locale view arg value
20 CurrentLocaleViewArg = "currentLocale"
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"
31 // All currently loaded message configs.
32 messages map[string]*config.Config
33 localeParameterName string
34 i18nLog = RevelLog.New("section", "i18n")
37 // MessageFunc allows you to override the translation interface.
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.
42 // See Message(...) in i18n.go for example of function.
43 var MessageFunc = Message
45 // MessageLanguages returns all currently loaded message languages.
46 func MessageLanguages() []string {
47 languages := make([]string, len(messages))
49 for language := range messages {
50 languages[i] = language
56 // Message performs a message look-up for the given locale and message using the given arguments.
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()
63 messageConfig, knownLanguage := messages[language]
65 i18nLog.Debugf("Unsupported language for locale '%s' and message '%s', trying default language", locale, message)
67 if defaultLanguage, found := Config.String(defaultLanguageOption); found {
68 i18nLog.Debugf("Using default language '%s'", defaultLanguage)
70 messageConfig, knownLanguage = messages[defaultLanguage]
72 i18nLog.Debugf("Unsupported default language for locale '%s' and message '%s'", defaultLanguage, message)
73 return fmt.Sprintf(unknownValueFormat, message)
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)
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)
85 i18nLog.Warnf("Unknown message '%s' for locale '%s'", message, locale)
86 return fmt.Sprintf(unknownValueFormat, message)
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) {
95 safeArgs = append(safeArgs, a)
97 safeArgs = append(safeArgs, template.HTML(template.HTMLEscapeString(a)))
99 safeArgs = append(safeArgs, a)
102 value = fmt.Sprintf(value, safeArgs...)
108 func parseLocale(locale string) (language, region string) {
109 if strings.Contains(locale, "-") {
110 languageAndRegion := strings.Split(locale, "-")
111 return languageAndRegion[0], languageAndRegion[1]
117 // Retrieve message format or default format when i18n message is missing.
118 func getUnknownValueFormat() string {
119 return Config.StringDefault(unknownFormatConfigKey, defaultUnknownFormat)
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)
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)
136 if err := Walk(path, loadMessageFile); err != nil && !os.IsNotExist(err) {
137 i18nLog.Error("Error reading messages files:", "error", err)
141 // Load a single message file
142 func loadMessageFile(path string, info os.FileInfo, osError error) error {
150 if matched, _ := regexp.MatchString(messageFilePattern, info.Name()); matched {
151 messageConfig, err := parseMessagesFile(path)
155 locale := parseLocaleFromFileName(info.Name())
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)
162 messages[locale] = messageConfig
165 i18nLog.Debug("Successfully loaded messages from file", "file", info.Name())
167 i18nLog.Warn("Ignoring file because it did not have a valid extension", "file", info.Name())
173 func parseMessagesFile(path string) (messageConfig *config.Config, err error) {
174 messageConfig, err = config.ReadDefault(path)
178 func parseLocaleFromFileName(file string) string {
179 extension := filepath.Ext(file)[1:]
180 return strings.ToLower(extension)
185 loadMessages(filepath.Join(BasePath, messageFilesDirectory))
186 localeParameterName = Config.StringDefault("i18n.locale.parameter", "")
190 func I18nFilter(c *Controller, fc []Filter) {
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])
197 i18nLog.Debug("Found locale parameter value: ", "locale", locale[0])
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)
208 i18nLog.Debug("Unable to find locale in cookie or header, using empty string")
209 setCurrentLocaleControllerArguments(c, "")
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
221 // Determine whether the given request has valid Accept-Language value.
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
233 // Determine whether the given request has a valid language cookie value.
234 func hasLocaleCookie(request *Request) (bool, string) {
236 name := Config.StringDefault(localeCookieConfigKey, CookiePrefix+"_LANG")
237 cookie, err := request.Cookie(name)
239 return true, cookie.GetValue()
241 i18nLog.Debug("Unable to read locale cookie ", "name", name, "error", err)