This repository has been archived on 2020-11-12. You can view files and clone it, but cannot push or open issues or pull requests.
i18n/i18n.go
Andrew Thornton 0d444cb714
All checks were successful
continuous-integration/drone/push Build is passing
Update i18n to allow setting samesite (#7)
fix doc

Signed-off-by: Andrew Thornton <art27@cantab.net>

Update i18n to allow setting samesite

Signed-off-by: Andrew Thornton <art27@cantab.net>

Reviewed-on: #7
Reviewed-by: silverwind <silverwind@noreply.gitea.io>
Reviewed-by: 6543 <6543@obermui.de>
Co-Authored-By: Andrew Thornton <art27@cantab.net>
Co-Committed-By: Andrew Thornton <art27@cantab.net>
2020-11-12 15:45:44 +08:00

249 lines
7.0 KiB
Go

// Copyright 2014 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
// Package i18n provides an Internationalization and Localization middleware for Macaron applications.
package i18n
import (
"fmt"
"log"
"net/http"
"os"
"path"
"strings"
"gitea.com/macaron/macaron"
"gitea.com/macaron/macaron/cookie"
"github.com/unknwon/i18n"
"golang.org/x/text/language"
)
const _VERSION = "0.4.0"
func Version() string {
return _VERSION
}
// isFile returns true if given path is a file,
// or returns false when it's a directory or does not exist.
func isFile(filePath string) bool {
f, e := os.Stat(filePath)
if e != nil {
return false
}
return !f.IsDir()
}
// initLocales initializes language type list and Accept-Language header matcher.
func initLocales(opt Options) language.Matcher {
tags := make([]language.Tag, len(opt.Langs))
for i, lang := range opt.Langs {
tags[i] = language.Raw.Make(lang)
fname := fmt.Sprintf(opt.Format, lang)
// Append custom locale file.
custom := []interface{}{}
customPath := path.Join(opt.CustomDirectory, fname)
if isFile(customPath) {
custom = append(custom, customPath)
}
var locale interface{}
if data, ok := opt.Files[fname]; ok {
locale = data
} else {
locale = path.Join(opt.Directory, fname)
}
err := i18n.SetMessageWithDesc(lang, opt.Names[i], locale, custom...)
if err != nil && err != i18n.ErrLangAlreadyExist {
log.Printf("ERROR: failed to set message file(%s) for language %s: %v", lang, opt.Names[i], err)
}
}
return language.NewMatcher(tags)
}
// A Locale describles the information of localization.
type Locale struct {
i18n.Locale
}
// Language returns language current locale represents.
func (l Locale) Language() string {
return l.Lang
}
// Options represents a struct for specifying configuration options for the i18n middleware.
type Options struct {
// Suburl of path. Default is empty.
SubURL string
// Directory to load locale files. Default is "conf/locale"
Directory string
// File stores actual data of locale files. Used for in-memory purpose.
Files map[string][]byte
// Custom directory to overload locale files. Default is "custom/conf/locale"
CustomDirectory string
// Langauges that will be supported, order is meaningful.
Langs []string
// Human friendly names corresponding to Langs list.
Names []string
// Default language locale, leave empty to remain unset.
DefaultLang string
// Locale file naming style. Default is "locale_%s.ini".
Format string
// Name of language parameter name in URL. Default is "lang".
Parameter string
// Redirect when user uses get parameter to specify language.
Redirect bool
// Name that maps into template variable. Default is "i18n".
TmplName string
// Configuration section name. Default is "i18n".
Section string
// Domain used for `lang` cookie. Default is ""
CookieDomain string
// Set the Secure flag on the `lang` cookie. Default is disabled.
Secure bool
// Set the HTTP Only flag on the `lang` cookie. Default is disabled.
CookieHttpOnly bool
// Set the SameSite flag on the `lang` cookie. Default is unset
SameSite http.SameSite
}
func prepareOptions(options []Options) Options {
var opt Options
if len(options) > 0 {
opt = options[0]
}
if len(opt.Section) == 0 {
opt.Section = "i18n"
}
sec := macaron.Config().Section(opt.Section)
opt.SubURL = strings.TrimSuffix(opt.SubURL, "/")
if len(opt.Langs) == 0 {
opt.Langs = sec.Key("LANGS").Strings(",")
}
if len(opt.Names) == 0 {
opt.Names = sec.Key("NAMES").Strings(",")
}
if len(opt.Langs) == 0 {
panic("no language is specified")
} else if len(opt.Langs) != len(opt.Names) {
panic("length of langs is not same as length of names")
}
i18n.SetDefaultLang(opt.DefaultLang)
if len(opt.Directory) == 0 {
opt.Directory = sec.Key("DIRECTORY").MustString("conf/locale")
}
if len(opt.CustomDirectory) == 0 {
opt.CustomDirectory = sec.Key("CUSTOM_DIRECTORY").MustString("custom/conf/locale")
}
if len(opt.Format) == 0 {
opt.Format = sec.Key("FORMAT").MustString("locale_%s.ini")
}
if len(opt.Parameter) == 0 {
opt.Parameter = sec.Key("PARAMETER").MustString("lang")
}
if !opt.Redirect {
opt.Redirect = sec.Key("REDIRECT").MustBool()
}
if len(opt.TmplName) == 0 {
opt.TmplName = sec.Key("TMPL_NAME").MustString("i18n")
}
if opt.SameSite == 0 {
opt.SameSite = http.SameSite(sec.Key("SAME_SITE").MustInt())
}
return opt
}
type LangType struct {
Lang, Name string
}
// I18n is a middleware provides localization layer for your application.
// Paramenter langs must be in the form of "en-US", "zh-CN", etc.
// Otherwise it may not recognize browser input.
func I18n(options ...Options) macaron.Handler {
opt := prepareOptions(options)
m := initLocales(opt)
return func(ctx *macaron.Context) {
isNeedRedir := false
hasCookie := false
// 1. Check URL arguments.
lang := ctx.Query(opt.Parameter)
// 2. Get language information from cookies.
if len(lang) == 0 {
lang = ctx.GetCookie("lang")
hasCookie = true
} else {
isNeedRedir = true
}
// Check again in case someone modify by purpose.
if !i18n.IsExist(lang) {
lang = ""
isNeedRedir = false
hasCookie = false
}
// 3. Get language information from 'Accept-Language'.
// The first element in the list is chosen to be the default language automatically.
if len(lang) == 0 {
tags, _, _ := language.ParseAcceptLanguage(ctx.Req.Header.Get("Accept-Language"))
tag, _, _ := m.Match(tags...)
lang = tag.String()
isNeedRedir = false
}
curLang := LangType{
Lang: lang,
}
// Save language information in cookies.
if !hasCookie {
ctx.SetCookie("lang", curLang.Lang, 1<<31-1, "/"+strings.TrimPrefix(opt.SubURL, "/"), opt.CookieDomain, opt.Secure, opt.CookieHttpOnly, cookie.SameSite(opt.SameSite))
}
restLangs := make([]LangType, 0, i18n.Count()-1)
langs := i18n.ListLangs()
names := i18n.ListLangDescs()
for i, v := range langs {
if lang != v {
restLangs = append(restLangs, LangType{v, names[i]})
} else {
curLang.Name = names[i]
}
}
// Set language properties.
locale := Locale{Locale: i18n.Locale{Lang: lang}}
ctx.Map(locale)
ctx.Locale = locale
ctx.Data[opt.TmplName] = locale
ctx.Data["Tr"] = i18n.Tr
ctx.Data["Lang"] = locale.Lang
ctx.Data["LangName"] = curLang.Name
ctx.Data["AllLangs"] = append([]LangType{curLang}, restLangs...)
ctx.Data["RestLangs"] = restLangs
if opt.Redirect && isNeedRedir {
ctx.Redirect(opt.SubURL + path.Clean(ctx.Req.RequestURI[:strings.Index(ctx.Req.RequestURI, "?")]))
}
}
}