-
Search Results
+
{{ call .Trans "Search Results" }}
{{ template "messages.html" . }}
{{ range $result := .Results }}
diff --git a/go.mod b/go.mod
index 0c27c5c..e432275 100644
--- a/go.mod
+++ b/go.mod
@@ -3,15 +3,18 @@ module github.com/uberswe/golang-base-project
go 1.16
require (
+ github.com/BurntSushi/toml v0.3.1
github.com/gin-contrib/sessions v0.0.4
github.com/gin-gonic/gin v1.7.7
github.com/go-playground/validator/v10 v10.4.1
github.com/gorilla/securecookie v1.1.1
github.com/kr/text v0.2.0 // indirect
+ github.com/nicksnyder/go-i18n/v2 v2.1.2
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/oklog/ulid/v2 v2.0.2
github.com/ulule/limiter/v3 v3.9.0
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
+ golang.org/x/text v0.3.7
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
gorm.io/driver/mysql v1.2.1
diff --git a/go.sum b/go.sum
index 9a753b3..1ade5e2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,4 @@
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
@@ -161,6 +162,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/nicksnyder/go-i18n/v2 v2.1.2 h1:QHYxcUJnGHBaq7XbvgunmZ2Pn0focXFqTD61CkH146c=
+github.com/nicksnyder/go-i18n/v2 v2.1.2/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
diff --git a/lang/main.go b/lang/main.go
new file mode 100644
index 0000000..4961713
--- /dev/null
+++ b/lang/main.go
@@ -0,0 +1,39 @@
+package lang
+
+import (
+ "github.com/gin-gonic/gin"
+ "github.com/nicksnyder/go-i18n/v2/i18n"
+)
+
+type Service struct {
+ bundle *i18n.Bundle
+ ctx *gin.Context
+ localizer *i18n.Localizer
+}
+
+func New(ctx *gin.Context, bundle *i18n.Bundle) Service {
+ localizer := i18n.NewLocalizer(bundle, ctx.Request.Header.Get("Accept-Language"), "en")
+ return Service{
+ bundle: bundle,
+ ctx: ctx,
+ localizer: localizer,
+ }
+}
+
+func (s *Service) Trans(str string) string {
+ // TODO, modify this to handle plural and more types of phrases
+ for _, m := range translationMessages {
+ if m.ID == str {
+ localizedString, _ := s.localizer.Localize(&i18n.LocalizeConfig{
+ DefaultMessage: &m,
+ })
+ return localizedString
+ } else if m.Other == str {
+ localizedString, _ := s.localizer.Localize(&i18n.LocalizeConfig{
+ DefaultMessage: &m,
+ })
+ return localizedString
+ }
+ }
+ return str
+}
diff --git a/lang/messages.go b/lang/messages.go
new file mode 100644
index 0000000..5a4efea
--- /dev/null
+++ b/lang/messages.go
@@ -0,0 +1,236 @@
+package lang
+
+import (
+ "github.com/nicksnyder/go-i18n/v2/i18n"
+)
+
+var translationMessages = []i18n.Message{
+ {
+ ID: "site_name",
+ Other: "Golang Base Project",
+ },
+ {
+ ID: "home",
+ Other: "Home",
+ },
+ {
+ ID: "activation_validation_token",
+ Other: "Please provide a valid activation token",
+ },
+ {
+ ID: "activation_success",
+ Other: "Account activated. You may now proceed to login to your account.",
+ },
+ {
+ ID: "activate",
+ Other: "Activate",
+ },
+ {
+ ID: "admin",
+ Other: "Admin",
+ },
+ {
+ ID: "forgot_password",
+ Other: "Forgot Password",
+ },
+ {
+ ID: "forgot_password_success",
+ Other: "An email with instructions describing how to reset your password has been sent.",
+ },
+ {
+ ID: "password_reset",
+ Other: "Password Reset",
+ },
+ {
+ ID: "password_reset_email",
+ Other: "Use the following link to reset your password. If this was not requested by you, please ignore this email.\n%s",
+ },
+ {
+ ID: "login",
+ Other: "Login",
+ },
+ {
+ ID: "login_error",
+ Other: "Could not login, please make sure that you have typed in the correct email and password. If you have forgotten your password, please click the forgot password link below.",
+ },
+ {
+ ID: "login_activated_error",
+ Other: "Account is not activated yet.",
+ },
+ {
+ ID: "404_not_found",
+ Other: "404 Not Found",
+ },
+ {
+ ID: "register",
+ Other: "Register",
+ },
+ {
+ ID: "password_error",
+ Other: "Your password must be 8 characters in length or longer",
+ },
+ {
+ ID: "register_error",
+ Other: "Could not register, please make sure the details you have provided are correct and that you do not already have an existing account.",
+ },
+ {
+ ID: "register_success",
+ Other: "Thank you for registering. An activation email has been sent with steps describing how to activate your account.",
+ },
+ {
+ ID: "user_activation",
+ Other: "User Activation",
+ },
+ {
+ ID: "user_activation_email",
+ Other: "Use the following link to activate your account. If this was not requested by you, please ignore this email.\n%s",
+ },
+ {
+ ID: "resend_activation_email_subject",
+ Other: "Resend Activation Email",
+ },
+ {
+ ID: "resend_activation_email_success",
+ Other: "A new activation email has been sent if the account exists and is not already activated. Please remember to check your spam inbox in case the email is not showing in your inbox.",
+ },
+ {
+ ID: "reset_password",
+ Other: "Reset Password",
+ },
+ {
+ ID: "reset_password_error",
+ Other: "Could not reset password, please try again",
+ },
+ {
+ ID: "password_reset_success",
+ Other: "Your password has successfully been reset.",
+ },
+ {
+ ID: "search",
+ Other: "Search",
+ },
+ {
+ ID: "search_results",
+ Other: "Search Results",
+ },
+ {
+ ID: "no_results_found",
+ Other: "No results found",
+ },
+ {
+ ID: "404_message_1",
+ Other: "The page you're looking for could not be found.",
+ },
+ {
+ ID: "click_here",
+ Other: "Click here",
+ },
+ {
+ ID: "404_message_2",
+ Other: "to return to the main page.",
+ },
+ {
+ ID: "admin_dashboard",
+ Other: "Admin Dashboard",
+ },
+ {
+ ID: "dashboard_message",
+ Other: "You now have an authenticated session, feel free to log out using the link in the navbar above.",
+ },
+ {
+ ID: "footer_message_1",
+ Other: "Fork this project on",
+ },
+ {
+ ID: "created_by",
+ Other: "Created by",
+ },
+ {
+ ID: "forgot_password",
+ Other: "Forgot password?",
+ },
+ {
+ ID: "forgot_password_message",
+ Other: "Use the form below to reset your password. If we have an account with your email you will receive instructions on how to reset your password.",
+ },
+ {
+ ID: "email_address",
+ Other: "Email address",
+ },
+ {
+ ID: "request_reset_email",
+ Other: "Request reset email",
+ },
+ {
+ ID: "lang_key",
+ Other: "en",
+ },
+ {
+ ID: "home",
+ Other: "Home",
+ },
+ {
+ ID: "admin",
+ Other: "Admin",
+ },
+ {
+ ID: "logout",
+ Other: "Logout",
+ },
+ {
+ ID: "login",
+ Other: "Login",
+ },
+ {
+ ID: "register",
+ Other: "Register",
+ },
+ {
+ ID: "search",
+ Other: "Search",
+ },
+ {
+ ID: "index_message_1",
+ Other: "A simple website with user login and registration.",
+ },
+ {
+ ID: "index_message_2",
+ Other: "The frontend uses",
+ },
+ {
+ ID: "index_message_3",
+ Other: "and the backend is written in",
+ },
+ {
+ ID: "index_message_4",
+ Other: "Read more about this project on",
+ },
+ {
+ ID: "password",
+ Other: "Password",
+ },
+ {
+ ID: "login_terms",
+ Other: "By pressing the button below to login you agree to the use of cookies on this website.",
+ },
+ {
+ ID: "request_new_activation_email",
+ Other: "Request a new activation email",
+ },
+ {
+ ID: "resend_activation_email",
+ Other: "Resend Activation Email",
+ },
+ {
+ ID: "resend_activation_email_message",
+ Other: "If you have already registered but never activated your account you can use the form below to request a new activation email.",
+ },
+ {
+ ID: "request_activation_email",
+ Other: "Request activation email",
+ },
+ {
+ ID: "reset_password_message",
+ Other: "Please enter a new password.",
+ },
+}
diff --git a/main.go b/main.go
index 5033fed..1172116 100644
--- a/main.go
+++ b/main.go
@@ -2,11 +2,15 @@ package baseproject
import (
"embed"
+ "fmt"
+ "github.com/BurntSushi/toml"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
+ "github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/uberswe/golang-base-project/middleware"
"github.com/uberswe/golang-base-project/routes"
+ "golang.org/x/text/language"
"html/template"
"io/fs"
"log"
@@ -27,6 +31,20 @@ func Run() {
// We load environment variables, these are only read when the application launches
conf := loadEnvVariables()
+ // Translations
+ bundle := i18n.NewBundle(language.English)
+ bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
+ languages := []string{
+ "en",
+ "sv",
+ }
+ for _, l := range languages {
+ _, err := bundle.LoadMessageFile(fmt.Sprintf("active.%s.toml", l))
+ if err != nil {
+ log.Fatalln(err)
+ }
+ }
+
// We connect to the database using the configuration generated from the environment variables.
db, err := connectToDatabase(conf)
if err != nil {
@@ -81,7 +99,7 @@ func Run() {
r.Use(middleware.General())
// A new instance of the routes controller is created
- controller := routes.New(db, conf)
+ controller := routes.New(db, conf, bundle)
// Any request to / will call controller.Index
r.GET("/", controller.Index)
diff --git a/routes/activate.go b/routes/activate.go
index ed88169..b0894ee 100644
--- a/routes/activate.go
+++ b/routes/activate.go
@@ -10,13 +10,10 @@ import (
// Activate handles requests used to activate a users account
func (controller Controller) Activate(c *gin.Context) {
- activationError := "Please provide a valid activation token"
- activationSuccess := "Account activated. You may now proceed to login to your account."
- pd := PageData{
- Title: "Activate",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- }
+ pd := controller.DefaultPageData(c)
+ activationError := pd.Trans("Please provide a valid activation token")
+ activationSuccess := pd.Trans("Account activated. You may now proceed to login to your account.")
+ pd.Title = pd.Trans("Activate")
token := c.Param("token")
activationToken := models.Token{
Value: token,
diff --git a/routes/admin.go b/routes/admin.go
index 726492f..533efa3 100644
--- a/routes/admin.go
+++ b/routes/admin.go
@@ -7,10 +7,7 @@ import (
// Admin renders the admin dashboard
func (controller Controller) Admin(c *gin.Context) {
- pd := PageData{
- Title: "Admin",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- }
+ pd := controller.DefaultPageData(c)
+ pd.Title = pd.Trans("Admin")
c.HTML(http.StatusOK, "admin.html", pd)
}
diff --git a/routes/forgotpassword.go b/routes/forgotpassword.go
index 775bfa3..fa0fd4a 100644
--- a/routes/forgotpassword.go
+++ b/routes/forgotpassword.go
@@ -16,38 +16,32 @@ import (
// ForgotPassword renders the HTML page where a password request can be initiated
func (controller Controller) ForgotPassword(c *gin.Context) {
- pd := PageData{
- Title: "Forgot Password",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- }
+ pd := controller.DefaultPageData(c)
+ pd.Title = pd.Trans("Forgot Password")
c.HTML(http.StatusOK, "forgotpassword.html", pd)
}
// ForgotPasswordPost handles the POST request which requests a password reset and then renders the HTML page with the appropriate message
func (controller Controller) ForgotPasswordPost(c *gin.Context) {
- pd := PageData{
- Title: "Forgot Password",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- }
+ pd := controller.DefaultPageData(c)
+ pd.Title = pd.Trans("Forgot Password")
email := c.PostForm("email")
user := models.User{Email: email}
res := controller.db.Where(&user).First(&user)
if res.Error == nil && user.ActivatedAt != nil {
- go controller.forgotPasswordEmailHandler(user.ID, email)
+ go controller.forgotPasswordEmailHandler(user.ID, email, pd.Trans)
}
pd.Messages = append(pd.Messages, Message{
Type: "success",
- Content: "An email with instructions describing how to reset your password has been sent.",
+ Content: pd.Trans("An email with instructions describing how to reset your password has been sent."),
})
// We always return a positive response here to prevent user enumeration
c.HTML(http.StatusOK, "forgotpassword.html", pd)
}
-func (controller Controller) forgotPasswordEmailHandler(userID uint, email string) {
+func (controller Controller) forgotPasswordEmailHandler(userID uint, email string, trans func(string) string) {
forgotPasswordToken := models.Token{
Value: ulid.Generate(),
Type: models.TokenPasswordReset,
@@ -56,7 +50,7 @@ func (controller Controller) forgotPasswordEmailHandler(userID uint, email strin
res := controller.db.Where(&forgotPasswordToken).First(&forgotPasswordToken)
if (res.Error != nil && res.Error != gorm.ErrRecordNotFound) || res.RowsAffected > 0 {
// If the forgot password token already exists we try to generate it again
- controller.forgotPasswordEmailHandler(userID, email)
+ controller.forgotPasswordEmailHandler(userID, email, trans)
return
}
@@ -70,10 +64,10 @@ func (controller Controller) forgotPasswordEmailHandler(userID uint, email strin
log.Println(res.Error)
return
}
- controller.sendForgotPasswordEmail(forgotPasswordToken.Value, email)
+ controller.sendForgotPasswordEmail(forgotPasswordToken.Value, email, trans)
}
-func (controller Controller) sendForgotPasswordEmail(token string, email string) {
+func (controller Controller) sendForgotPasswordEmail(token string, email string, trans func(string) string) {
u, err := url.Parse(controller.config.BaseURL)
if err != nil {
log.Println(err)
@@ -86,5 +80,5 @@ func (controller Controller) sendForgotPasswordEmail(token string, email string)
emailService := email2.New(controller.config)
- emailService.Send(email, "Password Reset", fmt.Sprintf("Use the following link to reset your password. If this was not requested by you, please ignore this email.\n%s", resetPasswordURL))
+ emailService.Send(email, trans("Password Reset"), fmt.Sprintf(trans("Use the following link to reset your password. If this was not requested by you, please ignore this email.\n%s"), resetPasswordURL))
}
diff --git a/routes/index.go b/routes/index.go
index e92081d..2afa588 100644
--- a/routes/index.go
+++ b/routes/index.go
@@ -7,10 +7,7 @@ import (
// Index renders the HTML of the index page
func (controller Controller) Index(c *gin.Context) {
- pd := PageData{
- Title: "Home",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- }
+ pd := controller.DefaultPageData(c)
+ pd.Title = pd.Trans("Home")
c.HTML(http.StatusOK, "index.html", pd)
}
diff --git a/routes/login.go b/routes/login.go
index a65d42c..84c6d3e 100644
--- a/routes/login.go
+++ b/routes/login.go
@@ -14,22 +14,16 @@ import (
// Login renders the HTML of the login page
func (controller Controller) Login(c *gin.Context) {
- pd := PageData{
- Title: "Login",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- }
+ pd := controller.DefaultPageData(c)
+ pd.Title = pd.Trans("Login")
c.HTML(http.StatusOK, "login.html", pd)
}
// LoginPost handles login requests and returns the appropriate HTML and messages
func (controller Controller) LoginPost(c *gin.Context) {
- loginError := "Could not login, please make sure that you have typed in the correct email and password. If you have forgotten your password, please click the forgot password link below."
- pd := PageData{
- Title: "Login",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- }
+ pd := controller.DefaultPageData(c)
+ loginError := pd.Trans("Could not login, please make sure that you have typed in the correct email and password. If you have forgotten your password, please click the forgot password link below.")
+ pd.Title = pd.Trans("Login")
email := c.PostForm("email")
user := models.User{Email: email}
@@ -56,7 +50,7 @@ func (controller Controller) LoginPost(c *gin.Context) {
if user.ActivatedAt == nil {
pd.Messages = append(pd.Messages, Message{
Type: "error",
- Content: "Account is not activated yet.",
+ Content: pd.Trans("Account is not activated yet."),
})
c.HTML(http.StatusBadRequest, "login.html", pd)
return
diff --git a/routes/logout.go b/routes/logout.go
index 7346fcc..6952e29 100644
--- a/routes/logout.go
+++ b/routes/logout.go
@@ -13,7 +13,9 @@ func (controller Controller) Logout(c *gin.Context) {
session := sessions.Default(c)
session.Delete(middleware.SessionIDKey)
err := session.Save()
- log.Println(err)
+ if err != nil {
+ log.Println(err)
+ }
c.Redirect(http.StatusTemporaryRedirect, "/")
}
diff --git a/routes/main.go b/routes/main.go
index 77f27ce..f5cc240 100644
--- a/routes/main.go
+++ b/routes/main.go
@@ -3,7 +3,9 @@ package routes
import (
"github.com/gin-gonic/gin"
+ "github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/uberswe/golang-base-project/config"
+ "github.com/uberswe/golang-base-project/lang"
"github.com/uberswe/golang-base-project/middleware"
"gorm.io/gorm"
)
@@ -12,13 +14,15 @@ import (
type Controller struct {
db *gorm.DB
config config.Config
+ bundle *i18n.Bundle
}
// New creates a new instance of the routes.Controller
-func New(db *gorm.DB, c config.Config) Controller {
+func New(db *gorm.DB, c config.Config, bundle *i18n.Bundle) Controller {
return Controller{
db: db,
config: c,
+ bundle: bundle,
}
}
@@ -28,6 +32,7 @@ type PageData struct {
Messages []Message
IsAuthenticated bool
CacheParameter string
+ Trans func(s string) string
}
// Message holds a message which can be rendered as responses on HTML pages
@@ -41,3 +46,14 @@ func isAuthenticated(c *gin.Context) bool {
_, exists := c.Get(middleware.UserIDKey)
return exists
}
+
+func (controller Controller) DefaultPageData(c *gin.Context) PageData {
+ langService := lang.New(c, controller.bundle)
+ return PageData{
+ Title: "Home",
+ Messages: nil,
+ IsAuthenticated: isAuthenticated(c),
+ CacheParameter: controller.config.CacheParameter,
+ Trans: langService.Trans,
+ }
+}
diff --git a/routes/noroute.go b/routes/noroute.go
index 66d16c5..eee7c6b 100644
--- a/routes/noroute.go
+++ b/routes/noroute.go
@@ -7,10 +7,7 @@ import (
// NoRoute handles rendering of the 404 page
func (controller Controller) NoRoute(c *gin.Context) {
- pd := PageData{
- Title: "404 Not Found",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- }
+ pd := controller.DefaultPageData(c)
+ pd.Title = pd.Trans("404 Not Found")
c.HTML(http.StatusOK, "404.html", pd)
}
diff --git a/routes/register.go b/routes/register.go
index 5bf58ce..d17882b 100644
--- a/routes/register.go
+++ b/routes/register.go
@@ -18,24 +18,18 @@ import (
// Register renders the HTML content of the register page
func (controller Controller) Register(c *gin.Context) {
- pd := PageData{
- Title: "Register",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- }
+ pd := controller.DefaultPageData(c)
+ pd.Title = pd.Trans("Register")
c.HTML(http.StatusOK, "register.html", pd)
}
// RegisterPost handles requests to register users and returns appropriate messages as HTML content
func (controller Controller) RegisterPost(c *gin.Context) {
- passwordError := "Your password must be 8 characters in length or longer"
- registerError := "Could not register, please make sure the details you have provided are correct and that you do not already have an existing account."
- registerSuccess := "Thank you for registering. An activation email has been sent with steps describing how to activate your account."
- pd := PageData{
- Title: "Register",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- }
+ pd := controller.DefaultPageData(c)
+ passwordError := pd.Trans("Your password must be 8 characters in length or longer")
+ registerError := pd.Trans("Could not register, please make sure the details you have provided are correct and that you do not already have an existing account.")
+ registerSuccess := pd.Trans("Thank you for registering. An activation email has been sent with steps describing how to activate your account.")
+ pd.Title = pd.Trans("Register")
password := c.PostForm("password")
if len(password) < 8 {
pd.Messages = append(pd.Messages, Message{
@@ -111,7 +105,7 @@ func (controller Controller) RegisterPost(c *gin.Context) {
}
// Generate activation token and send activation email
- go controller.activationEmailHandler(user.ID, email)
+ go controller.activationEmailHandler(user.ID, email, pd.Trans)
pd.Messages = append(pd.Messages, Message{
Type: "success",
@@ -121,7 +115,7 @@ func (controller Controller) RegisterPost(c *gin.Context) {
c.HTML(http.StatusOK, "register.html", pd)
}
-func (controller Controller) activationEmailHandler(userID uint, email string) {
+func (controller Controller) activationEmailHandler(userID uint, email string, trans func(string) string) {
activationToken := models.Token{
Value: ulid.Generate(),
Type: models.TokenUserActivation,
@@ -130,7 +124,7 @@ func (controller Controller) activationEmailHandler(userID uint, email string) {
res := controller.db.Where(&activationToken).First(&activationToken)
if (res.Error != nil && res.Error != gorm.ErrRecordNotFound) || res.RowsAffected > 0 {
// If the activation token already exists we try to generate it again
- controller.activationEmailHandler(userID, email)
+ controller.activationEmailHandler(userID, email, trans)
return
}
@@ -143,10 +137,10 @@ func (controller Controller) activationEmailHandler(userID uint, email string) {
log.Println(res.Error)
return
}
- controller.sendActivationEmail(activationToken.Value, email)
+ controller.sendActivationEmail(activationToken.Value, email, trans)
}
-func (controller Controller) sendActivationEmail(token string, email string) {
+func (controller Controller) sendActivationEmail(token string, email string, trans func(string) string) {
u, err := url.Parse(controller.config.BaseURL)
if err != nil {
log.Println(err)
@@ -159,5 +153,5 @@ func (controller Controller) sendActivationEmail(token string, email string) {
emailService := email2.New(controller.config)
- emailService.Send(email, "User Activation", fmt.Sprintf("Use the following link to activate your account. If this was not requested by you, please ignore this email.\n%s", activationURL))
+ emailService.Send(email, trans("User Activation"), fmt.Sprintf(trans("Use the following link to activate your account. If this was not requested by you, please ignore this email.\n%s"), activationURL))
}
diff --git a/routes/resendactivation.go b/routes/resendactivation.go
index 0103d1f..4356777 100644
--- a/routes/resendactivation.go
+++ b/routes/resendactivation.go
@@ -9,21 +9,15 @@ import (
// ResendActivation renders the HTML page used to request a new activation email
func (controller Controller) ResendActivation(c *gin.Context) {
- pd := PageData{
- Title: "Resend Activation Email",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- }
+ pd := controller.DefaultPageData(c)
+ pd.Title = pd.Trans("Resend Activation Email")
c.HTML(http.StatusOK, "resendactivation.html", pd)
}
// ResendActivationPost handles the post request for requesting a new activation email
func (controller Controller) ResendActivationPost(c *gin.Context) {
- pd := PageData{
- Title: "Resend Activation Email",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- }
+ pd := controller.DefaultPageData(c)
+ pd.Title = pd.Trans("Resend Activation Email")
email := c.PostForm("email")
user := models.User{Email: email}
res := controller.db.Where(&user).First(&user)
@@ -36,10 +30,10 @@ func (controller Controller) ResendActivationPost(c *gin.Context) {
res = controller.db.Where(&activationToken).First(&activationToken)
if res.Error == nil {
// If the activation token exists we simply send an email
- go controller.sendActivationEmail(activationToken.Value, user.Email)
+ go controller.sendActivationEmail(activationToken.Value, user.Email, pd.Trans)
} else {
// If there is no token then we need to generate a new token
- go controller.activationEmailHandler(user.ID, user.Email)
+ go controller.activationEmailHandler(user.ID, user.Email, pd.Trans)
}
} else {
log.Println(res.Error)
@@ -48,7 +42,7 @@ func (controller Controller) ResendActivationPost(c *gin.Context) {
// We always return a positive response here to prevent user enumeration and other attacks
pd.Messages = append(pd.Messages, Message{
Type: "success",
- Content: "A new activation email has been sent if the account exists and is not already activated. Please remember to check your spam inbox in case the email is not showing in your inbox.",
+ Content: pd.Trans("A new activation email has been sent if the account exists and is not already activated. Please remember to check your spam inbox in case the email is not showing in your inbox."),
})
c.HTML(http.StatusOK, "resendactivation.html", pd)
}
diff --git a/routes/resetpassword.go b/routes/resetpassword.go
index 41f5a24..761501c 100644
--- a/routes/resetpassword.go
+++ b/routes/resetpassword.go
@@ -17,30 +17,26 @@ type ResetPasswordPageData struct {
// ResetPassword renders the HTML page for resetting the users password
func (controller Controller) ResetPassword(c *gin.Context) {
token := c.Param("token")
+ pdPre := controller.DefaultPageData(c)
+ pdPre.Title = pdPre.Trans("Reset Password")
pd := ResetPasswordPageData{
- PageData: PageData{
- Title: "Reset Password",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- },
- Token: token,
+ PageData: pdPre,
+ Token: token,
}
c.HTML(http.StatusOK, "resetpassword.html", pd)
}
// ResetPasswordPost handles post request used to reset users passwords
func (controller Controller) ResetPasswordPost(c *gin.Context) {
- passwordError := "Your password must be 8 characters in length or longer"
- resetError := "Could not reset password, please try again"
+ pdPre := controller.DefaultPageData(c)
+ passwordError := pdPre.Trans("Your password must be 8 characters in length or longer")
+ resetError := pdPre.Trans("Could not reset password, please try again")
token := c.Param("token")
+ pdPre.Title = pdPre.Trans("Reset Password")
pd := ResetPasswordPageData{
- PageData: PageData{
- Title: "Reset Password",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- },
- Token: token,
+ PageData: pdPre,
+ Token: token,
}
password := c.PostForm("password")
@@ -125,7 +121,7 @@ func (controller Controller) ResetPasswordPost(c *gin.Context) {
pd.Messages = append(pd.Messages, Message{
Type: "success",
- Content: "Your password has successfully been reset.",
+ Content: pdPre.Trans("Your password has successfully been reset."),
})
c.HTML(http.StatusOK, "resetpassword.html", pd)
diff --git a/routes/search.go b/routes/search.go
index e079ff4..4898f08 100644
--- a/routes/search.go
+++ b/routes/search.go
@@ -16,12 +16,10 @@ type SearchData struct {
// Search renders the search HTML page and any search results
func (controller Controller) Search(c *gin.Context) {
+ pdS := controller.DefaultPageData(c)
+ pdS.Title = pdS.Trans("Search")
pd := SearchData{
- PageData: PageData{
- Title: "Search",
- IsAuthenticated: isAuthenticated(c),
- CacheParameter: controller.config.CacheParameter,
- },
+ PageData: pdS,
}
search := c.PostForm("search")
@@ -36,7 +34,7 @@ func (controller Controller) Search(c *gin.Context) {
if res.Error != nil || len(results) == 0 {
pd.Messages = append(pd.Messages, Message{
Type: "error",
- Content: "No results found",
+ Content: pdS.Trans("No results found"),
})
log.Println(res.Error)
c.HTML(http.StatusOK, "search.html", pd)
diff --git a/vendor/github.com/nicksnyder/go-i18n/v2/LICENSE b/vendor/github.com/nicksnyder/go-i18n/v2/LICENSE
new file mode 100644
index 0000000..609cce7
--- /dev/null
+++ b/vendor/github.com/nicksnyder/go-i18n/v2/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2014 Nick Snyder https://github.com/nicksnyder
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/vendor/github.com/nicksnyder/go-i18n/v2/i18n/bundle.go b/vendor/github.com/nicksnyder/go-i18n/v2/i18n/bundle.go
new file mode 100644
index 0000000..513c127
--- /dev/null
+++ b/vendor/github.com/nicksnyder/go-i18n/v2/i18n/bundle.go
@@ -0,0 +1,144 @@
+package i18n
+
+import (
+ "fmt"
+ "io/ioutil"
+
+ "github.com/nicksnyder/go-i18n/v2/internal/plural"
+
+ "golang.org/x/text/language"
+)
+
+// UnmarshalFunc unmarshals data into v.
+type UnmarshalFunc func(data []byte, v interface{}) error
+
+// Bundle stores a set of messages and pluralization rules.
+// Most applications only need a single bundle
+// that is initialized early in the application's lifecycle.
+// It is not goroutine safe to modify the bundle while Localizers
+// are reading from it.
+type Bundle struct {
+ defaultLanguage language.Tag
+ unmarshalFuncs map[string]UnmarshalFunc
+ messageTemplates map[language.Tag]map[string]*MessageTemplate
+ pluralRules plural.Rules
+ tags []language.Tag
+ matcher language.Matcher
+}
+
+// artTag is the language tag used for artificial languages
+// https://en.wikipedia.org/wiki/Codes_for_constructed_languages
+var artTag = language.MustParse("art")
+
+// NewBundle returns a bundle with a default language and a default set of plural rules.
+func NewBundle(defaultLanguage language.Tag) *Bundle {
+ b := &Bundle{
+ defaultLanguage: defaultLanguage,
+ pluralRules: plural.DefaultRules(),
+ }
+ b.pluralRules[artTag] = b.pluralRules.Rule(language.English)
+ b.addTag(defaultLanguage)
+ return b
+}
+
+// RegisterUnmarshalFunc registers an UnmarshalFunc for format.
+func (b *Bundle) RegisterUnmarshalFunc(format string, unmarshalFunc UnmarshalFunc) {
+ if b.unmarshalFuncs == nil {
+ b.unmarshalFuncs = make(map[string]UnmarshalFunc)
+ }
+ b.unmarshalFuncs[format] = unmarshalFunc
+}
+
+// LoadMessageFile loads the bytes from path
+// and then calls ParseMessageFileBytes.
+func (b *Bundle) LoadMessageFile(path string) (*MessageFile, error) {
+ buf, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+ return b.ParseMessageFileBytes(buf, path)
+}
+
+// MustLoadMessageFile is similar to LoadTranslationFile
+// except it panics if an error happens.
+func (b *Bundle) MustLoadMessageFile(path string) {
+ if _, err := b.LoadMessageFile(path); err != nil {
+ panic(err)
+ }
+}
+
+// ParseMessageFileBytes parses the bytes in buf to add translations to the bundle.
+//
+// The format of the file is everything after the last ".".
+//
+// The language tag of the file is everything after the second to last "." or after the last path separator, but before the format.
+func (b *Bundle) ParseMessageFileBytes(buf []byte, path string) (*MessageFile, error) {
+ messageFile, err := ParseMessageFileBytes(buf, path, b.unmarshalFuncs)
+ if err != nil {
+ return nil, err
+ }
+ if err := b.AddMessages(messageFile.Tag, messageFile.Messages...); err != nil {
+ return nil, err
+ }
+ return messageFile, nil
+}
+
+// MustParseMessageFileBytes is similar to ParseMessageFileBytes
+// except it panics if an error happens.
+func (b *Bundle) MustParseMessageFileBytes(buf []byte, path string) {
+ if _, err := b.ParseMessageFileBytes(buf, path); err != nil {
+ panic(err)
+ }
+}
+
+// AddMessages adds messages for a language.
+// It is useful if your messages are in a format not supported by ParseMessageFileBytes.
+func (b *Bundle) AddMessages(tag language.Tag, messages ...*Message) error {
+ pluralRule := b.pluralRules.Rule(tag)
+ if pluralRule == nil {
+ return fmt.Errorf("no plural rule registered for %s", tag)
+ }
+ if b.messageTemplates == nil {
+ b.messageTemplates = map[language.Tag]map[string]*MessageTemplate{}
+ }
+ if b.messageTemplates[tag] == nil {
+ b.messageTemplates[tag] = map[string]*MessageTemplate{}
+ b.addTag(tag)
+ }
+ for _, m := range messages {
+ b.messageTemplates[tag][m.ID] = NewMessageTemplate(m)
+ }
+ return nil
+}
+
+// MustAddMessages is similar to AddMessages except it panics if an error happens.
+func (b *Bundle) MustAddMessages(tag language.Tag, messages ...*Message) {
+ if err := b.AddMessages(tag, messages...); err != nil {
+ panic(err)
+ }
+}
+
+func (b *Bundle) addTag(tag language.Tag) {
+ for _, t := range b.tags {
+ if t == tag {
+ // Tag already exists
+ return
+ }
+ }
+ b.tags = append(b.tags, tag)
+ b.matcher = language.NewMatcher(b.tags)
+}
+
+// LanguageTags returns the list of language tags
+// of all the translations loaded into the bundle
+func (b *Bundle) LanguageTags() []language.Tag {
+ return b.tags
+}
+
+func (b *Bundle) getMessageTemplate(tag language.Tag, id string) *MessageTemplate {
+ templates := b.messageTemplates[tag]
+ if templates == nil {
+ return nil
+ }
+ return templates[id]
+}
diff --git a/vendor/github.com/nicksnyder/go-i18n/v2/i18n/doc.go b/vendor/github.com/nicksnyder/go-i18n/v2/i18n/doc.go
new file mode 100644
index 0000000..73d7a06
--- /dev/null
+++ b/vendor/github.com/nicksnyder/go-i18n/v2/i18n/doc.go
@@ -0,0 +1,24 @@
+// Package i18n provides support for looking up messages
+// according to a set of locale preferences.
+//
+// Create a Bundle to use for the lifetime of your application.
+// bundle := i18n.NewBundle(language.English)
+//
+// Load translations into your bundle during initialization.
+// bundle.LoadMessageFile("en-US.yaml")
+//
+// Create a Localizer to use for a set of language preferences.
+// func(w http.ResponseWriter, r *http.Request) {
+// lang := r.FormValue("lang")
+// accept := r.Header.Get("Accept-Language")
+// localizer := i18n.NewLocalizer(bundle, lang, accept)
+// }
+//
+// Use the Localizer to lookup messages.
+// localizer.MustLocalize(&i18n.LocalizeConfig{
+// DefaultMessage: &i18n.Message{
+// ID: "HelloWorld",
+// Other: "Hello World!",
+// },
+// })
+package i18n
diff --git a/vendor/github.com/nicksnyder/go-i18n/v2/i18n/localizer.go b/vendor/github.com/nicksnyder/go-i18n/v2/i18n/localizer.go
new file mode 100644
index 0000000..17261e2
--- /dev/null
+++ b/vendor/github.com/nicksnyder/go-i18n/v2/i18n/localizer.go
@@ -0,0 +1,214 @@
+package i18n
+
+import (
+ "fmt"
+ "text/template"
+
+ "github.com/nicksnyder/go-i18n/v2/internal/plural"
+ "golang.org/x/text/language"
+)
+
+// Localizer provides Localize and MustLocalize methods that return localized messages.
+type Localizer struct {
+ // bundle contains the messages that can be returned by the Localizer.
+ bundle *Bundle
+
+ // tags is the list of language tags that the Localizer checks
+ // in order when localizing a message.
+ tags []language.Tag
+}
+
+// NewLocalizer returns a new Localizer that looks up messages
+// in the bundle according to the language preferences in langs.
+// It can parse Accept-Language headers as defined in http://www.ietf.org/rfc/rfc2616.txt.
+func NewLocalizer(bundle *Bundle, langs ...string) *Localizer {
+ return &Localizer{
+ bundle: bundle,
+ tags: parseTags(langs),
+ }
+}
+
+func parseTags(langs []string) []language.Tag {
+ tags := []language.Tag{}
+ for _, lang := range langs {
+ t, _, err := language.ParseAcceptLanguage(lang)
+ if err != nil {
+ continue
+ }
+ tags = append(tags, t...)
+ }
+ return tags
+}
+
+// LocalizeConfig configures a call to the Localize method on Localizer.
+type LocalizeConfig struct {
+ // MessageID is the id of the message to lookup.
+ // This field is ignored if DefaultMessage is set.
+ MessageID string
+
+ // TemplateData is the data passed when executing the message's template.
+ // If TemplateData is nil and PluralCount is not nil, then the message template
+ // will be executed with data that contains the plural count.
+ TemplateData interface{}
+
+ // PluralCount determines which plural form of the message is used.
+ PluralCount interface{}
+
+ // DefaultMessage is used if the message is not found in any message files.
+ DefaultMessage *Message
+
+ // Funcs is used to extend the Go template engine's built in functions
+ Funcs template.FuncMap
+}
+
+type invalidPluralCountErr struct {
+ messageID string
+ pluralCount interface{}
+ err error
+}
+
+func (e *invalidPluralCountErr) Error() string {
+ return fmt.Sprintf("invalid plural count %#v for message id %q: %s", e.pluralCount, e.messageID, e.err)
+}
+
+// MessageNotFoundErr is returned from Localize when a message could not be found.
+type MessageNotFoundErr struct {
+ tag language.Tag
+ messageID string
+}
+
+func (e *MessageNotFoundErr) Error() string {
+ return fmt.Sprintf("message %q not found in language %q", e.messageID, e.tag)
+}
+
+type pluralizeErr struct {
+ messageID string
+ tag language.Tag
+}
+
+func (e *pluralizeErr) Error() string {
+ return fmt.Sprintf("unable to pluralize %q because there no plural rule for %q", e.messageID, e.tag)
+}
+
+type messageIDMismatchErr struct {
+ messageID string
+ defaultMessageID string
+}
+
+func (e *messageIDMismatchErr) Error() string {
+ return fmt.Sprintf("message id %q does not match default message id %q", e.messageID, e.defaultMessageID)
+}
+
+// Localize returns a localized message.
+func (l *Localizer) Localize(lc *LocalizeConfig) (string, error) {
+ msg, _, err := l.LocalizeWithTag(lc)
+ return msg, err
+}
+
+// Localize returns a localized message.
+func (l *Localizer) LocalizeMessage(msg *Message) (string, error) {
+ return l.Localize(&LocalizeConfig{
+ DefaultMessage: msg,
+ })
+}
+
+// TODO: uncomment this (and the test) when extract has been updated to extract these call sites too.
+// Localize returns a localized message.
+// func (l *Localizer) LocalizeMessageID(messageID string) (string, error) {
+// return l.Localize(&LocalizeConfig{
+// MessageID: messageID,
+// })
+// }
+
+// LocalizeWithTag returns a localized message and the language tag.
+// It may return a best effort localized message even if an error happens.
+func (l *Localizer) LocalizeWithTag(lc *LocalizeConfig) (string, language.Tag, error) {
+ messageID := lc.MessageID
+ if lc.DefaultMessage != nil {
+ if messageID != "" && messageID != lc.DefaultMessage.ID {
+ return "", language.Und, &messageIDMismatchErr{messageID: messageID, defaultMessageID: lc.DefaultMessage.ID}
+ }
+ messageID = lc.DefaultMessage.ID
+ }
+
+ var operands *plural.Operands
+ templateData := lc.TemplateData
+ if lc.PluralCount != nil {
+ var err error
+ operands, err = plural.NewOperands(lc.PluralCount)
+ if err != nil {
+ return "", language.Und, &invalidPluralCountErr{messageID: messageID, pluralCount: lc.PluralCount, err: err}
+ }
+ if templateData == nil {
+ templateData = map[string]interface{}{
+ "PluralCount": lc.PluralCount,
+ }
+ }
+ }
+
+ tag, template, err := l.getMessageTemplate(messageID, lc.DefaultMessage)
+ if template == nil {
+ return "", language.Und, err
+ }
+
+ pluralForm := l.pluralForm(tag, operands)
+ msg, err2 := template.Execute(pluralForm, templateData, lc.Funcs)
+ if err2 != nil {
+ if err == nil {
+ err = err2
+ }
+
+ // Attempt to fallback to "Other" pluralization in case translations are incomplete.
+ if pluralForm != plural.Other {
+ msg2, err3 := template.Execute(plural.Other, templateData, lc.Funcs)
+ if err3 == nil {
+ msg = msg2
+ }
+ }
+ }
+ return msg, tag, err
+}
+
+func (l *Localizer) getMessageTemplate(id string, defaultMessage *Message) (language.Tag, *MessageTemplate, error) {
+ _, i, _ := l.bundle.matcher.Match(l.tags...)
+ tag := l.bundle.tags[i]
+ mt := l.bundle.getMessageTemplate(tag, id)
+ if mt != nil {
+ return tag, mt, nil
+ }
+
+ if tag == l.bundle.defaultLanguage {
+ if defaultMessage == nil {
+ return language.Und, nil, &MessageNotFoundErr{tag: tag, messageID: id}
+ }
+ return tag, NewMessageTemplate(defaultMessage), nil
+ }
+
+ // Fallback to default language in bundle.
+ mt = l.bundle.getMessageTemplate(l.bundle.defaultLanguage, id)
+ if mt != nil {
+ return l.bundle.defaultLanguage, mt, &MessageNotFoundErr{tag: tag, messageID: id}
+ }
+
+ // Fallback to default message.
+ if defaultMessage == nil {
+ return language.Und, nil, &MessageNotFoundErr{tag: tag, messageID: id}
+ }
+ return l.bundle.defaultLanguage, NewMessageTemplate(defaultMessage), &MessageNotFoundErr{tag: tag, messageID: id}
+}
+
+func (l *Localizer) pluralForm(tag language.Tag, operands *plural.Operands) plural.Form {
+ if operands == nil {
+ return plural.Other
+ }
+ return l.bundle.pluralRules.Rule(tag).PluralFormFunc(operands)
+}
+
+// MustLocalize is similar to Localize, except it panics if an error happens.
+func (l *Localizer) MustLocalize(lc *LocalizeConfig) string {
+ localized, err := l.Localize(lc)
+ if err != nil {
+ panic(err)
+ }
+ return localized
+}
diff --git a/vendor/github.com/nicksnyder/go-i18n/v2/i18n/message.go b/vendor/github.com/nicksnyder/go-i18n/v2/i18n/message.go
new file mode 100644
index 0000000..f8f789a
--- /dev/null
+++ b/vendor/github.com/nicksnyder/go-i18n/v2/i18n/message.go
@@ -0,0 +1,221 @@
+package i18n
+
+import (
+ "fmt"
+ "strings"
+)
+
+// Message is a string that can be localized.
+type Message struct {
+ // ID uniquely identifies the message.
+ ID string
+
+ // Hash uniquely identifies the content of the message
+ // that this message was translated from.
+ Hash string
+
+ // Description describes the message to give additional
+ // context to translators that may be relevant for translation.
+ Description string
+
+ // LeftDelim is the left Go template delimiter.
+ LeftDelim string
+
+ // RightDelim is the right Go template delimiter.``
+ RightDelim string
+
+ // Zero is the content of the message for the CLDR plural form "zero".
+ Zero string
+
+ // One is the content of the message for the CLDR plural form "one".
+ One string
+
+ // Two is the content of the message for the CLDR plural form "two".
+ Two string
+
+ // Few is the content of the message for the CLDR plural form "few".
+ Few string
+
+ // Many is the content of the message for the CLDR plural form "many".
+ Many string
+
+ // Other is the content of the message for the CLDR plural form "other".
+ Other string
+}
+
+// NewMessage parses data and returns a new message.
+func NewMessage(data interface{}) (*Message, error) {
+ m := &Message{}
+ if err := m.unmarshalInterface(data); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+// MustNewMessage is similar to NewMessage except it panics if an error happens.
+func MustNewMessage(data interface{}) *Message {
+ m, err := NewMessage(data)
+ if err != nil {
+ panic(err)
+ }
+ return m
+}
+
+// unmarshalInterface unmarshals a message from data.
+func (m *Message) unmarshalInterface(v interface{}) error {
+ strdata, err := stringMap(v)
+ if err != nil {
+ return err
+ }
+ for k, v := range strdata {
+ switch strings.ToLower(k) {
+ case "id":
+ m.ID = v
+ case "description":
+ m.Description = v
+ case "hash":
+ m.Hash = v
+ case "leftdelim":
+ m.LeftDelim = v
+ case "rightdelim":
+ m.RightDelim = v
+ case "zero":
+ m.Zero = v
+ case "one":
+ m.One = v
+ case "two":
+ m.Two = v
+ case "few":
+ m.Few = v
+ case "many":
+ m.Many = v
+ case "other":
+ m.Other = v
+ }
+ }
+ return nil
+}
+
+type keyTypeErr struct {
+ key interface{}
+}
+
+func (err *keyTypeErr) Error() string {
+ return fmt.Sprintf("expected key to be a string but got %#v", err.key)
+}
+
+type valueTypeErr struct {
+ value interface{}
+}
+
+func (err *valueTypeErr) Error() string {
+ return fmt.Sprintf("unsupported type %#v", err.value)
+}
+
+func stringMap(v interface{}) (map[string]string, error) {
+ switch value := v.(type) {
+ case string:
+ return map[string]string{
+ "other": value,
+ }, nil
+ case map[string]string:
+ return value, nil
+ case map[string]interface{}:
+ strdata := make(map[string]string, len(value))
+ for k, v := range value {
+ err := stringSubmap(k, v, strdata)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return strdata, nil
+ case map[interface{}]interface{}:
+ strdata := make(map[string]string, len(value))
+ for k, v := range value {
+ kstr, ok := k.(string)
+ if !ok {
+ return nil, &keyTypeErr{key: k}
+ }
+ err := stringSubmap(kstr, v, strdata)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return strdata, nil
+ default:
+ return nil, &valueTypeErr{value: value}
+ }
+}
+
+func stringSubmap(k string, v interface{}, strdata map[string]string) error {
+ if k == "translation" {
+ switch vt := v.(type) {
+ case string:
+ strdata["other"] = vt
+ default:
+ v1Message, err := stringMap(v)
+ if err != nil {
+ return err
+ }
+ for kk, vv := range v1Message {
+ strdata[kk] = vv
+ }
+ }
+ return nil
+ }
+
+ switch vt := v.(type) {
+ case string:
+ strdata[k] = vt
+ return nil
+ case nil:
+ return nil
+ default:
+ return fmt.Errorf("expected value for key %q be a string but got %#v", k, v)
+ }
+}
+
+// isMessage tells whether the given data is a message, or a map containing
+// nested messages.
+// A map is assumed to be a message if it contains any of the "reserved" keys:
+// "id", "description", "hash", "leftdelim", "rightdelim", "zero", "one", "two", "few", "many", "other"
+// with a string value.
+// e.g.,
+// - {"message": {"description": "world"}} is a message
+// - {"message": {"description": "world", "foo": "bar"}} is a message ("foo" key is ignored)
+// - {"notmessage": {"description": {"hello": "world"}}} is not
+// - {"notmessage": {"foo": "bar"}} is not
+func isMessage(v interface{}) bool {
+ reservedKeys := []string{"id", "description", "hash", "leftdelim", "rightdelim", "zero", "one", "two", "few", "many", "other"}
+ switch data := v.(type) {
+ case string:
+ return true
+ case map[string]interface{}:
+ for _, key := range reservedKeys {
+ val, ok := data[key]
+ if !ok {
+ continue
+ }
+ _, ok = val.(string)
+ if !ok {
+ continue
+ }
+ // v is a message if it contains a "reserved" key holding a string value
+ return true
+ }
+ case map[interface{}]interface{}:
+ for _, key := range reservedKeys {
+ val, ok := data[key]
+ if !ok {
+ continue
+ }
+ _, ok = val.(string)
+ if !ok {
+ continue
+ }
+ // v is a message if it contains a "reserved" key holding a string value
+ return true
+ }
+ }
+ return false
+}
diff --git a/vendor/github.com/nicksnyder/go-i18n/v2/i18n/message_template.go b/vendor/github.com/nicksnyder/go-i18n/v2/i18n/message_template.go
new file mode 100644
index 0000000..a1a619e
--- /dev/null
+++ b/vendor/github.com/nicksnyder/go-i18n/v2/i18n/message_template.go
@@ -0,0 +1,65 @@
+package i18n
+
+import (
+ "fmt"
+
+ "text/template"
+
+ "github.com/nicksnyder/go-i18n/v2/internal"
+ "github.com/nicksnyder/go-i18n/v2/internal/plural"
+)
+
+// MessageTemplate is an executable template for a message.
+type MessageTemplate struct {
+ *Message
+ PluralTemplates map[plural.Form]*internal.Template
+}
+
+// NewMessageTemplate returns a new message template.
+func NewMessageTemplate(m *Message) *MessageTemplate {
+ pluralTemplates := map[plural.Form]*internal.Template{}
+ setPluralTemplate(pluralTemplates, plural.Zero, m.Zero, m.LeftDelim, m.RightDelim)
+ setPluralTemplate(pluralTemplates, plural.One, m.One, m.LeftDelim, m.RightDelim)
+ setPluralTemplate(pluralTemplates, plural.Two, m.Two, m.LeftDelim, m.RightDelim)
+ setPluralTemplate(pluralTemplates, plural.Few, m.Few, m.LeftDelim, m.RightDelim)
+ setPluralTemplate(pluralTemplates, plural.Many, m.Many, m.LeftDelim, m.RightDelim)
+ setPluralTemplate(pluralTemplates, plural.Other, m.Other, m.LeftDelim, m.RightDelim)
+ if len(pluralTemplates) == 0 {
+ return nil
+ }
+ return &MessageTemplate{
+ Message: m,
+ PluralTemplates: pluralTemplates,
+ }
+}
+
+func setPluralTemplate(pluralTemplates map[plural.Form]*internal.Template, pluralForm plural.Form, src, leftDelim, rightDelim string) {
+ if src != "" {
+ pluralTemplates[pluralForm] = &internal.Template{
+ Src: src,
+ LeftDelim: leftDelim,
+ RightDelim: rightDelim,
+ }
+ }
+}
+
+type pluralFormNotFoundError struct {
+ pluralForm plural.Form
+ messageID string
+}
+
+func (e pluralFormNotFoundError) Error() string {
+ return fmt.Sprintf("message %q has no plural form %q", e.messageID, e.pluralForm)
+}
+
+// Execute executes the template for the plural form and template data.
+func (mt *MessageTemplate) Execute(pluralForm plural.Form, data interface{}, funcs template.FuncMap) (string, error) {
+ t := mt.PluralTemplates[pluralForm]
+ if t == nil {
+ return "", pluralFormNotFoundError{
+ pluralForm: pluralForm,
+ messageID: mt.Message.ID,
+ }
+ }
+ return t.Execute(funcs, data)
+}
diff --git a/vendor/github.com/nicksnyder/go-i18n/v2/i18n/parse.go b/vendor/github.com/nicksnyder/go-i18n/v2/i18n/parse.go
new file mode 100644
index 0000000..57dd7fe
--- /dev/null
+++ b/vendor/github.com/nicksnyder/go-i18n/v2/i18n/parse.go
@@ -0,0 +1,166 @@
+package i18n
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+
+ "golang.org/x/text/language"
+)
+
+// MessageFile represents a parsed message file.
+type MessageFile struct {
+ Path string
+ Tag language.Tag
+ Format string
+ Messages []*Message
+}
+
+// ParseMessageFileBytes returns the messages parsed from file.
+func ParseMessageFileBytes(buf []byte, path string, unmarshalFuncs map[string]UnmarshalFunc) (*MessageFile, error) {
+ lang, format := parsePath(path)
+ tag := language.Make(lang)
+ messageFile := &MessageFile{
+ Path: path,
+ Tag: tag,
+ Format: format,
+ }
+ if len(buf) == 0 {
+ return messageFile, nil
+ }
+ unmarshalFunc := unmarshalFuncs[messageFile.Format]
+ if unmarshalFunc == nil {
+ if messageFile.Format == "json" {
+ unmarshalFunc = json.Unmarshal
+ } else {
+ return nil, fmt.Errorf("no unmarshaler registered for %s", messageFile.Format)
+ }
+ }
+ var err error
+ var raw interface{}
+ if err = unmarshalFunc(buf, &raw); err != nil {
+ return nil, err
+ }
+
+ if messageFile.Messages, err = recGetMessages(raw, isMessage(raw), true); err != nil {
+ return nil, err
+ }
+
+ return messageFile, nil
+}
+
+const nestedSeparator = "."
+
+var errInvalidTranslationFile = errors.New("invalid translation file, expected key-values, got a single value")
+
+// recGetMessages looks for translation messages inside "raw" parameter,
+// scanning nested maps using recursion.
+func recGetMessages(raw interface{}, isMapMessage, isInitialCall bool) ([]*Message, error) {
+ var messages []*Message
+ var err error
+
+ switch data := raw.(type) {
+ case string:
+ if isInitialCall {
+ return nil, errInvalidTranslationFile
+ }
+ m, err := NewMessage(data)
+ return []*Message{m}, err
+
+ case map[string]interface{}:
+ if isMapMessage {
+ m, err := NewMessage(data)
+ return []*Message{m}, err
+ }
+ messages = make([]*Message, 0, len(data))
+ for id, data := range data {
+ // recursively scan map items
+ messages, err = addChildMessages(id, data, messages)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ case map[interface{}]interface{}:
+ if isMapMessage {
+ m, err := NewMessage(data)
+ return []*Message{m}, err
+ }
+ messages = make([]*Message, 0, len(data))
+ for id, data := range data {
+ strid, ok := id.(string)
+ if !ok {
+ return nil, fmt.Errorf("expected key to be string but got %#v", id)
+ }
+ // recursively scan map items
+ messages, err = addChildMessages(strid, data, messages)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ case []interface{}:
+ // Backward compatibility for v1 file format.
+ messages = make([]*Message, 0, len(data))
+ for _, data := range data {
+ // recursively scan slice items
+ childMessages, err := recGetMessages(data, isMessage(data), false)
+ if err != nil {
+ return nil, err
+ }
+ messages = append(messages, childMessages...)
+ }
+
+ default:
+ return nil, fmt.Errorf("unsupported file format %T", raw)
+ }
+
+ return messages, nil
+}
+
+func addChildMessages(id string, data interface{}, messages []*Message) ([]*Message, error) {
+ isChildMessage := isMessage(data)
+ childMessages, err := recGetMessages(data, isChildMessage, false)
+ if err != nil {
+ return nil, err
+ }
+ for _, m := range childMessages {
+ if isChildMessage {
+ if m.ID == "" {
+ m.ID = id // start with innermost key
+ }
+ } else {
+ m.ID = id + nestedSeparator + m.ID // update ID with each nested key on the way
+ }
+ messages = append(messages, m)
+ }
+ return messages, nil
+}
+
+func parsePath(path string) (langTag, format string) {
+ formatStartIdx := -1
+ for i := len(path) - 1; i >= 0; i-- {
+ c := path[i]
+ if os.IsPathSeparator(c) {
+ if formatStartIdx != -1 {
+ langTag = path[i+1 : formatStartIdx]
+ }
+ return
+ }
+ if path[i] == '.' {
+ if formatStartIdx != -1 {
+ langTag = path[i+1 : formatStartIdx]
+ return
+ }
+ if formatStartIdx == -1 {
+ format = path[i+1:]
+ formatStartIdx = i
+ }
+ }
+ }
+ if formatStartIdx != -1 {
+ langTag = path[:formatStartIdx]
+ }
+ return
+}
diff --git a/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/doc.go b/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/doc.go
new file mode 100644
index 0000000..c2a71d5
--- /dev/null
+++ b/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/doc.go
@@ -0,0 +1,3 @@
+// Package plural provides support for pluralizing messages
+// according to CLDR rules http://cldr.unicode.org/index/cldr-spec/plural-rules
+package plural
diff --git a/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/form.go b/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/form.go
new file mode 100644
index 0000000..287a87f
--- /dev/null
+++ b/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/form.go
@@ -0,0 +1,16 @@
+package plural
+
+// Form represents a language pluralization form as defined here:
+// http://cldr.unicode.org/index/cldr-spec/plural-rules
+type Form string
+
+// All defined plural forms.
+const (
+ Invalid Form = ""
+ Zero Form = "zero"
+ One Form = "one"
+ Two Form = "two"
+ Few Form = "few"
+ Many Form = "many"
+ Other Form = "other"
+)
diff --git a/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/operands.go b/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/operands.go
new file mode 100644
index 0000000..4d29726
--- /dev/null
+++ b/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/operands.go
@@ -0,0 +1,120 @@
+package plural
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+// Operands is a representation of http://unicode.org/reports/tr35/tr35-numbers.html#Operands
+type Operands struct {
+ N float64 // absolute value of the source number (integer and decimals)
+ I int64 // integer digits of n
+ V int64 // number of visible fraction digits in n, with trailing zeros
+ W int64 // number of visible fraction digits in n, without trailing zeros
+ F int64 // visible fractional digits in n, with trailing zeros
+ T int64 // visible fractional digits in n, without trailing zeros
+}
+
+// NEqualsAny returns true if o represents an integer equal to any of the arguments.
+func (o *Operands) NEqualsAny(any ...int64) bool {
+ for _, i := range any {
+ if o.I == i && o.T == 0 {
+ return true
+ }
+ }
+ return false
+}
+
+// NModEqualsAny returns true if o represents an integer equal to any of the arguments modulo mod.
+func (o *Operands) NModEqualsAny(mod int64, any ...int64) bool {
+ modI := o.I % mod
+ for _, i := range any {
+ if modI == i && o.T == 0 {
+ return true
+ }
+ }
+ return false
+}
+
+// NInRange returns true if o represents an integer in the closed interval [from, to].
+func (o *Operands) NInRange(from, to int64) bool {
+ return o.T == 0 && from <= o.I && o.I <= to
+}
+
+// NModInRange returns true if o represents an integer in the closed interval [from, to] modulo mod.
+func (o *Operands) NModInRange(mod, from, to int64) bool {
+ modI := o.I % mod
+ return o.T == 0 && from <= modI && modI <= to
+}
+
+// NewOperands returns the operands for number.
+func NewOperands(number interface{}) (*Operands, error) {
+ switch number := number.(type) {
+ case int:
+ return newOperandsInt64(int64(number)), nil
+ case int8:
+ return newOperandsInt64(int64(number)), nil
+ case int16:
+ return newOperandsInt64(int64(number)), nil
+ case int32:
+ return newOperandsInt64(int64(number)), nil
+ case int64:
+ return newOperandsInt64(number), nil
+ case string:
+ return newOperandsString(number)
+ case float32, float64:
+ return nil, fmt.Errorf("floats should be formatted into a string")
+ default:
+ return nil, fmt.Errorf("invalid type %T; expected integer or string", number)
+ }
+}
+
+func newOperandsInt64(i int64) *Operands {
+ if i < 0 {
+ i = -i
+ }
+ return &Operands{float64(i), i, 0, 0, 0, 0}
+}
+
+func newOperandsString(s string) (*Operands, error) {
+ if s[0] == '-' {
+ s = s[1:]
+ }
+ n, err := strconv.ParseFloat(s, 64)
+ if err != nil {
+ return nil, err
+ }
+ ops := &Operands{N: n}
+ parts := strings.SplitN(s, ".", 2)
+ ops.I, err = strconv.ParseInt(parts[0], 10, 64)
+ if err != nil {
+ return nil, err
+ }
+ if len(parts) == 1 {
+ return ops, nil
+ }
+ fraction := parts[1]
+ ops.V = int64(len(fraction))
+ for i := ops.V - 1; i >= 0; i-- {
+ if fraction[i] != '0' {
+ ops.W = i + 1
+ break
+ }
+ }
+ if ops.V > 0 {
+ f, err := strconv.ParseInt(fraction, 10, 0)
+ if err != nil {
+ return nil, err
+ }
+ ops.F = f
+ }
+ if ops.W > 0 {
+ t, err := strconv.ParseInt(fraction[:ops.W], 10, 0)
+ if err != nil {
+ return nil, err
+ }
+ ops.T = t
+ }
+ return ops, nil
+}
diff --git a/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rule.go b/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rule.go
new file mode 100644
index 0000000..0869c84
--- /dev/null
+++ b/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rule.go
@@ -0,0 +1,44 @@
+package plural
+
+import (
+ "golang.org/x/text/language"
+)
+
+// Rule defines the CLDR plural rules for a language.
+// http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
+// http://unicode.org/reports/tr35/tr35-numbers.html#Operands
+type Rule struct {
+ PluralForms map[Form]struct{}
+ PluralFormFunc func(*Operands) Form
+}
+
+func addPluralRules(rules Rules, ids []string, ps *Rule) {
+ for _, id := range ids {
+ if id == "root" {
+ continue
+ }
+ tag := language.MustParse(id)
+ rules[tag] = ps
+ }
+}
+
+func newPluralFormSet(pluralForms ...Form) map[Form]struct{} {
+ set := make(map[Form]struct{}, len(pluralForms))
+ for _, plural := range pluralForms {
+ set[plural] = struct{}{}
+ }
+ return set
+}
+
+func intInRange(i, from, to int64) bool {
+ return from <= i && i <= to
+}
+
+func intEqualsAny(i int64, any ...int64) bool {
+ for _, a := range any {
+ if i == a {
+ return true
+ }
+ }
+ return false
+}
diff --git a/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rule_gen.go b/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rule_gen.go
new file mode 100644
index 0000000..9a32ca4
--- /dev/null
+++ b/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rule_gen.go
@@ -0,0 +1,589 @@
+// This file is generated by i18n/plural/codegen/generate.sh; DO NOT EDIT
+
+package plural
+
+// DefaultRules returns a map of Rules generated from CLDR language data.
+func DefaultRules() Rules {
+ rules := Rules{}
+
+ addPluralRules(rules, []string{"bm", "bo", "dz", "id", "ig", "ii", "in", "ja", "jbo", "jv", "jw", "kde", "kea", "km", "ko", "lkt", "lo", "ms", "my", "nqo", "osa", "root", "sah", "ses", "sg", "su", "th", "to", "vi", "wo", "yo", "yue", "zh"}, &Rule{
+ PluralForms: newPluralFormSet(Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"am", "as", "bn", "fa", "gu", "hi", "kn", "pcm", "zu"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 0 or n = 1
+ if intEqualsAny(ops.I, 0) ||
+ ops.NEqualsAny(1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"ff", "fr", "hy", "kab"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 0,1
+ if intEqualsAny(ops.I, 0, 1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"pt"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 0..1
+ if intInRange(ops.I, 0, 1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"ast", "ca", "de", "en", "et", "fi", "fy", "gl", "ia", "io", "it", "ji", "nl", "pt_PT", "sc", "scn", "sv", "sw", "ur", "yi"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 1 and v = 0
+ if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"si"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0,1 or i = 0 and f = 1
+ if ops.NEqualsAny(0, 1) ||
+ intEqualsAny(ops.I, 0) && intEqualsAny(ops.F, 1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"ak", "bho", "guw", "ln", "mg", "nso", "pa", "ti", "wa"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0..1
+ if ops.NInRange(0, 1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"tzm"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0..1 or n = 11..99
+ if ops.NInRange(0, 1) ||
+ ops.NInRange(11, 99) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"af", "an", "asa", "az", "bem", "bez", "bg", "brx", "ce", "cgg", "chr", "ckb", "dv", "ee", "el", "eo", "es", "eu", "fo", "fur", "gsw", "ha", "haw", "hu", "jgo", "jmc", "ka", "kaj", "kcg", "kk", "kkj", "kl", "ks", "ksb", "ku", "ky", "lb", "lg", "mas", "mgo", "ml", "mn", "mr", "nah", "nb", "nd", "ne", "nn", "nnh", "no", "nr", "ny", "nyn", "om", "or", "os", "pap", "ps", "rm", "rof", "rwk", "saq", "sd", "sdh", "seh", "sn", "so", "sq", "ss", "ssy", "st", "syr", "ta", "te", "teo", "tig", "tk", "tn", "tr", "ts", "ug", "uz", "ve", "vo", "vun", "wae", "xh", "xog"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"da"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1 or t != 0 and i = 0,1
+ if ops.NEqualsAny(1) ||
+ !intEqualsAny(ops.T, 0) && intEqualsAny(ops.I, 0, 1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"is"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // t = 0 and i % 10 = 1 and i % 100 != 11 or t != 0
+ if intEqualsAny(ops.T, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) ||
+ !intEqualsAny(ops.T, 0) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"mk"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) ||
+ intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"ceb", "fil", "tl"}, &Rule{
+ PluralForms: newPluralFormSet(One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // v = 0 and i = 1,2,3 or v = 0 and i % 10 != 4,6,9 or v != 0 and f % 10 != 4,6,9
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I, 1, 2, 3) ||
+ intEqualsAny(ops.V, 0) && !intEqualsAny(ops.I%10, 4, 6, 9) ||
+ !intEqualsAny(ops.V, 0) && !intEqualsAny(ops.F%10, 4, 6, 9) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"lv", "prg"}, &Rule{
+ PluralForms: newPluralFormSet(Zero, One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n % 10 = 0 or n % 100 = 11..19 or v = 2 and f % 100 = 11..19
+ if ops.NModEqualsAny(10, 0) ||
+ ops.NModInRange(100, 11, 19) ||
+ intEqualsAny(ops.V, 2) && intInRange(ops.F%100, 11, 19) {
+ return Zero
+ }
+ // n % 10 = 1 and n % 100 != 11 or v = 2 and f % 10 = 1 and f % 100 != 11 or v != 2 and f % 10 = 1
+ if ops.NModEqualsAny(10, 1) && !ops.NModEqualsAny(100, 11) ||
+ intEqualsAny(ops.V, 2) && intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) ||
+ !intEqualsAny(ops.V, 2) && intEqualsAny(ops.F%10, 1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"lag"}, &Rule{
+ PluralForms: newPluralFormSet(Zero, One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0
+ if ops.NEqualsAny(0) {
+ return Zero
+ }
+ // i = 0,1 and n != 0
+ if intEqualsAny(ops.I, 0, 1) && !ops.NEqualsAny(0) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"ksh"}, &Rule{
+ PluralForms: newPluralFormSet(Zero, One, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0
+ if ops.NEqualsAny(0) {
+ return Zero
+ }
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"iu", "naq", "sat", "se", "sma", "smi", "smj", "smn", "sms"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 2
+ if ops.NEqualsAny(2) {
+ return Two
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"shi"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 0 or n = 1
+ if intEqualsAny(ops.I, 0) ||
+ ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 2..10
+ if ops.NInRange(2, 10) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"mo", "ro"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 1 and v = 0
+ if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
+ return One
+ }
+ // v != 0 or n = 0 or n % 100 = 2..19
+ if !intEqualsAny(ops.V, 0) ||
+ ops.NEqualsAny(0) ||
+ ops.NModInRange(100, 2, 19) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"bs", "hr", "sh", "sr"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) ||
+ intEqualsAny(ops.F%10, 1) && !intEqualsAny(ops.F%100, 11) {
+ return One
+ }
+ // v = 0 and i % 10 = 2..4 and i % 100 != 12..14 or f % 10 = 2..4 and f % 100 != 12..14
+ if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) ||
+ intInRange(ops.F%10, 2, 4) && !intInRange(ops.F%100, 12, 14) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"gd"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1,11
+ if ops.NEqualsAny(1, 11) {
+ return One
+ }
+ // n = 2,12
+ if ops.NEqualsAny(2, 12) {
+ return Two
+ }
+ // n = 3..10,13..19
+ if ops.NInRange(3, 10) || ops.NInRange(13, 19) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"sl"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // v = 0 and i % 100 = 1
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 1) {
+ return One
+ }
+ // v = 0 and i % 100 = 2
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 2) {
+ return Two
+ }
+ // v = 0 and i % 100 = 3..4 or v != 0
+ if intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 3, 4) ||
+ !intEqualsAny(ops.V, 0) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"dsb", "hsb"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // v = 0 and i % 100 = 1 or f % 100 = 1
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 1) ||
+ intEqualsAny(ops.F%100, 1) {
+ return One
+ }
+ // v = 0 and i % 100 = 2 or f % 100 = 2
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 2) ||
+ intEqualsAny(ops.F%100, 2) {
+ return Two
+ }
+ // v = 0 and i % 100 = 3..4 or f % 100 = 3..4
+ if intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 3, 4) ||
+ intInRange(ops.F%100, 3, 4) {
+ return Few
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"he", "iw"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 1 and v = 0
+ if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
+ return One
+ }
+ // i = 2 and v = 0
+ if intEqualsAny(ops.I, 2) && intEqualsAny(ops.V, 0) {
+ return Two
+ }
+ // v = 0 and n != 0..10 and n % 10 = 0
+ if intEqualsAny(ops.V, 0) && !ops.NInRange(0, 10) && ops.NModEqualsAny(10, 0) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"cs", "sk"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 1 and v = 0
+ if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
+ return One
+ }
+ // i = 2..4 and v = 0
+ if intInRange(ops.I, 2, 4) && intEqualsAny(ops.V, 0) {
+ return Few
+ }
+ // v != 0
+ if !intEqualsAny(ops.V, 0) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"pl"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // i = 1 and v = 0
+ if intEqualsAny(ops.I, 1) && intEqualsAny(ops.V, 0) {
+ return One
+ }
+ // v = 0 and i % 10 = 2..4 and i % 100 != 12..14
+ if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) {
+ return Few
+ }
+ // v = 0 and i != 1 and i % 10 = 0..1 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 12..14
+ if intEqualsAny(ops.V, 0) && !intEqualsAny(ops.I, 1) && intInRange(ops.I%10, 0, 1) ||
+ intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 5, 9) ||
+ intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 12, 14) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"be"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n % 10 = 1 and n % 100 != 11
+ if ops.NModEqualsAny(10, 1) && !ops.NModEqualsAny(100, 11) {
+ return One
+ }
+ // n % 10 = 2..4 and n % 100 != 12..14
+ if ops.NModInRange(10, 2, 4) && !ops.NModInRange(100, 12, 14) {
+ return Few
+ }
+ // n % 10 = 0 or n % 10 = 5..9 or n % 100 = 11..14
+ if ops.NModEqualsAny(10, 0) ||
+ ops.NModInRange(10, 5, 9) ||
+ ops.NModInRange(100, 11, 14) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"lt"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n % 10 = 1 and n % 100 != 11..19
+ if ops.NModEqualsAny(10, 1) && !ops.NModInRange(100, 11, 19) {
+ return One
+ }
+ // n % 10 = 2..9 and n % 100 != 11..19
+ if ops.NModInRange(10, 2, 9) && !ops.NModInRange(100, 11, 19) {
+ return Few
+ }
+ // f != 0
+ if !intEqualsAny(ops.F, 0) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"mt"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 0 or n % 100 = 2..10
+ if ops.NEqualsAny(0) ||
+ ops.NModInRange(100, 2, 10) {
+ return Few
+ }
+ // n % 100 = 11..19
+ if ops.NModInRange(100, 11, 19) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"ru", "uk"}, &Rule{
+ PluralForms: newPluralFormSet(One, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // v = 0 and i % 10 = 1 and i % 100 != 11
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) && !intEqualsAny(ops.I%100, 11) {
+ return One
+ }
+ // v = 0 and i % 10 = 2..4 and i % 100 != 12..14
+ if intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 2, 4) && !intInRange(ops.I%100, 12, 14) {
+ return Few
+ }
+ // v = 0 and i % 10 = 0 or v = 0 and i % 10 = 5..9 or v = 0 and i % 100 = 11..14
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 0) ||
+ intEqualsAny(ops.V, 0) && intInRange(ops.I%10, 5, 9) ||
+ intEqualsAny(ops.V, 0) && intInRange(ops.I%100, 11, 14) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"br"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n % 10 = 1 and n % 100 != 11,71,91
+ if ops.NModEqualsAny(10, 1) && !ops.NModEqualsAny(100, 11, 71, 91) {
+ return One
+ }
+ // n % 10 = 2 and n % 100 != 12,72,92
+ if ops.NModEqualsAny(10, 2) && !ops.NModEqualsAny(100, 12, 72, 92) {
+ return Two
+ }
+ // n % 10 = 3..4,9 and n % 100 != 10..19,70..79,90..99
+ if (ops.NModInRange(10, 3, 4) || ops.NModEqualsAny(10, 9)) && !(ops.NModInRange(100, 10, 19) || ops.NModInRange(100, 70, 79) || ops.NModInRange(100, 90, 99)) {
+ return Few
+ }
+ // n != 0 and n % 1000000 = 0
+ if !ops.NEqualsAny(0) && ops.NModEqualsAny(1000000, 0) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"ga"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 2
+ if ops.NEqualsAny(2) {
+ return Two
+ }
+ // n = 3..6
+ if ops.NInRange(3, 6) {
+ return Few
+ }
+ // n = 7..10
+ if ops.NInRange(7, 10) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"gv"}, &Rule{
+ PluralForms: newPluralFormSet(One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // v = 0 and i % 10 = 1
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 1) {
+ return One
+ }
+ // v = 0 and i % 10 = 2
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%10, 2) {
+ return Two
+ }
+ // v = 0 and i % 100 = 0,20,40,60,80
+ if intEqualsAny(ops.V, 0) && intEqualsAny(ops.I%100, 0, 20, 40, 60, 80) {
+ return Few
+ }
+ // v != 0
+ if !intEqualsAny(ops.V, 0) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"kw"}, &Rule{
+ PluralForms: newPluralFormSet(Zero, One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0
+ if ops.NEqualsAny(0) {
+ return Zero
+ }
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n % 100 = 2,22,42,62,82 or n % 1000 = 0 and n % 100000 = 1000..20000,40000,60000,80000 or n != 0 and n % 1000000 = 100000
+ if ops.NModEqualsAny(100, 2, 22, 42, 62, 82) ||
+ ops.NModEqualsAny(1000, 0) && (ops.NModInRange(100000, 1000, 20000) || ops.NModEqualsAny(100000, 40000, 60000, 80000)) ||
+ !ops.NEqualsAny(0) && ops.NModEqualsAny(1000000, 100000) {
+ return Two
+ }
+ // n % 100 = 3,23,43,63,83
+ if ops.NModEqualsAny(100, 3, 23, 43, 63, 83) {
+ return Few
+ }
+ // n != 1 and n % 100 = 1,21,41,61,81
+ if !ops.NEqualsAny(1) && ops.NModEqualsAny(100, 1, 21, 41, 61, 81) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"ar", "ars"}, &Rule{
+ PluralForms: newPluralFormSet(Zero, One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0
+ if ops.NEqualsAny(0) {
+ return Zero
+ }
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 2
+ if ops.NEqualsAny(2) {
+ return Two
+ }
+ // n % 100 = 3..10
+ if ops.NModInRange(100, 3, 10) {
+ return Few
+ }
+ // n % 100 = 11..99
+ if ops.NModInRange(100, 11, 99) {
+ return Many
+ }
+ return Other
+ },
+ })
+ addPluralRules(rules, []string{"cy"}, &Rule{
+ PluralForms: newPluralFormSet(Zero, One, Two, Few, Many, Other),
+ PluralFormFunc: func(ops *Operands) Form {
+ // n = 0
+ if ops.NEqualsAny(0) {
+ return Zero
+ }
+ // n = 1
+ if ops.NEqualsAny(1) {
+ return One
+ }
+ // n = 2
+ if ops.NEqualsAny(2) {
+ return Two
+ }
+ // n = 3
+ if ops.NEqualsAny(3) {
+ return Few
+ }
+ // n = 6
+ if ops.NEqualsAny(6) {
+ return Many
+ }
+ return Other
+ },
+ })
+
+ return rules
+}
diff --git a/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rules.go b/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rules.go
new file mode 100644
index 0000000..87eb836
--- /dev/null
+++ b/vendor/github.com/nicksnyder/go-i18n/v2/internal/plural/rules.go
@@ -0,0 +1,24 @@
+package plural
+
+import "golang.org/x/text/language"
+
+// Rules is a set of plural rules by language tag.
+type Rules map[language.Tag]*Rule
+
+// Rule returns the closest matching plural rule for the language tag
+// or nil if no rule could be found.
+func (r Rules) Rule(tag language.Tag) *Rule {
+ t := tag
+ for {
+ if rule := r[t]; rule != nil {
+ return rule
+ }
+ t = t.Parent()
+ if t.IsRoot() {
+ break
+ }
+ }
+ base, _ := tag.Base()
+ baseTag, _ := language.Parse(base.String())
+ return r[baseTag]
+}
diff --git a/vendor/github.com/nicksnyder/go-i18n/v2/internal/template.go b/vendor/github.com/nicksnyder/go-i18n/v2/internal/template.go
new file mode 100644
index 0000000..2fe9923
--- /dev/null
+++ b/vendor/github.com/nicksnyder/go-i18n/v2/internal/template.go
@@ -0,0 +1,51 @@
+package internal
+
+import (
+ "bytes"
+ "strings"
+ "sync"
+ gotemplate "text/template"
+)
+
+// Template stores the template for a string.
+type Template struct {
+ Src string
+ LeftDelim string
+ RightDelim string
+
+ parseOnce sync.Once
+ parsedTemplate *gotemplate.Template
+ parseError error
+}
+
+func (t *Template) Execute(funcs gotemplate.FuncMap, data interface{}) (string, error) {
+ leftDelim := t.LeftDelim
+ if leftDelim == "" {
+ leftDelim = "{{"
+ }
+ if !strings.Contains(t.Src, leftDelim) {
+ // Fast path to avoid parsing a template that has no actions.
+ return t.Src, nil
+ }
+
+ var gt *gotemplate.Template
+ var err error
+ if funcs == nil {
+ t.parseOnce.Do(func() {
+ // If funcs is nil, then we only need to parse this template once.
+ t.parsedTemplate, t.parseError = gotemplate.New("").Delims(t.LeftDelim, t.RightDelim).Parse(t.Src)
+ })
+ gt, err = t.parsedTemplate, t.parseError
+ } else {
+ gt, err = gotemplate.New("").Delims(t.LeftDelim, t.RightDelim).Funcs(funcs).Parse(t.Src)
+ }
+
+ if err != nil {
+ return "", err
+ }
+ var buf bytes.Buffer
+ if err := gt.Execute(&buf, data); err != nil {
+ return "", err
+ }
+ return buf.String(), nil
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 6d69314..3879300 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1,3 +1,6 @@
+# github.com/BurntSushi/toml v0.3.1
+## explicit
+github.com/BurntSushi/toml
# github.com/gin-contrib/sessions v0.0.4
## explicit
github.com/gin-contrib/sessions
@@ -68,6 +71,11 @@ github.com/mattn/go-sqlite3
github.com/modern-go/concurrent
# github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742
github.com/modern-go/reflect2
+# github.com/nicksnyder/go-i18n/v2 v2.1.2
+## explicit
+github.com/nicksnyder/go-i18n/v2/i18n
+github.com/nicksnyder/go-i18n/v2/internal
+github.com/nicksnyder/go-i18n/v2/internal/plural
# github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e
## explicit
# github.com/oklog/ulid/v2 v2.0.2
@@ -95,6 +103,7 @@ golang.org/x/sys/cpu
golang.org/x/sys/internal/unsafeheader
golang.org/x/sys/unix
# golang.org/x/text v0.3.7
+## explicit
golang.org/x/text/cases
golang.org/x/text/internal
golang.org/x/text/internal/language