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
// PathParameter is the name of the path parameter which the language can be indentified,
// e.g. "lang" for "{lang:string}".
//
// Checked: Serving state, runtime.
//
// You can set custom handler to set the language too.
// Example:
// setLangMiddleware := func(ctx iris.Context){
// langKey := ctx.Application().ConfigurationReadOnly().GetTranslateLanguageContextKey()
// languageByPath := ctx.Params().Get("lang") // see {lang}
// ctx.Values().Set(langKey, languageByPath)
// ctx.Next()
// }
// app.Use(setLangMiddleware)
// app.Use(theI18nMiddlewareInstance)
PathParameter 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
2017-02-14 04:54:11 +01:00
2019-11-19 22:36:18 +01:00
// If SetCookie is true then it will set the cookie to the language found by URLParameter, PathParameter or by Context's Value's "lang" key.
// Defaults to false.
SetCookie bool
}
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
}
// test file: ../../_examples/miscellaneous/i18n/main_test.go
type i18nMiddleware struct {
config Config
}
// New returns a new i18n middleware.
func New ( c Config ) context . Handler {
c . loadLanguages ( )
i := & i18nMiddleware { config : c }
Publish the new version :airplane: | Look description please!
# FAQ
### Looking for free support?
http://support.iris-go.com
https://kataras.rocket.chat/channel/iris
### Looking for previous versions?
https://github.com/kataras/iris#version
### Should I upgrade my Iris?
Developers are not forced to upgrade if they don't really need it. Upgrade whenever you feel ready.
> Iris uses the [vendor directory](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) feature, so you get truly reproducible builds, as this method guards against upstream renames and deletes.
**How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`.
For further installation support, please click [here](http://support.iris-go.com/d/16-how-to-install-iris-web-framework).
### About our new home page
http://iris-go.com
Thanks to [Santosh Anand](https://github.com/santoshanand) the http://iris-go.com has been upgraded and it's really awesome!
[Santosh](https://github.com/santoshanand) is a freelancer, he has a great knowledge of nodejs and express js, Android, iOS, React Native, Vue.js etc, if you need a developer to find or create a solution for your problem or task, please contact with him.
The amount of the next two or three donations you'll send they will be immediately transferred to his own account balance, so be generous please!
Read more at https://github.com/kataras/iris/blob/master/HISTORY.md
Former-commit-id: eec2d71bbe011d6b48d2526eb25919e36e5ad94e
2017-06-03 22:22:52 +02:00
return i . ServeHTTP
2017-02-14 04:54:11 +01:00
}
2019-11-19 22:36:18 +01:00
// ServeHTTP serves the request, the actual middleware's job is located here.
func ( i * i18nMiddleware ) ServeHTTP ( ctx context . Context ) {
wasByCookie := false
langKey := ctx . Application ( ) . ConfigurationReadOnly ( ) . GetTranslateLanguageContextKey ( )
language := ctx . Values ( ) . GetString ( langKey )
if language == "" {
// try to get by path parameter
if i . config . PathParameter != "" {
language = ctx . Params ( ) . Get ( i . config . PathParameter )
}
if language == "" {
// try to get by url parameter
language = ctx . URLParam ( i . config . URLParameter )
if language == "" {
// then try to take the lang field from the cookie
language = ctx . GetCookie ( langKey )
if len ( language ) > 0 {
wasByCookie = true
} else {
// 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 ]
if lc , ok := i . config . Exists ( lc ) ; ok {
language = lc
break
}
}
}
}
}
}
}
2019-11-20 01:35:41 +01:00
if language == "" {
language = i . config . Default
}
2019-11-19 22:36:18 +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
}
// if it was not taken by the cookie, then set the cookie in order to have it.
if ! wasByCookie && i . config . SetCookie {
ctx . SetCookieKV ( langKey , language )
}
ctx . Values ( ) . Set ( langKey , language )
// 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 )
ctx . Next ( )
}
func getTranslateFunction ( lang string ) func ( string , ... interface { } ) string {
return func ( format string , args ... interface { } ) string {
return i18n . Tr ( lang , format , args ... )
}
}
// 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 ( c Config ) func ( http . ResponseWriter , * http . Request , http . HandlerFunc ) {
c . loadLanguages ( )
return func ( w http . ResponseWriter , r * http . Request , routerHandler http . HandlerFunc ) {
path := r . URL . Path [ 1 : ]
if idx := strings . IndexRune ( path , '/' ) ; idx > 0 {
path = path [ : idx ]
}
2019-11-20 01:35:41 +01:00
if path != "" {
if lang , ok := c . Exists ( path ) ; ok {
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-19 22:36:18 +01:00
}
}
routerHandler ( w , r )
}
}
// 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
}