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