2017-10-11 00:21:54 +02:00
// Package i18n provides internalization and localization via middleware.
// See _examples/miscellaneous/i18n
2017-02-14 04:54:11 +01:00
package i18n
import (
2019-11-19 22:36:18 +01:00
"fmt"
"net/http"
2017-02-14 04:54:11 +01:00
"reflect"
"strings"
2019-11-20 01:35:41 +01:00
"sync"
2017-02-14 04:54:11 +01:00
2017-10-11 00:21:54 +02:00
"github.com/iris-contrib/i18n"
2019-10-25 00:27:02 +02:00
"github.com/kataras/iris/v12/context"
2017-02-14 04:54:11 +01:00
)
2019-11-20 01:35:41 +01:00
// If `Config.Default` is missing and `Config.Languages` or `Config.Alternatives` contains this key then it will set as the default locale,
// no need to be exported(see `Config.Default`).
const defLang = "en-US"
2019-11-19 22:36:18 +01:00
// Config the i18n options.
type Config struct {
// Default set it if you want a default language.
//
// Checked: Configuration state, not at runtime.
Default string
// URLParameter is the name of the url parameter which the language can be indentified,
// e.g. "lang" for ?lang=.
//
// Checked: Serving state, runtime.
URLParameter string
2019-11-20 15:40:28 +01:00
// Cookie is the key of the request cookie which the language can be indentified,
// e.g. "lang".
2019-11-19 22:36:18 +01:00
//
// Checked: Serving state, runtime.
2019-11-20 15:40:28 +01:00
Cookie string
// If SetCookie is true and Cookie field is not empty
2019-11-21 20:50:37 +01:00
// then it will set the cookie to the language found by Context's Value's "lang" key or URLParameter or Cookie or Indentifier.
2019-11-20 15:40:28 +01:00
// Defaults to false.
SetCookie bool
// If Subdomain is true then it will try to map a subdomain
// with a valid language from the language list or alternatives.
Subdomain bool
// Indentifier is a function which the language can be indentified if the above URLParameter and Cookie failed to.
Indentifier func ( context . Context ) string
2017-02-14 04:54:11 +01:00
2019-11-19 22:36:18 +01:00
// Languages is a map[string]string which the key is the language i81n and the value is the file location.
//
// Example of key is: 'en-US'.
// Example of value is: './locales/en-US.ini'.
Languages map [ string ] string
// Alternatives is a language map which if it's filled,
// it tries to associate its keys with a value of "Languages" field when a possible value of "Language" was not present.
// Example of
// Languages: map[string]string{"en-US": "./locales/en-US.ini"} set
// Alternatives: map[string]string{ "en":"en-US", "english": "en-US"}.
Alternatives map [ string ] string
}
2018-11-06 03:36:05 +01:00
2019-11-19 22:36:18 +01:00
// Exists returns true if the language, or something similar
// exists (e.g. en-US maps to en or Alternatives[key] == lang).
// it returns the found name and whether it was able to match something.
func ( c * Config ) Exists ( lang string ) ( string , bool ) {
for k , v := range c . Alternatives {
if k == lang {
lang = v
break
2017-02-14 04:54:11 +01:00
}
2017-07-10 17:32:42 +02:00
}
2017-11-22 00:01:45 +01:00
2019-11-19 22:36:18 +01:00
return i18n . IsExistSimilar ( lang )
2017-02-14 04:54:11 +01:00
}
2019-11-20 01:35:41 +01:00
// all locale files passed, we keep them in order
// to check if a file is already passed by `New` or `NewWrapper`,
// because we don't have a way to check before the appending of
// a locale file and the same locale code can be used more than one to register different file names (at runtime too).
var (
localeFilesSet = make ( map [ string ] struct { } )
localesMutex sync . RWMutex
once sync . Once
)
2019-11-19 22:36:18 +01:00
func ( c * Config ) loadLanguages ( ) {
2017-02-14 04:54:11 +01:00
if len ( c . Languages ) == 0 {
2019-11-19 22:36:18 +01:00
panic ( "field Languages is empty" )
2017-02-14 04:54:11 +01:00
}
2019-11-19 22:36:18 +01:00
for k , v := range c . Alternatives {
if _ , ok := c . Languages [ v ] ; ! ok {
panic ( fmt . Sprintf ( "language alternative '%s' does not map to a valid language '%s'" , k , v ) )
}
}
2019-08-17 09:06:20 +02:00
// load the files
2017-11-22 00:01:45 +01:00
for k , langFileOrFiles := range c . Languages {
// remove all spaces.
langFileOrFiles = strings . Replace ( langFileOrFiles , " " , "" , - 1 )
// note: if only one, then the first element is the "v".
languages := strings . Split ( langFileOrFiles , "," )
for _ , v := range languages { // loop each of the files separated by comma, if any.
if ! strings . HasSuffix ( v , ".ini" ) {
v += ".ini"
}
2019-11-19 22:36:18 +01:00
2019-11-20 01:35:41 +01:00
localesMutex . RLock ( )
_ , exists := localeFilesSet [ v ]
localesMutex . RUnlock ( )
if ! exists {
localesMutex . Lock ( )
err := i18n . SetMessage ( k , v )
// fmt.Printf("add %s = %s\n", k, v)
if err != nil && err != i18n . ErrLangAlreadyExist {
panic ( fmt . Sprintf ( "Failed to set locale file' %s' with error: %v" , k , err ) )
}
localeFilesSet [ v ] = struct { } { }
localesMutex . Unlock ( )
2017-11-22 00:01:45 +01:00
}
2019-11-20 01:35:41 +01:00
2017-02-14 04:54:11 +01:00
}
}
2019-11-20 01:35:41 +01:00
2017-02-14 04:54:11 +01:00
if c . Default == "" {
2019-11-20 01:35:41 +01:00
if lang , ok := c . Exists ( defLang ) ; ok {
c . Default = lang
}
2017-02-14 04:54:11 +01:00
}
2019-11-20 01:35:41 +01:00
once . Do ( func ( ) { // set global default lang once.
// fmt.Printf("set default language: %s\n", c.Default)
i18n . SetDefaultLang ( c . Default )
} )
2019-11-19 22:36:18 +01:00
}
2019-11-20 15:40:28 +01:00
// I18n is the structure which keeps the i18n configuration and implement all Iris i18n features.
type I18n struct {
2019-11-19 22:36:18 +01:00
config Config
}
2019-11-20 15:40:28 +01:00
// NewI18n returns a new i18n middleware which contains
// the middleware itself and a router wrapper.
func NewI18n ( config Config ) * I18n {
config . loadLanguages ( )
return & I18n { config }
2017-02-14 04:54:11 +01:00
}
2019-11-20 15:40:28 +01:00
// Handler returns the middleware handler.
func ( i * I18n ) Handler ( ) context . Handler {
return func ( ctx context . Context ) {
wasByCookie := false
2019-11-19 22:36:18 +01:00
2019-11-20 15:40:28 +01:00
langKey := ctx . Application ( ) . ConfigurationReadOnly ( ) . GetTranslateLanguageContextKey ( )
language := ctx . Values ( ) . GetString ( langKey )
2019-11-19 22:36:18 +01:00
if language == "" {
2019-11-20 15:40:28 +01:00
if i . config . URLParameter != "" {
// try to get by url parameter
language = ctx . URLParam ( i . config . URLParameter )
}
2019-11-19 22:36:18 +01:00
if language == "" {
2019-11-20 15:40:28 +01:00
if i . config . Cookie != "" {
// then try to take the lang field from the cookie
language = ctx . GetCookie ( i . config . Cookie )
wasByCookie = language != ""
}
2019-11-19 22:36:18 +01:00
2019-11-20 15:40:28 +01:00
if language == "" && i . config . Subdomain {
if subdomain := ctx . Subdomain ( ) ; subdomain != "" {
if lang , ok := i . config . Exists ( subdomain ) ; ok {
language = lang
}
}
}
if language == "" {
2019-11-19 22:36:18 +01:00
// try to get by the request headers.
langHeader := ctx . GetHeader ( "Accept-Language" )
if len ( langHeader ) > 0 {
for _ , langEntry := range strings . Split ( langHeader , "," ) {
lc := strings . Split ( langEntry , ";" ) [ 0 ]
2019-11-20 15:40:28 +01:00
if lang , ok := i . config . Exists ( lc ) ; ok {
language = lang
2019-11-19 22:36:18 +01:00
break
}
}
}
}
2019-11-20 15:40:28 +01:00
if language == "" && i . config . Indentifier != nil {
language = i . config . Indentifier ( ctx )
}
2019-11-19 22:36:18 +01:00
}
}
2019-11-20 01:35:41 +01:00
2019-11-20 15:40:28 +01:00
if language == "" {
language = i . config . Default
}
2019-11-19 22:36:18 +01:00
2019-11-20 15:40:28 +01:00
// returns the original key of the language and true
// when the language, or something similar exists (e.g. en-US maps to en).
if lc , ok := i . config . Exists ( language ) ; ok {
language = lc
}
2019-11-19 22:36:18 +01:00
2019-11-20 15:40:28 +01:00
// if it was not taken by the cookie, then set the cookie in order to have it.
if ! wasByCookie && i . config . SetCookie && i . config . Cookie != "" {
ctx . SetCookieKV ( i . config . Cookie , language )
}
2019-11-19 22:36:18 +01:00
2019-11-20 15:40:28 +01:00
ctx . Values ( ) . Set ( langKey , language )
2019-11-19 22:36:18 +01:00
2019-11-20 15:40:28 +01:00
// Set iris.translate and iris.translateLang functions (they can be passed to templates as they are later on).
ctx . Values ( ) . Set ( ctx . Application ( ) . ConfigurationReadOnly ( ) . GetTranslateFunctionContextKey ( ) , getTranslateFunction ( language ) )
// Note: translate (global) language function input argument should match exactly, case-sensitive and "Alternatives" field is not part of the fetch progress.
ctx . Values ( ) . Set ( ctx . Application ( ) . ConfigurationReadOnly ( ) . GetTranslateLangFunctionContextKey ( ) , i18n . Tr )
2019-11-19 22:36:18 +01:00
2019-11-20 15:40:28 +01:00
ctx . Next ( )
2019-11-19 22:36:18 +01:00
}
}
2019-11-20 15:40:28 +01:00
// Wrapper returns a new router wrapper.
2019-11-19 22:36:18 +01:00
// The result function can be passed on `Application.WrapRouter`.
// It compares the path prefix for translated language and
// local redirects the requested path with the selected (from the path) language to the router.
//
2019-11-20 15:40:28 +01:00
// In order this to work as expected, it should be combined with `Application.Use(i.Handler())`
2019-11-19 22:36:18 +01:00
// which registers the i18n middleware itself.
2019-11-20 15:40:28 +01:00
func ( i * I18n ) Wrapper ( ) func ( http . ResponseWriter , * http . Request , http . HandlerFunc ) {
2019-11-19 22:36:18 +01:00
return func ( w http . ResponseWriter , r * http . Request , routerHandler http . HandlerFunc ) {
2019-11-20 15:40:28 +01:00
found := false
2019-11-19 22:36:18 +01:00
path := r . URL . Path [ 1 : ]
2019-11-20 15:40:28 +01:00
if idx := strings . IndexByte ( path , '/' ) ; idx > 0 {
2019-11-19 22:36:18 +01:00
path = path [ : idx ]
}
2019-11-20 01:35:41 +01:00
if path != "" {
2019-11-20 15:40:28 +01:00
if lang , ok := i . config . Exists ( path ) ; ok {
2019-11-20 01:35:41 +01:00
path = r . URL . Path [ len ( path ) + 1 : ]
if path == "" {
path = "/"
}
r . RequestURI = path
r . URL . Path = path
r . Header . Set ( "Accept-Language" , lang )
2019-11-20 15:40:28 +01:00
found = true
}
}
if ! found && i . config . Subdomain {
host := context . GetHost ( r )
if dotIdx := strings . IndexByte ( host , '.' ) ; dotIdx > 0 {
subdomain := host [ 0 : dotIdx ]
if subdomain != "" {
if lang , ok := i . config . Exists ( subdomain ) ; ok {
host = host [ dotIdx + 1 : ]
r . URL . Host = host
r . Host = host
r . Header . Set ( "Accept-Language" , lang )
}
}
2019-11-19 22:36:18 +01:00
}
}
routerHandler ( w , r )
}
}
2019-11-20 15:40:28 +01:00
func getTranslateFunction ( lang string ) func ( string , ... interface { } ) string {
return func ( format string , args ... interface { } ) string {
return i18n . Tr ( lang , format , args ... )
}
}
// New returns a new i18n middleware.
func New ( config Config ) context . Handler {
return NewI18n ( config ) . Handler ( )
}
// NewWrapper accepts a Config and returns a new router wrapper.
// The result function can be passed on `Application.WrapRouter`.
// It compares the path prefix for translated language and
// local redirects the requested path with the selected (from the path) language to the router.
//
// In order this to work as expected, it should be combined with `Application.Use(New)`
// which registers the i18n middleware itself.
func NewWrapper ( config Config ) func ( http . ResponseWriter , * http . Request , http . HandlerFunc ) {
return NewI18n ( config ) . Wrapper ( )
}
2019-11-19 22:36:18 +01:00
// Translate returns the translated word from a context based on the current selected locale.
// The second parameter is the key of the world or line inside the .ini file and
// the third parameter is the '%s' of the world or line inside the .ini file
func Translate ( ctx context . Context , format string , args ... interface { } ) string {
return ctx . Translate ( format , args ... )
}
// TranslateLang returns the translated word from a context based on the given "lang".
// The second parameter is the language key which the word "format" is translated to and
// the third parameter is the key of the world or line inside the .ini file and
// the forth parameter is the '%s' of the world or line inside the .ini file
func TranslateLang ( ctx context . Context , lang , format string , args ... interface { } ) string {
return ctx . TranslateLang ( lang , format , args ... )
}
2017-07-10 17:32:42 +02:00
// TranslatedMap returns translated map[string]interface{} from i18n structure.
func TranslatedMap ( ctx context . Context , sourceInterface interface { } ) map [ string ] interface { } {
2017-02-14 04:54:11 +01:00
iType := reflect . TypeOf ( sourceInterface ) . Elem ( )
result := make ( map [ string ] interface { } )
for i := 0 ; i < iType . NumField ( ) ; i ++ {
fieldName := reflect . TypeOf ( sourceInterface ) . Elem ( ) . Field ( i ) . Name
fieldValue := reflect . ValueOf ( sourceInterface ) . Elem ( ) . Field ( i ) . String ( )
result [ fieldName ] = Translate ( ctx , fieldValue )
}
return result
}