diff options
91 files changed, 3640 insertions, 708 deletions
diff --git a/api/admin.go b/api/admin.go index bdacb3afb..b19772fdf 100644 --- a/api/admin.go +++ b/api/admin.go @@ -27,6 +27,7 @@ func InitAdmin(r *mux.Router) { sr.Handle("/client_props", ApiAppHandler(getClientConfig)).Methods("GET") sr.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST") sr.Handle("/analytics/{id:[A-Za-z0-9]+}/{name:[A-Za-z0-9_]+}", ApiAppHandler(getAnalytics)).Methods("GET") + sr.Handle("/analytics/{name:[A-Za-z0-9_]+}", ApiAppHandler(getAnalytics)).Methods("GET") } func getLogs(c *Context, w http.ResponseWriter, r *http.Request) { @@ -153,13 +154,15 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { name := params["name"] if name == "standard" { - var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 3) + var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 4) rows[0] = &model.AnalyticsRow{"channel_open_count", 0} rows[1] = &model.AnalyticsRow{"channel_private_count", 0} rows[2] = &model.AnalyticsRow{"post_count", 0} + rows[3] = &model.AnalyticsRow{"unique_user_count", 0} openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN) privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE) postChan := Srv.Store.Post().AnalyticsPostCount(teamId) + userChan := Srv.Store.User().AnalyticsUniqueUserCount(teamId) if r := <-openChan; r.Err != nil { c.Err = r.Err @@ -182,6 +185,13 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) { rows[2].Value = float64(r.Data.(int64)) } + if r := <-userChan; r.Err != nil { + c.Err = r.Err + return + } else { + rows[3].Value = float64(r.Data.(int64)) + } + w.Write([]byte(rows.ToJson())) } else if name == "post_counts_day" { if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil { diff --git a/api/admin_test.go b/api/admin_test.go index f7b6a7eeb..c2f4e9c76 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -151,7 +151,7 @@ func TestEmailTest(t *testing.T) { } } -func TestGetAnalyticsStandard(t *testing.T) { +func TestGetTeamAnalyticsStandard(t *testing.T) { Setup() team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} @@ -169,7 +169,7 @@ func TestGetAnalyticsStandard(t *testing.T) { post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) - if _, err := Client.GetAnalytics(team.Id, "standard"); err == nil { + if _, err := Client.GetTeamAnalytics(team.Id, "standard"); err == nil { t.Fatal("Shouldn't have permissions") } @@ -180,7 +180,7 @@ func TestGetAnalyticsStandard(t *testing.T) { Client.LoginByEmail(team.Name, user.Email, "pwd") - if result, err := Client.GetAnalytics(team.Id, "standard"); err != nil { + if result, err := Client.GetTeamAnalytics(team.Id, "standard"); err != nil { t.Fatal(err) } else { rows := result.Data.(model.AnalyticsRows) @@ -214,6 +214,62 @@ func TestGetAnalyticsStandard(t *testing.T) { t.Log(rows.ToJson()) t.Fatal() } + + if rows[3].Name != "unique_user_count" { + t.Log(rows.ToJson()) + t.Fatal() + } + + if rows[3].Value != 1 { + t.Log(rows.ToJson()) + t.Fatal() + } + } + + if result, err := Client.GetSystemAnalytics("standard"); err != nil { + t.Fatal(err) + } else { + rows := result.Data.(model.AnalyticsRows) + + if rows[0].Name != "channel_open_count" { + t.Log(rows.ToJson()) + t.Fatal() + } + + if rows[0].Value < 2 { + t.Log(rows.ToJson()) + t.Fatal() + } + + if rows[1].Name != "channel_private_count" { + t.Log(rows.ToJson()) + t.Fatal() + } + + if rows[1].Value == 0 { + t.Log(rows.ToJson()) + t.Fatal() + } + + if rows[2].Name != "post_count" { + t.Log(rows.ToJson()) + t.Fatal() + } + + if rows[2].Value == 0 { + t.Log(rows.ToJson()) + t.Fatal() + } + + if rows[3].Name != "unique_user_count" { + t.Log(rows.ToJson()) + t.Fatal() + } + + if rows[3].Value == 0 { + t.Log(rows.ToJson()) + t.Fatal() + } } } @@ -239,7 +295,7 @@ func TestGetPostCount(t *testing.T) { Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId", map[string]interface{}{"ChannelId": channel1.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())}) - if _, err := Client.GetAnalytics(team.Id, "post_counts_day"); err == nil { + if _, err := Client.GetTeamAnalytics(team.Id, "post_counts_day"); err == nil { t.Fatal("Shouldn't have permissions") } @@ -250,7 +306,7 @@ func TestGetPostCount(t *testing.T) { Client.LoginByEmail(team.Name, user.Email, "pwd") - if result, err := Client.GetAnalytics(team.Id, "post_counts_day"); err != nil { + if result, err := Client.GetTeamAnalytics(team.Id, "post_counts_day"); err != nil { t.Fatal(err) } else { rows := result.Data.(model.AnalyticsRows) @@ -284,7 +340,7 @@ func TestUserCountsWithPostsByDay(t *testing.T) { Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId", map[string]interface{}{"ChannelId": channel1.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())}) - if _, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err == nil { + if _, err := Client.GetTeamAnalytics(team.Id, "user_counts_with_posts_day"); err == nil { t.Fatal("Shouldn't have permissions") } @@ -295,7 +351,7 @@ func TestUserCountsWithPostsByDay(t *testing.T) { Client.LoginByEmail(team.Name, user.Email, "pwd") - if result, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err != nil { + if result, err := Client.GetTeamAnalytics(team.Id, "user_counts_with_posts_day"); err != nil { t.Fatal(err) } else { rows := result.Data.(model.AnalyticsRows) diff --git a/api/api.go b/api/api.go index f537bbfdc..d202172d0 100644 --- a/api/api.go +++ b/api/api.go @@ -19,17 +19,25 @@ var ServerTemplates *template.Template type ServerTemplatePage Page -func NewServerTemplatePage(templateName string) *ServerTemplatePage { +func NewServerTemplatePage(templateName, locale string) *ServerTemplatePage { return &ServerTemplatePage{ TemplateName: templateName, Props: make(map[string]string), + Extra: make(map[string]string), + Html: make(map[string]template.HTML), ClientCfg: utils.ClientCfg, - Locale: model.DEFAULT_LOCALE, + Locale: locale, } } func (me *ServerTemplatePage) Render() string { var text bytes.Buffer + + T := utils.GetUserTranslations(me.Locale) + me.Props["Footer"] = T("api.templates.email_footer") + me.Html["EmailInfo"] = template.HTML(T("api.templates.email_info", + map[string]interface{}{"FeedbackEmail": me.ClientCfg["FeedbackEmail"], "SiteName": me.ClientCfg["SiteName"]})) + if err := ServerTemplates.ExecuteTemplate(&text, me.TemplateName, me); err != nil { l4g.Error(utils.T("api.api.render.error"), me.TemplateName, err) } diff --git a/api/context.go b/api/context.go index f47ed1c30..41a52fa0c 100644 --- a/api/context.go +++ b/api/context.go @@ -5,6 +5,7 @@ package api import ( "fmt" + "html/template" "net" "net/http" "net/url" @@ -37,6 +38,8 @@ type Context struct { type Page struct { TemplateName string Props map[string]string + Extra map[string]string + Html map[string]template.HTML ClientCfg map[string]string ClientLicense map[string]string User *model.User @@ -507,6 +510,10 @@ func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) props["SiteURL"] = GetProtocol(r) + "://" + r.Host } + T, _ := utils.GetTranslationsAndLocale(w, r) + props["Title"] = T("api.templates.error.title", map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) + props["Link"] = T("api.templates.error.link") + w.WriteHeader(err.StatusCode) ServerTemplates.ExecuteTemplate(w, "error.html", Page{Props: props, ClientCfg: utils.ClientCfg}) } diff --git a/api/license.go b/api/license.go index af46bf113..5c602a68e 100644 --- a/api/license.go +++ b/api/license.go @@ -5,6 +5,7 @@ package api import ( "bytes" + "fmt" l4g "github.com/alecthomas/log4go" "github.com/gorilla/mux" "github.com/mattermost/platform/model" @@ -63,6 +64,18 @@ func addLicense(c *Context, w http.ResponseWriter, r *http.Request) { if success, licenseStr := utils.ValidateLicense(data); success { license = model.LicenseFromJson(strings.NewReader(licenseStr)) + if result := <-Srv.Store.User().AnalyticsUniqueUserCount(""); result.Err != nil { + c.Err = model.NewAppError("addLicense", "Unable to count total unique users.", fmt.Sprintf("err=%v", result.Err.Error())) + return + } else { + uniqueUserCount := result.Data.(int64) + + if uniqueUserCount > int64(*license.Features.Users) { + c.Err = model.NewAppError("addLicense", fmt.Sprintf("This license only supports %d users, when your system has %d unique users. Unique users are counted distinctly by email address. You can see total user count under Site Reports -> View Statistics.", *license.Features.Users, uniqueUserCount), "") + return + } + } + if ok := utils.SetLicense(license); !ok { c.LogAudit("failed - expired or non-started license") c.Err = model.NewLocAppError("addLicense", "api.license.add_license.expired.app_error", nil, "") diff --git a/api/post.go b/api/post.go index bb6bcb337..d3807653d 100644 --- a/api/post.go +++ b/api/post.go @@ -10,6 +10,7 @@ import ( "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" + "html/template" "net/http" "net/url" "path/filepath" @@ -593,28 +594,26 @@ func sendNotificationsAndForget(c *Context, post *model.Post, team *model.Team, channelName = channel.DisplayName } - subjectPage := NewServerTemplatePage("post_subject") - subjectPage.Props["SiteURL"] = c.GetSiteURL() - subjectPage.Props["TeamDisplayName"] = team.DisplayName - subjectPage.Props["SubjectText"] = subjectText - subjectPage.Props["Month"] = tm.Month().String()[:3] - subjectPage.Props["Day"] = fmt.Sprintf("%d", tm.Day()) - subjectPage.Props["Year"] = fmt.Sprintf("%d", tm.Year()) + month := userLocale(tm.Month().String()) + day := fmt.Sprintf("%d", tm.Day()) + year := fmt.Sprintf("%d", tm.Year()) + zone, _ := tm.Zone() - bodyPage := NewServerTemplatePage("post_body") + subjectPage := NewServerTemplatePage("post_subject", c.Locale) + subjectPage.Props["Subject"] = userLocale("api.templates.post_subject", + map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName, + "Month": month[:3], "Day": day, "Year": year}) + + bodyPage := NewServerTemplatePage("post_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() - bodyPage.Props["Nickname"] = profileMap[id].FirstName - bodyPage.Props["TeamDisplayName"] = team.DisplayName - bodyPage.Props["ChannelName"] = channelName - bodyPage.Props["BodyText"] = bodyText - bodyPage.Props["SenderName"] = senderName - bodyPage.Props["Hour"] = fmt.Sprintf("%02d", tm.Hour()) - bodyPage.Props["Minute"] = fmt.Sprintf("%02d", tm.Minute()) - bodyPage.Props["Month"] = tm.Month().String()[:3] - bodyPage.Props["Day"] = fmt.Sprintf("%d", tm.Day()) - bodyPage.Props["TimeZone"], _ = tm.Zone() bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message) bodyPage.Props["TeamLink"] = teamURL + "/channels/" + channel.Name + bodyPage.Props["BodyText"] = bodyText + bodyPage.Props["Button"] = userLocale("api.templates.post_body.button") + bodyPage.Html["Info"] = template.HTML(userLocale("api.templates.post_body.info", + map[string]interface{}{"ChannelName": channelName, "SenderName": senderName, + "Hour": fmt.Sprintf("%02d", tm.Hour()), "Minute": fmt.Sprintf("%02d", tm.Minute()), + "TimeZone": zone, "Month": month, "Day": day})) // attempt to fill in a message body if the post doesn't have any text if len(strings.TrimSpace(bodyPage.Props["PostMessage"])) == 0 && len(post.Filenames) > 0 { diff --git a/api/team.go b/api/team.go index 57a0e0bd2..779a6affe 100644 --- a/api/team.go +++ b/api/team.go @@ -11,6 +11,7 @@ import ( "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" + "html/template" "net/http" "net/url" "strconv" @@ -57,10 +58,16 @@ func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - subjectPage := NewServerTemplatePage("signup_team_subject") - subjectPage.Props["SiteURL"] = c.GetSiteURL() - bodyPage := NewServerTemplatePage("signup_team_body") + subjectPage := NewServerTemplatePage("signup_team_subject", c.Locale) + subjectPage.Props["Subject"] = c.T("api.templates.signup_team_subject", + map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) + + bodyPage := NewServerTemplatePage("signup_team_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() + bodyPage.Props["Title"] = c.T("api.templates.signup_team_body.title") + bodyPage.Props["Button"] = c.T("api.templates.signup_team_body.button") + bodyPage.Html["Info"] = template.HTML(c.T("api.templates.signup_team_body.button", + map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})) props := make(map[string]string) props["email"] = email @@ -427,10 +434,16 @@ func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) { return } - subjectPage := NewServerTemplatePage("find_teams_subject") - subjectPage.ClientCfg["SiteURL"] = c.GetSiteURL() - bodyPage := NewServerTemplatePage("find_teams_body") - bodyPage.ClientCfg["SiteURL"] = c.GetSiteURL() + siteURL := c.GetSiteURL() + subjectPage := NewServerTemplatePage("find_teams_subject", c.Locale) + subjectPage.Props["Subject"] = c.T("api.templates.find_teams_subject", + map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]}) + + bodyPage := NewServerTemplatePage("find_teams_body", c.Locale) + bodyPage.Props["SiteURL"] = siteURL + bodyPage.Props["Title"] = c.T("api.templates.find_teams_body.title") + bodyPage.Props["Found"] = c.T("api.templates.find_teams_body.found") + bodyPage.Props["NotFound"] = c.T("api.templates.find_teams_body.not_found") if result := <-Srv.Store.Team().GetTeamsForEmail(email); result.Err != nil { c.Err = result.Err @@ -442,7 +455,7 @@ func emailTeams(c *Context, w http.ResponseWriter, r *http.Request) { for _, team := range teams { props[team.Name] = c.GetTeamURLFromTeam(team) } - bodyPage.Props = props + bodyPage.Extra = props if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error(utils.T("api.team.email_teams.sending.error"), err) @@ -511,16 +524,19 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str senderRole = c.T("api.team.invite_members.member") } - subjectPage := NewServerTemplatePage("invite_subject") - subjectPage.Props["SenderName"] = sender - subjectPage.Props["TeamDisplayName"] = team.DisplayName + subjectPage := NewServerTemplatePage("invite_subject", c.Locale) + subjectPage.Props["Subject"] = c.T("api.templates.invite_subject", + map[string]interface{}{"SenderName": sender, "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]}) - bodyPage := NewServerTemplatePage("invite_body") + bodyPage := NewServerTemplatePage("invite_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() - bodyPage.Props["TeamURL"] = c.GetTeamURL() - bodyPage.Props["TeamDisplayName"] = team.DisplayName - bodyPage.Props["SenderName"] = sender - bodyPage.Props["SenderStatus"] = senderRole + bodyPage.Props["Title"] = c.T("api.templates.invite_body.title") + bodyPage.Html["Info"] = template.HTML(c.T("api.templates.invite_body.info", + map[string]interface{}{"SenderStatus": senderRole, "SenderName": sender, "TeamDisplayName": team.DisplayName})) + bodyPage.Props["Button"] = c.T("api.templates.invite_body.button") + bodyPage.Html["ExtraInfo"] = template.HTML(c.T("api.templates.invite_body.extra_info", + map[string]interface{}{"TeamDisplayName": team.DisplayName, "TeamURL": c.GetTeamURL()})) + props := make(map[string]string) props["email"] = invite props["id"] = team.Id diff --git a/api/templates/email_change_body.html b/api/templates/email_change_body.html index 7349aee6f..4f28584c4 100644 --- a/api/templates/email_change_body.html +++ b/api/templates/email_change_body.html @@ -17,8 +17,8 @@ <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> - <h2 style="font-weight: normal; margin-top: 10px;">You updated your email</h2> - <p>You email address for {{.Props.TeamDisplayName}} has been changed to {{.Props.NewEmail}}.<br>If you did not make this change, please contact the system administrator.</p> + <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2> + <p>{{.Props.Info}}</p> </td> </tr> <tr> diff --git a/api/templates/email_change_subject.html b/api/templates/email_change_subject.html index 4ff8026f1..afabc2191 100644 --- a/api/templates/email_change_subject.html +++ b/api/templates/email_change_subject.html @@ -1 +1 @@ -{{define "email_change_subject"}}[{{.ClientCfg.SiteName}}] Your email address has changed for {{.Props.TeamDisplayName}}{{end}} +{{define "email_change_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/email_change_verify_body.html b/api/templates/email_change_verify_body.html index 9d2c559b3..0d0c0aaba 100644 --- a/api/templates/email_change_verify_body.html +++ b/api/templates/email_change_verify_body.html @@ -17,10 +17,10 @@ <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> - <h2 style="font-weight: normal; margin-top: 10px;">You updated your email</h2> - <p>To finish updating your email address for {{.Props.TeamDisplayName}}, please click the link below to confirm this is the right address.</p> + <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2> + <p>{{.Props.Info}}</p> <p style="margin: 20px 0 15px"> - <a href="{{.Props.VerifyUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Verify Email</a> + <a href="{{.Props.VerifyUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">{{.Props.VerifyButton}}</a> </p> </td> </tr> diff --git a/api/templates/email_change_verify_subject.html b/api/templates/email_change_verify_subject.html index 744aaccfc..4fc4f4846 100644 --- a/api/templates/email_change_verify_subject.html +++ b/api/templates/email_change_verify_subject.html @@ -1 +1 @@ -{{define "email_change_verify_subject"}}[{{.ClientCfg.SiteName}}] Verify new email address for {{.Props.TeamDisplayName}}{{end}} +{{define "email_change_verify_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/email_footer.html b/api/templates/email_footer.html index e3ff9c584..6dc7fa483 100644 --- a/api/templates/email_footer.html +++ b/api/templates/email_footer.html @@ -6,7 +6,7 @@ </p> <p style="padding: 0 50px;"> (c) 2015 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301.<br> - To change your notification preferences, log in to your team site and go to Account Settings > Notifications. + {{.Props.Footer}} </p> </td> diff --git a/api/templates/email_info.html b/api/templates/email_info.html index 48725d144..0a34f18a0 100644 --- a/api/templates/email_info.html +++ b/api/templates/email_info.html @@ -1,9 +1,7 @@ {{define "email_info"}} <td style="color: #999; padding-top: 20px; line-height: 25px; font-size: 13px;"> - Any questions at all, mail us any time: <a href="mailto:{{.ClientCfg.FeedbackEmail}}" style="text-decoration: none; color:#2389D7;">{{.ClientCfg.FeedbackEmail}}</a>.<br> - Best wishes,<br> - The {{.ClientCfg.SiteName}} Team<br> + {{.Html.EmailInfo}} </td> {{end}} diff --git a/api/templates/error.html b/api/templates/error.html index 9fb2da1ba..2b6211be2 100644 --- a/api/templates/error.html +++ b/api/templates/error.html @@ -22,9 +22,9 @@ <div class="container-fluid"> <div class="error__container"> <div class="error__icon"><i class="fa fa-exclamation-triangle"></i></div> - <h2>{{ .ClientCfg.SiteName }} needs your help:</h2> + <h2>{{.Props.Title}}</h2> <p>{{ .Props.Message }}</p> - <a href="{{.Props.SiteURL}}">Go back to team site</a> + <a href="{{.Props.SiteURL}}">{{.Props.Link}}</a> </div> </div> </body> diff --git a/api/templates/find_teams_body.html b/api/templates/find_teams_body.html index 0b52af033..1324091aa 100644 --- a/api/templates/find_teams_body.html +++ b/api/templates/find_teams_body.html @@ -17,14 +17,14 @@ <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> - <h2 style="font-weight: normal; margin-top: 10px;">Finding teams</h2> - <p>{{ if .Props }} - Your request to find teams associated with your email found the following:<br> - {{range $index, $element := .Props}} + <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2> + <p>{{ if .Extra }} + {{.Props.Found}}<br> + {{range $index, $element := .Extra}} <a href="{{ $element }}" style="text-decoration: none; color:#2389D7;">{{ $index }}</a><br> {{ end }} {{ else }} - We could not find any teams for the given email. + {{.Props.NotFound}} {{ end }} </p> </td> diff --git a/api/templates/find_teams_subject.html b/api/templates/find_teams_subject.html index f3a1437b3..ebc339562 100644 --- a/api/templates/find_teams_subject.html +++ b/api/templates/find_teams_subject.html @@ -1 +1 @@ -{{define "find_teams_subject"}}Your {{ .ClientCfg.SiteName }} Teams{{end}} +{{define "find_teams_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/invite_body.html b/api/templates/invite_body.html index a81d0c3d5..2b6bde6d3 100644 --- a/api/templates/invite_body.html +++ b/api/templates/invite_body.html @@ -17,13 +17,13 @@ <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> - <h2 style="font-weight: normal; margin-top: 10px;">You've been invited</h2> - <p>The team {{.Props.SenderStatus}} <strong>{{.Props.SenderName}}</strong>, has invited you to join <strong>{{.Props.TeamDisplayName}}</strong>.</p> + <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2> + <p>{{.Html.Info}}</p> <p style="margin: 30px 0 15px"> - <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Join Team</a> + <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">{{.Props.Button}}</a> </p> <br/> - <p>Mattermost lets you share messages and files from your PC or phone, with instant search and archiving. After you’ve joined <strong>{{.Props.TeamDisplayName}}</strong>, you can sign-in to your new team and access these features anytime from the web address:<br/><br/><a href="{{.Props.TeamURL}}">{{.Props.TeamURL}}</a></p> + <p>{{.Html.ExtraInfo}}</p> </td> </tr> <tr> diff --git a/api/templates/invite_subject.html b/api/templates/invite_subject.html index 10f68969f..504915d50 100644 --- a/api/templates/invite_subject.html +++ b/api/templates/invite_subject.html @@ -1 +1 @@ -{{define "invite_subject"}}{{ .Props.SenderName }} invited you to join {{ .Props.TeamDisplayName }} Team on {{.ClientCfg.SiteName}}{{end}} +{{define "invite_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/password_change_body.html b/api/templates/password_change_body.html index 6199a3423..2c4ba10ca 100644 --- a/api/templates/password_change_body.html +++ b/api/templates/password_change_body.html @@ -17,8 +17,8 @@ <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> - <h2 style="font-weight: normal; margin-top: 10px;">You updated your password</h2> - <p>Your password has been updated for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }} by {{.Props.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator.</p> + <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2> + <p>{{.Html.Info}}</p> </td> </tr> <tr> diff --git a/api/templates/password_change_subject.html b/api/templates/password_change_subject.html index 0cbf052c1..897f1210d 100644 --- a/api/templates/password_change_subject.html +++ b/api/templates/password_change_subject.html @@ -1 +1 @@ -{{define "password_change_subject"}}Your password has been updated for {{.Props.TeamDisplayName}} on {{ .ClientCfg.SiteName }}{{end}} +{{define "password_change_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/post_body.html b/api/templates/post_body.html index 468d5e205..54f34d1dd 100644 --- a/api/templates/post_body.html +++ b/api/templates/post_body.html @@ -17,10 +17,10 @@ <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> - <h2 style="font-weight: normal; margin-top: 10px;">You were mentioned</h2> - <p>CHANNEL: {{.Props.ChannelName}}<br>{{.Props.SenderName}} - {{.Props.Hour}}:{{.Props.Minute}} {{.Props.TimeZone}}, {{.Props.Month}} {{.Props.Day}}<br><pre style="text-align:left;font-family: 'Lato', sans-serif; white-space: pre-wrap; white-space: -moz-pre-wrap; white-space: -pre-wrap; white-space: -o-pre-wrap; word-wrap: break-word;">{{.Props.PostMessage}}</pre></p> + <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.BodyText}}</h2> + <p>{{.Html.Info}}<br><pre style="text-align:left;font-family: 'Lato', sans-serif; white-space: pre-wrap; white-space: -moz-pre-wrap; white-space: -pre-wrap; white-space: -o-pre-wrap; word-wrap: break-word;">{{.Props.PostMessage}}</pre></p> <p style="margin: 20px 0 15px"> - <a href="{{.Props.TeamLink}}" style="background: #2389D7; display: inline-block; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 170px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Go To Channel</a> + <a href="{{.Props.TeamLink}}" style="background: #2389D7; display: inline-block; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 170px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">{{.Props.Button}}</a> </p> </td> </tr> diff --git a/api/templates/post_subject.html b/api/templates/post_subject.html index f53353d85..60daaa432 100644 --- a/api/templates/post_subject.html +++ b/api/templates/post_subject.html @@ -1 +1 @@ -{{define "post_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.TeamDisplayName}} Team Notifications for {{.Props.Month}} {{.Props.Day}}, {{.Props.Year}}{{end}} +{{define "post_subject"}}[{{.ClientCfg.SiteName}}] {{.Props.Subject}}{{end}} diff --git a/api/templates/reset_body.html b/api/templates/reset_body.html index a608c804a..69cd44957 100644 --- a/api/templates/reset_body.html +++ b/api/templates/reset_body.html @@ -17,10 +17,10 @@ <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> - <h2 style="font-weight: normal; margin-top: 10px;">You requested a password reset</h2> - <p>To change your password, click "Reset Password" below.<br>If you did not mean to reset your password, please ignore this email and your password will remain the same.</p> + <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2> + <p>{{.Html.Info}}</p> <p style="margin: 20px 0 15px"> - <a href="{{.Props.ResetUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Reset Password</a> + <a href="{{.Props.ResetUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">{{.Props.Button}}</a> </p> </td> </tr> diff --git a/api/templates/reset_subject.html b/api/templates/reset_subject.html index 87ad7bc38..a2852d332 100644 --- a/api/templates/reset_subject.html +++ b/api/templates/reset_subject.html @@ -1 +1 @@ -{{define "reset_subject"}}Reset your password{{end}} +{{define "reset_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/signin_change_body.html b/api/templates/signin_change_body.html index 5b96df944..af8577f0f 100644 --- a/api/templates/signin_change_body.html +++ b/api/templates/signin_change_body.html @@ -17,8 +17,8 @@ <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> - <h2 style="font-weight: normal; margin-top: 10px;">You updated your sign-in method</h2> - <p>You updated your sign-in method for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }} to {{.Props.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator.</p> + <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2> + <p>{{.Html.Info}}</p> </td> </tr> <tr> diff --git a/api/templates/signin_change_subject.html b/api/templates/signin_change_subject.html index b1d644a28..606dc4df3 100644 --- a/api/templates/signin_change_subject.html +++ b/api/templates/signin_change_subject.html @@ -1 +1 @@ -{{define "signin_change_subject"}}You updated your sign-in method for {{.Props.TeamDisplayName}} on {{ .ClientCfg.SiteName }}{{end}} +{{define "signin_change_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/signup_team_body.html b/api/templates/signup_team_body.html index 2f384ac43..683a9891e 100644 --- a/api/templates/signup_team_body.html +++ b/api/templates/signup_team_body.html @@ -17,11 +17,11 @@ <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> - <h2 style="font-weight: normal; margin-top: 10px;">Thanks for creating a team!</h2> + <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2> <p style="margin: 20px 0 25px"> - <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Set up your team</a> + <a href="{{.Props.Link}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">{{.Props.Button}}</a> </p> - {{ .ClientCfg.SiteName }} is one place for all your team communication, searchable and available anywhere.<br>You'll get more out of {{ .ClientCfg.SiteName }} when your team is in constant communication--let's get them on board.<br></p> + {{.Html.Info}}<br></p> </td> </tr> <tr> diff --git a/api/templates/signup_team_subject.html b/api/templates/signup_team_subject.html index 4fc5b3d72..413a5c8c1 100644 --- a/api/templates/signup_team_subject.html +++ b/api/templates/signup_team_subject.html @@ -1 +1 @@ -{{define "signup_team_subject"}}{{ .ClientCfg.SiteName }} Team Setup{{end}}
\ No newline at end of file +{{define "signup_team_subject"}}{{.Props.Subject}}{{end}}
\ No newline at end of file diff --git a/api/templates/verify_body.html b/api/templates/verify_body.html index c42b2a372..2b0d25f94 100644 --- a/api/templates/verify_body.html +++ b/api/templates/verify_body.html @@ -17,10 +17,10 @@ <table border="0" cellpadding="0" cellspacing="0" style="padding: 20px 50px 0; text-align: center; margin: 0 auto"> <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> - <h2 style="font-weight: normal; margin-top: 10px;">You've joined the {{ .Props.TeamDisplayName }} team</h2> - <p>Please verify your email address by clicking below.</p> + <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2> + <p>{{.Props.Info}}</p> <p style="margin: 20px 0 15px"> - <a href="{{.Props.VerifyUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Verify Email</a> + <a href="{{.Props.VerifyUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">{{.Props.Button}}</a> </p> </td> </tr> diff --git a/api/templates/verify_subject.html b/api/templates/verify_subject.html index 9a3a11282..ad7fc2aaa 100644 --- a/api/templates/verify_subject.html +++ b/api/templates/verify_subject.html @@ -1 +1 @@ -{{define "verify_subject"}}[{{ .Props.TeamDisplayName }} {{ .ClientCfg.SiteName }}] Email Verification{{end}} +{{define "verify_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/templates/welcome_body.html b/api/templates/welcome_body.html index 71d838b08..b5ca9beb3 100644 --- a/api/templates/welcome_body.html +++ b/api/templates/welcome_body.html @@ -18,19 +18,19 @@ {{if .Props.VerifyUrl }} <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> - <h2 style="font-weight: normal; margin-top: 10px;">You've joined the {{ .Props.TeamDisplayName }} team</h2> - <p>Please verify your email address by clicking below.</p> + <h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2> + <p>{{.Props.Info}}</p> <p style="margin: 20px 0 15px"> - <a href="{{.Props.VerifyUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">Verify Email</a> + <a href="{{.Props.VerifyUrl}}" style="background: #2389D7; border-radius: 3px; color: #fff; border: none; outline: none; min-width: 200px; padding: 15px 25px; font-size: 14px; font-family: inherit; cursor: pointer; -webkit-appearance: none;text-decoration: none;">{{.Props.Button}}</a> </p> </td> </tr> {{end}} <tr> <td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;"> - <h2 style="font-weight: normal; margin-top: 25px; line-height: 1.5;">You can sign-in to your new team from the web address:</h2> + <h2 style="font-weight: normal; margin-top: 25px; line-height: 1.5;">{{.Props.Info2}}</h2> <a href="{{.Props.TeamURL}}">{{.Props.TeamURL}}</a> - <p>Mattermost lets you share messages and files from your PC or phone, with instant search and archiving.</p> + <p>{{.Props.Info3}}</p> </td> </tr> </table> diff --git a/api/templates/welcome_subject.html b/api/templates/welcome_subject.html index c3b70ef20..95189b900 100644 --- a/api/templates/welcome_subject.html +++ b/api/templates/welcome_subject.html @@ -1 +1 @@ -{{define "welcome_subject"}}You joined {{ .Props.TeamDisplayName }}{{end}} +{{define "welcome_subject"}}{{.Props.Subject}}{{end}} diff --git a/api/user.go b/api/user.go index 9d3fe0b5e..33fe6f6dd 100644 --- a/api/user.go +++ b/api/user.go @@ -17,6 +17,7 @@ import ( "github.com/mattermost/platform/utils" "github.com/mssola/user_agent" "hash/fnv" + "html/template" "image" "image/color" "image/draw" @@ -134,7 +135,7 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { } if sendWelcomeEmail { - sendWelcomeEmailAndForget(ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), ruser.EmailVerified) + sendWelcomeEmailAndForget(c, ruser.Id, ruser.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team), ruser.EmailVerified) } w.Write([]byte(ruser.ToJson())) @@ -308,13 +309,19 @@ func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service return ruser } -func sendWelcomeEmailAndForget(userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) { +func sendWelcomeEmailAndForget(c *Context, userId, email, teamName, teamDisplayName, siteURL, teamURL string, verified bool) { go func() { - subjectPage := NewServerTemplatePage("welcome_subject") - subjectPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage := NewServerTemplatePage("welcome_body") + subjectPage := NewServerTemplatePage("welcome_subject", c.Locale) + subjectPage.Props["Subject"] = c.T("api.templates.welcome_subject", map[string]interface{}{"TeamDisplayName": teamDisplayName}) + + bodyPage := NewServerTemplatePage("welcome_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL + bodyPage.Props["Title"] = c.T("api.templates.welcome_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName}) + bodyPage.Props["Info"] = c.T("api.templates.welcome_body.info") + bodyPage.Props["Button"] = c.T("api.templates.welcome_body.button") + bodyPage.Props["Info2"] = c.T("api.templates.welcome_body.info2") + bodyPage.Props["Info3"] = c.T("api.templates.welcome_body.info3") bodyPage.Props["TeamURL"] = teamURL if !verified { @@ -367,18 +374,21 @@ func addDirectChannelsAndForget(user *model.User) { }() } -func SendVerifyEmailAndForget(userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { +func SendVerifyEmailAndForget(c *Context, userId, userEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, userEmail) - subjectPage := NewServerTemplatePage("verify_subject") - subjectPage.Props["SiteURL"] = siteURL - subjectPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage := NewServerTemplatePage("verify_body") + subjectPage := NewServerTemplatePage("verify_subject", c.Locale) + subjectPage.Props["Subject"] = c.T("api.templates.verify_subject", + map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) + + bodyPage := NewServerTemplatePage("verify_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["TeamDisplayName"] = teamDisplayName + bodyPage.Props["Title"] = c.T("api.templates.verify_body.title", map[string]interface{}{"TeamDisplayName": teamDisplayName}) + bodyPage.Props["Info"] = c.T("api.templates.verify_body.info") bodyPage.Props["VerifyUrl"] = link + bodyPage.Props["Button"] = c.T("api.templates.verify_body.button") if err := utils.SendMail(userEmail, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_verify_email_and_forget.failed.error"), err) @@ -1141,10 +1151,10 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) { l4g.Error(tresult.Err.Message) } else { team := tresult.Data.(*model.Team) - sendEmailChangeEmailAndForget(rusers[1].Email, rusers[0].Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL()) + sendEmailChangeEmailAndForget(c, rusers[1].Email, rusers[0].Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL()) if utils.Cfg.EmailSettings.RequireEmailVerification { - SendEmailChangeVerifyEmailAndForget(rusers[0].Id, rusers[0].Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + SendEmailChangeVerifyEmailAndForget(c, rusers[0].Id, rusers[0].Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) } } } @@ -1224,7 +1234,7 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) { l4g.Error(tresult.Err.Message) } else { team := tresult.Data.(*model.Team) - sendPasswordChangeEmailAndForget(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), c.T("api.user.update_password.menu")) + sendPasswordChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), c.T("api.user.update_password.menu")) } data := make(map[string]string) @@ -1530,11 +1540,15 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) { link := fmt.Sprintf("%s/reset_password?d=%s&h=%s", c.GetTeamURLFromTeam(team), url.QueryEscape(data), url.QueryEscape(hash)) - subjectPage := NewServerTemplatePage("reset_subject") - subjectPage.Props["SiteURL"] = c.GetSiteURL() - bodyPage := NewServerTemplatePage("reset_body") + subjectPage := NewServerTemplatePage("reset_subject", c.Locale) + subjectPage.Props["Subject"] = c.T("api.templates.reset_subject") + + bodyPage := NewServerTemplatePage("reset_body", c.Locale) bodyPage.Props["SiteURL"] = c.GetSiteURL() + bodyPage.Props["Title"] = c.T("api.templates.reset_body.title") + bodyPage.Html["Info"] = template.HTML(c.T("api.templates.reset_body.info")) bodyPage.Props["ResetUrl"] = link + bodyPage.Props["Button"] = c.T("api.templates.reset_body.button") if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { c.Err = model.NewLocAppError("sendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message) @@ -1636,23 +1650,24 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAuditWithUserId(userId, "success") } - sendPasswordChangeEmailAndForget(user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), "using a reset password link") + sendPasswordChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetTeamURLFromTeam(team), c.GetSiteURL(), c.T("api.user.reset_password.method")) props["new_password"] = "" w.Write([]byte(model.MapToJson(props))) } -func sendPasswordChangeEmailAndForget(email, teamDisplayName, teamURL, siteURL, method string) { +func sendPasswordChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) { go func() { - subjectPage := NewServerTemplatePage("password_change_subject") - subjectPage.Props["SiteURL"] = siteURL - subjectPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage := NewServerTemplatePage("password_change_body") + subjectPage := NewServerTemplatePage("password_change_subject", c.Locale) + subjectPage.Props["Subject"] = c.T("api.templates.password_change_subject", + map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) + + bodyPage := NewServerTemplatePage("password_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage.Props["TeamURL"] = teamURL - bodyPage.Props["Method"] = method + bodyPage.Props["Title"] = c.T("api.templates.password_change_body.title") + bodyPage.Html["Info"] = template.HTML(c.T("api.templates.password_change_body.info", + map[string]interface{}{"TeamDisplayName": teamDisplayName, "TeamURL": teamURL, "Method": method})) if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_password_change_email_and_forget.error"), err) @@ -1661,17 +1676,18 @@ func sendPasswordChangeEmailAndForget(email, teamDisplayName, teamURL, siteURL, }() } -func sendEmailChangeEmailAndForget(oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) { +func sendEmailChangeEmailAndForget(c *Context, oldEmail, newEmail, teamDisplayName, teamURL, siteURL string) { go func() { - subjectPage := NewServerTemplatePage("email_change_subject") - subjectPage.Props["SiteURL"] = siteURL - subjectPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage := NewServerTemplatePage("email_change_body") + subjectPage := NewServerTemplatePage("email_change_subject", c.Locale) + subjectPage.Props["Subject"] = c.T("api.templates.email_change_body", + map[string]interface{}{"TeamDisplayName": teamDisplayName}) + + bodyPage := NewServerTemplatePage("email_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage.Props["TeamURL"] = teamURL - bodyPage.Props["NewEmail"] = newEmail + bodyPage.Props["Title"] = c.T("api.templates.email_change_body.title") + bodyPage.Props["Info"] = c.T("api.templates.email_change_body.info", + map[string]interface{}{"TeamDisplayName": teamDisplayName, "NewEmail": newEmail}) if err := utils.SendMail(oldEmail, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_email_change_email_and_forget.error"), err) @@ -1680,18 +1696,22 @@ func sendEmailChangeEmailAndForget(oldEmail, newEmail, teamDisplayName, teamURL, }() } -func SendEmailChangeVerifyEmailAndForget(userId, newUserEmail, teamName, teamDisplayName, siteURL, teamURL string) { +func SendEmailChangeVerifyEmailAndForget(c *Context, userId, newUserEmail, teamName, teamDisplayName, siteURL, teamURL string) { go func() { link := fmt.Sprintf("%s/verify_email?uid=%s&hid=%s&teamname=%s&email=%s", siteURL, userId, model.HashPassword(userId), teamName, newUserEmail) - subjectPage := NewServerTemplatePage("email_change_verify_subject") - subjectPage.Props["SiteURL"] = siteURL - subjectPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage := NewServerTemplatePage("email_change_verify_body") + subjectPage := NewServerTemplatePage("email_change_verify_subject", c.Locale) + subjectPage.Props["Subject"] = c.T("api.templates.email_change_verify_subject", + map[string]interface{}{"TeamDisplayName": teamDisplayName}) + + bodyPage := NewServerTemplatePage("email_change_verify_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["TeamDisplayName"] = teamDisplayName + bodyPage.Props["Title"] = c.T("api.templates.email_change_verify_body.title") + bodyPage.Props["Info"] = c.T("api.templates.email_change_verify_body.info", + map[string]interface{}{"TeamDisplayName": teamDisplayName}) bodyPage.Props["VerifyUrl"] = link + bodyPage.Props["VerifyButton"] = c.T("api.templates.email_change_verify_body.button") if err := utils.SendMail(newUserEmail, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_email_change_verify_email_and_forget.error"), err) @@ -2032,7 +2052,7 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request, return } - sendSignInChangeEmailAndForget(user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), strings.Title(service)+" SSO") + sendSignInChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), strings.Title(service)+" SSO") } func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) { @@ -2089,7 +2109,7 @@ func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) { return } - sendSignInChangeEmailAndForget(user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), "email and password") + sendSignInChangeEmailAndForget(c, user.Email, team.DisplayName, c.GetSiteURL()+"/"+team.Name, c.GetSiteURL(), c.T("api.templates.signin_change_email.body.method_email")) RevokeAllSession(c, c.Session.UserId) if c.Err != nil { @@ -2103,17 +2123,18 @@ func switchToEmail(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(m))) } -func sendSignInChangeEmailAndForget(email, teamDisplayName, teamURL, siteURL, method string) { +func sendSignInChangeEmailAndForget(c *Context, email, teamDisplayName, teamURL, siteURL, method string) { go func() { - subjectPage := NewServerTemplatePage("signin_change_subject") - subjectPage.Props["SiteURL"] = siteURL - subjectPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage := NewServerTemplatePage("signin_change_body") + subjectPage := NewServerTemplatePage("signin_change_subject", c.Locale) + subjectPage.Props["Subject"] = c.T("api.templates.singin_change_email.subject", + map[string]interface{}{"TeamDisplayName": teamDisplayName, "SiteName": utils.ClientCfg["SiteName"]}) + + bodyPage := NewServerTemplatePage("signin_change_body", c.Locale) bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["TeamDisplayName"] = teamDisplayName - bodyPage.Props["TeamURL"] = teamURL - bodyPage.Props["Method"] = method + bodyPage.Props["Title"] = c.T("api.templates.signin_change_email.body.title") + bodyPage.Html["Info"] = template.HTML(c.T("api.templates.singin_change_email.body.info", + map[string]interface{}{"TeamDisplayName": teamDisplayName, "TeamURL": teamURL, "Method": method})) if err := utils.SendMail(email, subjectPage.Render(), bodyPage.Render()); err != nil { l4g.Error(utils.T("api.user.send_sign_in_change_email_and_forget.error"), err) diff --git a/api/web_conn.go b/api/web_conn.go index 2b0e29038..515a8ab31 100644 --- a/api/web_conn.go +++ b/api/web_conn.go @@ -8,6 +8,7 @@ import ( "github.com/gorilla/websocket" "github.com/mattermost/platform/model" "github.com/mattermost/platform/store" + "github.com/mattermost/platform/utils" "time" ) @@ -33,11 +34,11 @@ func NewWebConn(ws *websocket.Conn, teamId string, userId string, sessionId stri pchan := Srv.Store.User().UpdateLastPingAt(userId, model.GetMillis()) if result := <-achan; result.Err != nil { - l4g.Error("Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v", userId, sessionId, result.Err) + l4g.Error(utils.T("api.web_conn.new_web_conn.last_activity.error"), userId, sessionId, result.Err) } if result := <-pchan; result.Err != nil { - l4g.Error("Failed to updated LastPingAt for user_id=%v, err=%v", userId, result.Err) + l4g.Error(utils.T("api.web_conn.new_web_conn.last_ping.error"), userId, result.Err) } }() @@ -56,7 +57,7 @@ func (c *WebConn) readPump() { go func() { if result := <-Srv.Store.User().UpdateLastPingAt(c.UserId, model.GetMillis()); result.Err != nil { - l4g.Error("Failed to updated LastPingAt for user_id=%v, err=%v", c.UserId, result.Err) + l4g.Error(utils.T("api.web_conn.new_web_conn.last_ping.error"), c.UserId, result.Err) } }() diff --git a/api/web_hub.go b/api/web_hub.go index 4361d1035..5fe9d6ae8 100644 --- a/api/web_hub.go +++ b/api/web_hub.go @@ -6,6 +6,7 @@ package api import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" ) type Hub struct { @@ -86,7 +87,7 @@ func (h *Hub) Start() { nh.broadcast <- msg } case s := <-h.stop: - l4g.Debug("stopping %v connections", s) + l4g.Debug(utils.T("api.web_hub.start.stopping.debug"), s) for _, v := range h.teamHubs { v.Stop() } diff --git a/api/web_socket.go b/api/web_socket.go index 995e2a677..7590e6646 100644 --- a/api/web_socket.go +++ b/api/web_socket.go @@ -8,11 +8,12 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/websocket" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" "net/http" ) func InitWebSocket(r *mux.Router) { - l4g.Debug("Initializing web socket api routes") + l4g.Debug(utils.T("api.web_socket.init.debug")) r.Handle("/websocket", ApiUserRequired(connect)).Methods("GET") hub.Start() } @@ -28,8 +29,8 @@ func connect(c *Context, w http.ResponseWriter, r *http.Request) { ws, err := upgrader.Upgrade(w, r, nil) if err != nil { - l4g.Error("websocket connect err: %v", err) - c.Err = model.NewAppError("connect", "Failed to upgrade websocket connection", "") + l4g.Error(utils.T("api.web_socket.connect.error"), err) + c.Err = model.NewLocAppError("connect", "api.web_socket.connect.upgrade.app_error", nil, "") return } diff --git a/api/web_team_hub.go b/api/web_team_hub.go index bb9ed9526..55300c828 100644 --- a/api/web_team_hub.go +++ b/api/web_team_hub.go @@ -6,6 +6,7 @@ package api import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" ) type TeamHub struct { @@ -65,7 +66,7 @@ func (h *TeamHub) Start() { case s := <-h.stop: if s { - l4g.Debug("team hub stopping for teamId=%v", h.teamId) + l4g.Debug(utils.T("api.web_team_hun.start.debug"), h.teamId) for webCon := range h.connections { webCon.WebSocket.Close() diff --git a/api/webhook.go b/api/webhook.go index 33e7f957a..399f62fdb 100644 --- a/api/webhook.go +++ b/api/webhook.go @@ -12,7 +12,7 @@ import ( ) func InitWebhook(r *mux.Router) { - l4g.Debug("Initializing webhook api routes") + l4g.Debug(utils.T("api.webhook.init.debug")) sr := r.PathPrefix("/hooks").Subrouter() sr.Handle("/incoming/create", ApiUserRequired(createIncomingHook)).Methods("POST") @@ -27,7 +27,7 @@ func InitWebhook(r *mux.Router) { func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { - c.Err = model.NewAppError("createIncomingHook", "Incoming webhooks have been disabled by the system admin.", "") + c.Err = model.NewLocAppError("createIncomingHook", "api.webhook.create_incoming.disabled.app_errror", nil, "") c.Err.StatusCode = http.StatusNotImplemented return } @@ -82,7 +82,7 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { - c.Err = model.NewAppError("deleteIncomingHook", "Incoming webhooks have been disabled by the system admin.", "") + c.Err = model.NewLocAppError("deleteIncomingHook", "api.webhook.delete_incoming.disabled.app_errror", nil, "") c.Err.StatusCode = http.StatusNotImplemented return } @@ -111,7 +111,7 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { } else { if c.Session.UserId != result.Data.(*model.IncomingWebhook).UserId && !c.IsTeamAdmin() { c.LogAudit("fail - inappropriate permissions") - c.Err = model.NewAppError("deleteIncomingHook", "Inappropriate permissions to delete incoming webhook", "user_id="+c.Session.UserId) + c.Err = model.NewLocAppError("deleteIncomingHook", "api.webhook.delete_incoming.permissions.app_errror", nil, "user_id="+c.Session.UserId) return } } @@ -127,7 +127,7 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) { func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { - c.Err = model.NewAppError("getIncomingHooks", "Incoming webhooks have been disabled by the system admin.", "") + c.Err = model.NewLocAppError("getIncomingHooks", "api.webhook.get_incoming.disabled.app_error", nil, "") c.Err.StatusCode = http.StatusNotImplemented return } @@ -151,7 +151,7 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) { func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { - c.Err = model.NewAppError("createOutgoingHook", "Outgoing webhooks have been disabled by the system admin.", "") + c.Err = model.NewLocAppError("createOutgoingHook", "api.webhook.create_outgoing.disabled.app_error", nil, "") c.Err.StatusCode = http.StatusNotImplemented return } @@ -199,7 +199,7 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { } } } else if len(hook.TriggerWords) == 0 { - c.Err = model.NewAppError("createOutgoingHook", "Either trigger_words or channel_id must be set", "") + c.Err = model.NewLocAppError("createOutgoingHook", "api.webhook.create_outgoing.triggers.app_error", nil, "") return } @@ -215,7 +215,7 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks { - c.Err = model.NewAppError("getOutgoingHooks", "Outgoing webhooks have been disabled by the system admin.", "") + c.Err = model.NewLocAppError("getOutgoingHooks", "api.webhook.get_outgoing.disabled.app_error", nil, "") c.Err.StatusCode = http.StatusNotImplemented return } @@ -239,7 +239,7 @@ func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) { func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { - c.Err = model.NewAppError("deleteOutgoingHook", "Outgoing webhooks have been disabled by the system admin.", "") + c.Err = model.NewLocAppError("deleteOutgoingHook", "api.webhook.delete_outgoing.disabled.app_error", nil, "") c.Err.StatusCode = http.StatusNotImplemented return } @@ -268,7 +268,7 @@ func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { } else { if c.Session.UserId != result.Data.(*model.OutgoingWebhook).CreatorId && !c.IsTeamAdmin() { c.LogAudit("fail - inappropriate permissions") - c.Err = model.NewAppError("deleteOutgoingHook", "Inappropriate permissions to delete outcoming webhook", "user_id="+c.Session.UserId) + c.Err = model.NewLocAppError("deleteOutgoingHook", "api.webhook.delete_outgoing.permissions.app_error", nil, "user_id="+c.Session.UserId) return } } @@ -284,7 +284,7 @@ func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) { func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) { if !utils.Cfg.ServiceSettings.EnableIncomingWebhooks { - c.Err = model.NewAppError("regenOutgoingHookToken", "Outgoing webhooks have been disabled by the system admin.", "") + c.Err = model.NewLocAppError("regenOutgoingHookToken", "api.webhook.regen_outgoing_token.disabled.app_error", nil, "") c.Err.StatusCode = http.StatusNotImplemented return } @@ -316,7 +316,7 @@ func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request) if c.Session.TeamId != hook.TeamId && c.Session.UserId != hook.CreatorId && !c.IsTeamAdmin() { c.LogAudit("fail - inappropriate permissions") - c.Err = model.NewAppError("regenOutgoingHookToken", "Inappropriate permissions to regenerate outcoming webhook token", "user_id="+c.Session.UserId) + c.Err = model.NewLocAppError("regenOutgoingHookToken", "api.webhook.regen_outgoing_token.permissions.app_error", nil, "user_id="+c.Session.UserId) return } } diff --git a/i18n/en.json b/i18n/en.json index 4edc176d6..6a7a858e7 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1,5 +1,53 @@ [ { + "id": "April", + "translation": "April" + }, + { + "id": "August", + "translation": "August" + }, + { + "id": "December", + "translation": "December" + }, + { + "id": "February", + "translation": "February" + }, + { + "id": "January", + "translation": "January" + }, + { + "id": "July", + "translation": "July" + }, + { + "id": "June", + "translation": "June" + }, + { + "id": "March", + "translation": "March" + }, + { + "id": "May", + "translation": "May" + }, + { + "id": "November", + "translation": "November" + }, + { + "id": "October", + "translation": "October" + }, + { + "id": "September", + "translation": "September" + }, + { "id": "api.admin.file_read_error", "translation": "Error reading log file" }, @@ -749,7 +797,7 @@ }, { "id": "api.post.update_post.permissions_details.app_error", - "translation": "Already delted id={{.PostId}}" + "translation": "Already deleted id={{.PostId}}" }, { "id": "api.post_get_post_by_id.get.app_error", @@ -992,6 +1040,198 @@ "translation": "You do not have the appropriate permissions" }, { + "id": "api.templates.email_change_body.info", + "translation": "You email address for {{.TeamDisplayName}} has been changed to {{.NewEmail}}.<br>If you did not make this change, please contact the system administrator." + }, + { + "id": "api.templates.email_change_body.title", + "translation": "You updated your email" + }, + { + "id": "api.templates.email_change_subject", + "translation": "Your email address has changed for {{.TeamDisplayName}}" + }, + { + "id": "api.templates.email_change_verify_body.button", + "translation": "Verify Email" + }, + { + "id": "api.templates.email_change_verify_body.info", + "translation": "To finish updating your email address for {{.TeamDisplayName}}, please click the link below to confirm this is the right address." + }, + { + "id": "api.templates.email_change_verify_body.title", + "translation": "You updated your email" + }, + { + "id": "api.templates.email_change_verify_subject", + "translation": "Verify new email address for {{.TeamDisplayName}}" + }, + { + "id": "api.templates.email_footer", + "translation": "To change your notification preferences, log in to your team site and go to Account Settings > Notifications." + }, + { + "id": "api.templates.email_info", + "translation": "Any questions at all, mail us any time: <a href='mailto:{{.FeedbackEmail}}' style='text-decoration: none; color:#2389D7;'>{{.FeedbackEmail}}</a>.<br>Best wishes,<br>The {{.SiteName}} Team<br>" + }, + { + "id": "api.templates.error.link", + "translation": "Go back to team site" + }, + { + "id": "api.templates.error.title", + "translation": "{{ .SiteName }} needs your help:" + }, + { + "id": "api.templates.find_teams_body.found", + "translation": "Your request to find teams associated with your email found the following:" + }, + { + "id": "api.templates.find_teams_body.not_found", + "translation": "We could not find any teams for the given email." + }, + { + "id": "api.templates.find_teams_body.title", + "translation": "Finding teams" + }, + { + "id": "api.templates.find_teams_subject", + "translation": "Your {{ .SiteName }} Teams" + }, + { + "id": "api.templates.invite_body.button", + "translation": "Join Team" + }, + { + "id": "api.templates.invite_body.extra_info", + "translation": "Mattermost lets you share messages and files from your PC or phone, with instant search and archiving. After you’ve joined <strong>{{.TeamDisplayName}}</strong>, you can sign-in to your new team and access these features anytime from the web address:<br/><br/><a href='{{.TeamURL}}'>{{.TeamURL}}</a>" + }, + { + "id": "api.templates.invite_body.info", + "translation": "The team {{.SenderStatus}} <strong>{{.SenderName}}</strong>, has invited you to join <strong>{{.TeamDisplayName}}</strong>." + }, + { + "id": "api.templates.invite_body.title", + "translation": "You've been invited" + }, + { + "id": "api.templates.invite_subject", + "translation": "{{ .SenderName }} invited you to join {{ .TeamDisplayName }} Team on {{.SiteName}}" + }, + { + "id": "api.templates.password_change_body.info", + "translation": "Your password has been updated for {{.TeamDisplayName}} on {{ .TeamURL }} by {{.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator." + }, + { + "id": "api.templates.password_change_body.title", + "translation": "You updated your password" + }, + { + "id": "api.templates.password_change_subject", + "translation": "Your password has been updated for {{.TeamDisplayName}} on {{ .SiteName }}" + }, + { + "id": "api.templates.post_body.button", + "translation": "Go To Channel" + }, + { + "id": "api.templates.post_body.info", + "translation": "CHANNEL: {{.ChannelName}}<br>{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + }, + { + "id": "api.templates.post_subject", + "translation": "{{.SubjectText}} on {{.TeamDisplayName}} at {{.Month}} {{.Day}}, {{.Year}}" + }, + { + "id": "api.templates.reset_body.button", + "translation": "Reset Password" + }, + { + "id": "api.templates.reset_body.info", + "translation": "To change your password, click \"Reset Password\" below.<br>If you did not mean to reset your password, please ignore this email and your password will remain the same." + }, + { + "id": "api.templates.reset_body.title", + "translation": "You requested a password reset" + }, + { + "id": "api.templates.reset_subject", + "translation": "Reset your password" + }, + { + "id": "api.templates.signin_change_email.body.method_email", + "translation": "email and password" + }, + { + "id": "api.templates.signin_change_email.body.title", + "translation": "You updated your sign-in method" + }, + { + "id": "api.templates.signup_team_body.button", + "translation": "Set up your team" + }, + { + "id": "api.templates.signup_team_body.info", + "translation": "{{ .SiteName }} is one place for all your team communication, searchable and available anywhere.<br>You'll get more out of {{ .SiteName }} when your team is in constant communication--let's get them on board." + }, + { + "id": "api.templates.signup_team_body.title", + "translation": "Thanks for creating a team!" + }, + { + "id": "api.templates.signup_team_subject", + "translation": "{{ .SiteName }} Team Setup" + }, + { + "id": "api.templates.singin_change_email.body.info", + "translation": "You updated your sign-in method for {{.TeamDisplayName}} on {{ .TeamURL }} to {{.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator." + }, + { + "id": "api.templates.singin_change_email.subject", + "translation": "You updated your sign-in method for {{.TeamDisplayName}} on {{ .SiteName }}" + }, + { + "id": "api.templates.verify_body.button", + "translation": "Verify Email" + }, + { + "id": "api.templates.verify_body.info", + "translation": "Please verify your email address by clicking below." + }, + { + "id": "api.templates.verify_body.title", + "translation": "You've joined the {{ .TeamDisplayName }} team" + }, + { + "id": "api.templates.verify_subject", + "translation": "[{{ .TeamDisplayName }} {{ .SiteName }}] Email Verification" + }, + { + "id": "api.templates.welcome_body.button", + "translation": "Verify Email" + }, + { + "id": "api.templates.welcome_body.info", + "translation": "Please verify your email address by clicking below." + }, + { + "id": "api.templates.welcome_body.info2", + "translation": "You can sign-in to your new team from the web address:" + }, + { + "id": "api.templates.welcome_body.info3", + "translation": "Mattermost lets you share messages and files from your PC or phone, with instant search and archiving." + }, + { + "id": "api.templates.welcome_body.title", + "translation": "You've joined the {{ .TeamDisplayName }} team" + }, + { + "id": "api.templates.welcome_subject", + "translation": "You joined {{ .TeamDisplayName }}" + }, + { "id": "api.user.add_direct_channels_and_forget.failed.error", "translation": "Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v" }, @@ -1192,6 +1432,10 @@ "translation": "The reset link has expired" }, { + "id": "api.user.reset_password.method", + "translation": "using a reset password link" + }, + { "id": "api.user.reset_password.sso.app_error", "translation": "Cannot reset password for SSO accounts" }, @@ -1320,11 +1564,695 @@ "translation": "Unable to upload profile image. File is too large." }, { + "id": "api.web_conn.new_web_conn.last_activity.error", + "translation": "Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v" + }, + { + "id": "api.web_conn.new_web_conn.last_ping.error", + "translation": "Failed to update LastPingAt for user_id=%v, err=%v" + }, + { + "id": "api.web_hub.start.stopping.debug", + "translation": "stopping %v connections" + }, + { + "id": "api.web_socket.connect.error", + "translation": "websocket connect err: %v" + }, + { + "id": "api.web_socket.connect.upgrade.app_error", + "translation": "Failed to upgrade websocket connection" + }, + { + "id": "api.web_socket.init.debug", + "translation": "Initializing web socket api routes" + }, + { + "id": "api.web_team_hun.start.debug", + "translation": "team hub stopping for teamId=%v" + }, + { + "id": "api.webhook.create_incoming.disabled.app_errror", + "translation": "Incoming webhooks have been disabled by the system admin." + }, + { + "id": "api.webhook.create_outgoing.disabled.app_error", + "translation": "Outgoing webhooks have been disabled by the system admin." + }, + { + "id": "api.webhook.create_outgoing.triggers.app_error", + "translation": "Either trigger_words or channel_id must be set" + }, + { + "id": "api.webhook.delete_incoming.disabled.app_errror", + "translation": "Incoming webhooks have been disabled by the system admin." + }, + { + "id": "api.webhook.delete_incoming.permissions.app_errror", + "translation": "Inappropriate permissions to delete incoming webhook" + }, + { + "id": "api.webhook.delete_outgoing.disabled.app_error", + "translation": "Outgoing webhooks have been disabled by the system admin." + }, + { + "id": "api.webhook.delete_outgoing.permissions.app_error", + "translation": "Inappropriate permissions to delete outcoming webhook" + }, + { + "id": "api.webhook.get_incoming.disabled.app_error", + "translation": "Incoming webhooks have been disabled by the system admin." + }, + { + "id": "api.webhook.get_outgoing.disabled.app_error", + "translation": "Outgoing webhooks have been disabled by the system admin." + }, + { + "id": "api.webhook.init.debug", + "translation": "Initializing webhook api routes" + }, + { + "id": "api.webhook.regen_outgoing_token.disabled.app_error", + "translation": "Outgoing webhooks have been disabled by the system admin." + }, + { + "id": "api.webhook.regen_outgoing_token.permissions.app_error", + "translation": "Inappropriate permissions to regenerate outcoming webhook token" + }, + { + "id": "manaultesting.get_channel_id.no_found.debug", + "translation": "Could not find channel: %v, %v possibilites searched" + }, + { + "id": "manaultesting.get_channel_id.unable.debug", + "translation": "Unable to get channels" + }, + { + "id": "manaultesting.manual_test.create.info", + "translation": "Creating user and team" + }, + { + "id": "manaultesting.manual_test.parse.app_error", + "translation": "Unable to parse URL" + }, + { + "id": "manaultesting.manual_test.setup.info", + "translation": "Setting up for manual test..." + }, + { + "id": "manaultesting.manual_test.uid.debug", + "translation": "No uid in url" + }, + { + "id": "manaultesting.test_autolink.info", + "translation": "Manual Auto Link Test" + }, + { + "id": "manaultesting.test_autolink.unable.app_error", + "translation": "Unable to get channels" + }, + { "id": "mattermost.current_version", "translation": "Current version is %v (%v/%v/%v)" }, { + "id": "model.access.is_valid.access_token.app_error", + "translation": "Invalid access token" + }, + { + "id": "model.access.is_valid.auth_code.app_error", + "translation": "Invalid auth code" + }, + { + "id": "model.access.is_valid.redirect_uri.app_error", + "translation": "Invalid redirect uri" + }, + { + "id": "model.access.is_valid.refresh_token.app_error", + "translation": "Invalid refresh token" + }, + { + "id": "model.authorize.is_valid.auth_code.app_error", + "translation": "Invalid authorization code" + }, + { + "id": "model.authorize.is_valid.client_id.app_error", + "translation": "Invalid client id" + }, + { + "id": "model.authorize.is_valid.create_at.app_error", + "translation": "Create at must be a valid time" + }, + { + "id": "model.authorize.is_valid.expires.app_error", + "translation": "Expires in must be set" + }, + { + "id": "model.authorize.is_valid.redirect_uri.app_error", + "translation": "Invalid redirect uri" + }, + { + "id": "model.authorize.is_valid.scope.app_error", + "translation": "Invalid scope" + }, + { + "id": "model.authorize.is_valid.state.app_error", + "translation": "Invalid state" + }, + { + "id": "model.authorize.is_valid.user_id.app_error", + "translation": "Invalid user id" + }, + { + "id": "model.channel.is_valid.2_or_more.app_error", + "translation": "Name must be 2 or more lowercase alphanumeric characters" + }, + { + "id": "model.channel.is_valid.create_at.app_error", + "translation": "Create at must be a valid time" + }, + { + "id": "model.channel.is_valid.creator_id.app_error", + "translation": "Invalid creator id" + }, + { + "id": "model.channel.is_valid.display_name.app_error", + "translation": "Invalid display name" + }, + { + "id": "model.channel.is_valid.header.app_error", + "translation": "Invalid header" + }, + { + "id": "model.channel.is_valid.id.app_error", + "translation": "Invalid Id" + }, + { + "id": "model.channel.is_valid.name.app_error", + "translation": "Invalid name" + }, + { + "id": "model.channel.is_valid.purpose.app_error", + "translation": "Invalid purpose" + }, + { + "id": "model.channel.is_valid.type.app_error", + "translation": "Invalid type" + }, + { + "id": "model.channel.is_valid.update_at.app_error", + "translation": "Update at must be a valid time" + }, + { + "id": "model.channel_member.is_valid.channel_id.app_error", + "translation": "Invalid channel id" + }, + { + "id": "model.channel_member.is_valid.notify_level.app_error", + "translation": "Invalid notify level" + }, + { + "id": "model.channel_member.is_valid.role.app_error", + "translation": "Invalid role" + }, + { + "id": "model.channel_member.is_valid.unread_level.app_error", + "translation": "Invalid mark unread level" + }, + { + "id": "model.channel_member.is_valid.user_id.app_error", + "translation": "Invalid user id" + }, + { + "id": "model.client.connecting.app_error", + "translation": "We encountered an error while connecting to the server" + }, + { + "id": "model.client.login.app_error", + "translation": "Authentication tokens didn't match" + }, + { + "id": "model.config.is_valid.email_reset_salt.app_error", + "translation": "Invalid password reset salt for email settings. Must be 32 chars or more." + }, + { + "id": "model.config.is_valid.email_salt.app_error", + "translation": "Invalid invite salt for email settings. Must be 32 chars or more." + }, + { + "id": "model.config.is_valid.email_security.app_error", + "translation": "Invalid connection security for email settings. Must be '', 'TLS', or 'STARTTLS'" + }, + { + "id": "model.config.is_valid.encrypt_sql.app_error", + "translation": "Invalid at rest encrypt key for SQL settings. Must be 32 chars or more." + }, + { + "id": "model.config.is_valid.file_driver.app_error", + "translation": "Invalid driver name for file settings. Must be 'local' or 'amazons3'" + }, + { + "id": "model.config.is_valid.file_preview_height.app_error", + "translation": "Invalid preview height for file settings. Must be a zero or positive number." + }, + { + "id": "model.config.is_valid.file_preview_width.app_error", + "translation": "Invalid preview width for file settings. Must be a positive number." + }, + { + "id": "model.config.is_valid.file_profile_height.app_error", + "translation": "Invalid profile height for file settings. Must be a positive number." + }, + { + "id": "model.config.is_valid.file_profile_width.app_error", + "translation": "Invalid profile width for file settings. Must be a positive number." + }, + { + "id": "model.config.is_valid.file_salt.app_error", + "translation": "Invalid public link salt for file settings. Must be 32 chars or more." + }, + { + "id": "model.config.is_valid.file_thumb_height.app_error", + "translation": "Invalid thumbnail height for file settings. Must be a positive number." + }, + { + "id": "model.config.is_valid.file_thumb_width.app_error", + "translation": "Invalid thumbnail width for file settings. Must be a positive number." + }, + { + "id": "model.config.is_valid.listen_address.app_error", + "translation": "Invalid listen address for service settings Must be set." + }, + { + "id": "model.config.is_valid.login_attempts.app_error", + "translation": "Invalid maximum login attempts for service settings. Must be a positive number." + }, + { + "id": "model.config.is_valid.max_users.app_error", + "translation": "Invalid maximum users per team for team settings. Must be a positive number." + }, + { + "id": "model.config.is_valid.rate_mem.app_error", + "translation": "Invalid memory store size for rate limit settings. Must be a positive number" + }, + { + "id": "model.config.is_valid.rate_sec.app_error", + "translation": "Invalid per sec for rate limit settings. Must be a positive number" + }, + { + "id": "model.config.is_valid.sql_data_src.app_error", + "translation": "Invalid data source for SQL settings. Must be set." + }, + { + "id": "model.config.is_valid.sql_driver.app_error", + "translation": "Invalid driver name for SQL settings. Must be 'mysql' or 'postgres'" + }, + { + "id": "model.config.is_valid.sql_idle.app_error", + "translation": "Invalid maximum idle connection for SQL settings. Must be a positive number." + }, + { + "id": "model.config.is_valid.sql_max_conn.app_error", + "translation": "Invalid maximum open connection for SQL settings. Must be a positive number." + }, + { + "id": "model.file_info.get.gif.app_error", + "translation": "Could not decode gif." + }, + { + "id": "model.incoming_hook.channel_id.app_error", + "translation": "Invalid channel id" + }, + { + "id": "model.incoming_hook.create_at.app_error", + "translation": "Create at must be a valid time" + }, + { + "id": "model.incoming_hook.id.app_error", + "translation": "Invalid Id" + }, + { + "id": "model.incoming_hook.team_id.app_error", + "translation": "Invalid team id" + }, + { + "id": "model.incoming_hook.update_at.app_error", + "translation": "Update at must be a valid time" + }, + { + "id": "model.incoming_hook.user_id.app_error", + "translation": "Invalid user id" + }, + { + "id": "model.oauth.is_valid.app_id.app_error", + "translation": "Invalid app id" + }, + { + "id": "model.oauth.is_valid.callback.app_error", + "translation": "Invalid callback urls" + }, + { + "id": "model.oauth.is_valid.client_secret.app_error", + "translation": "Invalid client secret" + }, + { + "id": "model.oauth.is_valid.create_at.app_error", + "translation": "Create at must be a valid time" + }, + { + "id": "model.oauth.is_valid.creator_id.app_error", + "translation": "Invalid creator id" + }, + { + "id": "model.oauth.is_valid.description.app_error", + "translation": "Invalid description" + }, + { + "id": "model.oauth.is_valid.homepage.app_error", + "translation": "Invalid homepage" + }, + { + "id": "model.oauth.is_valid.name.app_error", + "translation": "Invalid name" + }, + { + "id": "model.oauth.is_valid.update_at.app_error", + "translation": "Update at must be a valid time" + }, + { + "id": "model.outgoing_hook.is_valid.callback.app_error", + "translation": "Invalid callback urls" + }, + { + "id": "model.outgoing_hook.is_valid.channel_id.app_error", + "translation": "Invalid channel id" + }, + { + "id": "model.outgoing_hook.is_valid.create_at.app_error", + "translation": "Create at must be a valid time" + }, + { + "id": "model.outgoing_hook.is_valid.id.app_error", + "translation": "Invalid Id" + }, + { + "id": "model.outgoing_hook.is_valid.team_id.app_error", + "translation": "Invalid team id" + }, + { + "id": "model.outgoing_hook.is_valid.token.app_error", + "translation": "Invalid token" + }, + { + "id": "model.outgoing_hook.is_valid.update_at.app_error", + "translation": "Update at must be a valid time" + }, + { + "id": "model.outgoing_hook.is_valid.url.app_error", + "translation": "Invalid callback URLs. Each must be a valid URL and start with http:// or https://" + }, + { + "id": "model.outgoing_hook.is_valid.user_id.app_error", + "translation": "Invalid user id" + }, + { + "id": "model.outgoing_hook.is_valid.words.app_error", + "translation": "Invalid trigger words" + }, + { + "id": "model.post.is_valid.channel_id.app_error", + "translation": "Invalid channel id" + }, + { + "id": "model.post.is_valid.create_at.app_error", + "translation": "Create at must be a valid time" + }, + { + "id": "model.post.is_valid.filenames.app_error", + "translation": "Invalid filenames" + }, + { + "id": "model.post.is_valid.hashtags.app_error", + "translation": "Invalid hashtags" + }, + { + "id": "model.post.is_valid.id.app_error", + "translation": "Invalid Id" + }, + { + "id": "model.post.is_valid.msg.app_error", + "translation": "Invalid message" + }, + { + "id": "model.post.is_valid.original_id.app_error", + "translation": "Invalid original id" + }, + { + "id": "model.post.is_valid.parent_id.app_error", + "translation": "Invalid parent id" + }, + { + "id": "model.post.is_valid.props.app_error", + "translation": "Invalid props" + }, + { + "id": "model.post.is_valid.root_id.app_error", + "translation": "Invalid root id" + }, + { + "id": "model.post.is_valid.root_parent.app_error", + "translation": "Invalid root id must be set if parent id set" + }, + { + "id": "model.post.is_valid.type.app_error", + "translation": "Invalid type" + }, + { + "id": "model.post.is_valid.update_at.app_error", + "translation": "Update at must be a valid time" + }, + { + "id": "model.post.is_valid.user_id.app_error", + "translation": "Invalid user id" + }, + { + "id": "model.preference.is_valid.category.app_error", + "translation": "Invalid category" + }, + { + "id": "model.preference.is_valid.id.app_error", + "translation": "Invalid user id" + }, + { + "id": "model.preference.is_valid.name.app_error", + "translation": "Invalid name" + }, + { + "id": "model.preference.is_valid.value.app_error", + "translation": "Value is too long" + }, + { + "id": "model.team.is_valid.characters.app_error", + "translation": "Name must be 4 or more lowercase alphanumeric characters" + }, + { + "id": "model.team.is_valid.company.app_error", + "translation": "Invalid company name" + }, + { + "id": "model.team.is_valid.create_at.app_error", + "translation": "Create at must be a valid time" + }, + { + "id": "model.team.is_valid.domains.app_error", + "translation": "Invalid allowed domains" + }, + { + "id": "model.team.is_valid.email.app_error", + "translation": "Invalid email" + }, + { + "id": "model.team.is_valid.id.app_error", + "translation": "Invalid Id" + }, + { + "id": "model.team.is_valid.name.app_error", + "translation": "Invalid name" + }, + { + "id": "model.team.is_valid.reserved.app_error", + "translation": "This URL is unavailable. Please try another." + }, + { + "id": "model.team.is_valid.type.app_error", + "translation": "Invalid type" + }, + { + "id": "model.team.is_valid.update_at.app_error", + "translation": "Update at must be a valid time" + }, + { + "id": "model.team.is_valid.url.app_error", + "translation": "Invalid URL Identifier" + }, + { + "id": "model.user.is_valid.auth_data.app_error", + "translation": "Invalid auth data" + }, + { + "id": "model.user.is_valid.auth_data_pwd.app_error", + "translation": "Invalid user, password and auth data cannot both be set" + }, + { + "id": "model.user.is_valid.auth_data_type.app_error", + "translation": "Invalid user, auth data must be set with auth type" + }, + { + "id": "model.user.is_valid.create_at.app_error", + "translation": "Create at must be a valid time" + }, + { + "id": "model.user.is_valid.email.app_error", + "translation": "Invalid email" + }, + { + "id": "model.user.is_valid.first_name.app_error", + "translation": "Invalid first name" + }, + { + "id": "model.user.is_valid.id.app_error", + "translation": "Invalid user id" + }, + { + "id": "model.user.is_valid.last_name.app_error", + "translation": "Invalid last name" + }, + { + "id": "model.user.is_valid.nickname.app_error", + "translation": "Invalid nickname" + }, + { + "id": "model.user.is_valid.pwd.app_error", + "translation": "Invalid password" + }, + { + "id": "model.user.is_valid.team_id.app_error", + "translation": "Invalid team id" + }, + { + "id": "model.user.is_valid.theme.app_error", + "translation": "Invalid theme" + }, + { + "id": "model.user.is_valid.update_at.app_error", + "translation": "Update at must be a valid time" + }, + { + "id": "model.user.is_valid.username.app_error", + "translation": "Invalid username" + }, + { + "id": "model.utils.decode_json.app_error", + "translation": "could not decode" + }, + { + "id": "utils.config.load_config.decoding.panic", + "translation": "Error decoding config file={{.Filename}}, err={{.Error}}" + }, + { + "id": "utils.config.load_config.getting.panic", + "translation": "Error getting config info file={{.Filename}}, err={{.Error}}" + }, + { + "id": "utils.config.load_config.opening.panic", + "translation": "Error opening config file={{.Filename}}, err={{.Error}}" + }, + { + "id": "utils.config.load_config.validating.panic", + "translation": "Error validating config file={{.Filename}}, err={{.Error}}" + }, + { + "id": "utils.config.save_config.saving.app_error", + "translation": "An error occurred while saving the file to {{.Filename}}" + }, + { "id": "utils.i18n.loaded", "translation": "Loaded system translations for '%v' from '%v'" + }, + { + "id": "utils.iru.with_evict", + "translation": "Must provide a positive size" + }, + { + "id": "utils.license.load_license.invalid.warn", + "translation": "No valid enterprise license found" + }, + { + "id": "utils.license.load_license.open_find.warn", + "translation": "Unable to open/find license file" + }, + { + "id": "utils.license.remove_license.unable.error", + "translation": "Unable to remove license file, err=%v" + }, + { + "id": "utils.license.validate_license.decode.error", + "translation": "Encountered error decoding license, err=%v" + }, + { + "id": "utils.license.validate_license.invalid.error", + "translation": "Invalid signature, err=%v" + }, + { + "id": "utils.license.validate_license.not_long.error", + "translation": "Signed license not long enough" + }, + { + "id": "utils.license.validate_license.signing.error", + "translation": "Encountered error signing license, err=%v" + }, + { + "id": "utils.mail.connect_smtp.open.app_error", + "translation": "Failed to open connection" + }, + { + "id": "utils.mail.connect_smtp.open_tls.app_error", + "translation": "Failed to open TLS connection" + }, + { + "id": "utils.mail.new_client.auth.app_error", + "translation": "Failed to authenticate on SMTP server" + }, + { + "id": "utils.mail.new_client.open.error", + "translation": "Failed to open a connection to SMTP server %v" + }, + { + "id": "utils.mail.send_mail.close.app_error", + "translation": "Failed to close connection to SMTP server" + }, + { + "id": "utils.mail.send_mail.from_address.app_error", + "translation": "Failed to add from email address" + }, + { + "id": "utils.mail.send_mail.msg.app_error", + "translation": "Failed to write email message" + }, + { + "id": "utils.mail.send_mail.msg_data.app_error", + "translation": "Failed to add email messsage data" + }, + { + "id": "utils.mail.send_mail.sending.debug", + "translation": "sending mail to %v with subject of '%v'" + }, + { + "id": "utils.mail.send_mail.to_address.app_error", + "translation": "Failed to add to email address" + }, + { + "id": "utils.mail.test.configured.error", + "translation": "SMTP server settings do not appear to be configured properly err=%v details=%v" } ]
\ No newline at end of file diff --git a/i18n/es.json b/i18n/es.json index 480f23857..57dd22bc8 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -1,5 +1,53 @@ [ { + "id": "April", + "translation": "Abril" + }, + { + "id": "August", + "translation": "Agosto" + }, + { + "id": "December", + "translation": "Diciembre" + }, + { + "id": "February", + "translation": "Febrero" + }, + { + "id": "January", + "translation": "Enero" + }, + { + "id": "July", + "translation": "Julio" + }, + { + "id": "June", + "translation": "Junio" + }, + { + "id": "March", + "translation": "Marzo" + }, + { + "id": "May", + "translation": "Mayo" + }, + { + "id": "November", + "translation": "Noviembre" + }, + { + "id": "October", + "translation": "Octubre" + }, + { + "id": "September", + "translation": "Septiembre" + }, + { "id": "api.admin.file_read_error", "translation": "Error leyendo el archivo de registro" }, @@ -749,7 +797,7 @@ }, { "id": "api.post.update_post.permissions_details.app_error", - "translation": "Already delted id={{.PostId}}" + "translation": "Ya fué elminado el id={{.PostId}}" }, { "id": "api.post_get_post_by_id.get.app_error", @@ -992,6 +1040,198 @@ "translation": "No tienes los permisos apropiados" }, { + "id": "api.templates.email_change_body.info", + "translation": "Tu dirección de correo electrónico para {{.TeamDisplayName}} ha sido cambiada por {{.NewEmail}}.<br>Si este cambio no fue realizado por ti, por favor contacta un administrador de sistema." + }, + { + "id": "api.templates.email_change_body.title", + "translation": "Haz actualizado tu correo electrónico" + }, + { + "id": "api.templates.email_change_subject", + "translation": "Tu dirección de correo para {{.TeamDisplayName}} ha cambiado" + }, + { + "id": "api.templates.email_change_verify_body.button", + "translation": "Confirmar Correo" + }, + { + "id": "api.templates.email_change_verify_body.info", + "translation": "Para terminar de actualizar tu dirección de correo para {{.TeamDisplayName}}, por favor pincha en botón de abajo para confirmar que está es la dirección correcta." + }, + { + "id": "api.templates.email_change_verify_body.title", + "translation": "Haz actualizado tu correo electrónico" + }, + { + "id": "api.templates.email_change_verify_subject", + "translation": "Verificación de la nueva dirección de correo electrónico para {{.TeamDisplayName}}" + }, + { + "id": "api.templates.email_footer", + "translation": "Para cambiar tus preferencias de notificaciones, inicia sesión en tu equipo y dirigete a Configurar Cuenta > Notificaciones." + }, + { + "id": "api.templates.email_info", + "translation": "Si tienes alguna pregunta, escribenos en cualquier momento a: <a href=\"mailto:{{.FeedbackEmail}}\" style=\"text-decoration: none; color:#2389D7;\">{{.FeedbackEmail}}</a>.<br>Los mejores deseos,<br>El Equipo {{.SiteName}}<br>" + }, + { + "id": "api.templates.error.link", + "translation": "Volver al sitio del equipo" + }, + { + "id": "api.templates.error.title", + "translation": "Necesitamos de tu ayuda en {{ .SiteName }}:" + }, + { + "id": "api.templates.find_teams_body.found", + "translation": "Haz solicitado encontrar los equipos a los que tienes asociado tu correo electrónico y encontramos los siguientes:" + }, + { + "id": "api.templates.find_teams_body.not_found", + "translation": "No hemos podido encontrar ningún equipo asociado al correo electrónico suministrado." + }, + { + "id": "api.templates.find_teams_body.title", + "translation": "Tus Equipos" + }, + { + "id": "api.templates.find_teams_subject", + "translation": "Tus equipos de {{ .SiteName }}" + }, + { + "id": "api.templates.invite_body.button", + "translation": "Unirme al Equipo" + }, + { + "id": "api.templates.invite_body.extra_info", + "translation": "Mattermost te permite compartir mensajes y archivos de tu Computador o teléfono, incluyendo busquedas instantáneas. Una vez que te hayas unido a <strong>{{.TeamDisplayName}}</strong>, Podrás iniciar sesión en tu nuevo equipo y utilizar todas las caracterÃsticas en cualquier momento desde la dirección web:<br/><br/><a href=\"{{.TeamURL}}\">{{.TeamURL}}</a>" + }, + { + "id": "api.templates.invite_body.info", + "translation": "<strong>{{.SenderName}}</strong> {{.SenderStatus}} del equipo <strong>{{.TeamDisplayName}}</strong>, te ha invitado a unirte." + }, + { + "id": "api.templates.invite_body.title", + "translation": "Haz sido invitado" + }, + { + "id": "api.templates.invite_subject", + "translation": "{{ .SenderName }} te ha invitado a unirte al equipo {{ .TeamDisplayName }} en {{.SiteName}}" + }, + { + "id": "api.templates.password_change_body.info", + "translation": "Tu contraseña ha sido cambiada para {{.TeamDisplayName}} en {{ .TeamURL }} por el método de {{.Method}}.<br>Si este cambio no fue realizado por ti, por favor contacta a un administrador del sistema." + }, + { + "id": "api.templates.password_change_body.title", + "translation": "Haz cambiado tu contraseña" + }, + { + "id": "api.templates.password_change_subject", + "translation": "Tu contraseña ha sido cambiada para {{.TeamDisplayName}} en {{ .SiteName }}" + }, + { + "id": "api.templates.post_body.button", + "translation": "Ir al Canal" + }, + { + "id": "api.templates.post_body.info", + "translation": "Canal: {{.ChannelName}}<br>{{.SenderName}} - {{.Day}} {{.Month}}, {{.Hour}}:{{.Minute}} {{.TimeZone}}" + }, + { + "id": "api.templates.post_subject", + "translation": "{{.SubjectText}} en {{.TeamDisplayName}} el {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "api.templates.reset_body.button", + "translation": "Restablecer Contraseña" + }, + { + "id": "api.templates.reset_body.info", + "translation": "Para cambiar tu contraseña, pincha el botón \"Restablecer Contraseña\" que se encuentra abajo.<br>Si no fue tu intención restablecer tu contraseña, por favor ignora este correo y tu contraseña seguirá siendo la misma." + }, + { + "id": "api.templates.reset_body.title", + "translation": "Haz solicitado restablecer tu contraseña" + }, + { + "id": "api.templates.reset_subject", + "translation": "Restablece tu contraseña" + }, + { + "id": "api.templates.signin_change_email.body.method_email", + "translation": "correo electrónico y contraseña" + }, + { + "id": "api.templates.signin_change_email.body.title", + "translation": "Haz actualizado el método con el que inicias sesión" + }, + { + "id": "api.templates.signup_team_body.button", + "translation": "Configura tu equipo" + }, + { + "id": "api.templates.signup_team_body.info", + "translation": "{{ .SiteName }} es el lugar para todas las comunicaciones de tu equipo, con capacidades de búsqueda y disponible desde cualquier parte.<br>Podrás aprovechar al máximo {{ .SiteName }} cuando tu equipo esté en constante comunicación--Traigamoslos a bordo." + }, + { + "id": "api.templates.signup_team_body.title", + "translation": "¡Gracias por haber creado un equipo!" + }, + { + "id": "api.templates.signup_team_subject", + "translation": "Configuración del equipo en {{ .SiteName }}" + }, + { + "id": "api.templates.singin_change_email.body.info", + "translation": "Haz actualizado el método con el que inicias sesión en {{.TeamURL}} para el equipo {{.TeamDisplayName}} por {{.Method}}.<br>Si este cambio no fue realizado por ti, por favor contacta a un administrador del sistema." + }, + { + "id": "api.templates.singin_change_email.subject", + "translation": "Cambio del método de inicio de sesión para {{.TeamDisplayName}} en {{ .SiteName }}" + }, + { + "id": "api.templates.verify_body.button", + "translation": "Confirmar Correo" + }, + { + "id": "api.templates.verify_body.info", + "translation": "Por favor verifica tu correo electrónico al pinchar el botón de abajo." + }, + { + "id": "api.templates.verify_body.title", + "translation": "Te haz unido al equipo {{ .TeamDisplayName }}" + }, + { + "id": "api.templates.verify_subject", + "translation": "[{{ .TeamDisplayName }} {{ .SiteName }}] Correo de Verificación" + }, + { + "id": "api.templates.welcome_body.button", + "translation": "Confirmar Correo" + }, + { + "id": "api.templates.welcome_body.info", + "translation": "Por favor verifica tu correo electrónico al pinchar el botón de abajo." + }, + { + "id": "api.templates.welcome_body.info2", + "translation": "Puedes iniciar sesión en tu nuevo equipo desde la dirección web:" + }, + { + "id": "api.templates.welcome_body.info3", + "translation": "Mattermost te permite compartir mensajes y archivos desde un computador o teléfono desde donde te encuentres." + }, + { + "id": "api.templates.welcome_body.title", + "translation": "Te haz unido al equipo {{ .TeamDisplayName }}" + }, + { + "id": "api.templates.welcome_subject", + "translation": "Te haz unido a {{ .TeamDisplayName }}" + }, + { "id": "api.user.add_direct_channels_and_forget.failed.error", "translation": "Falla al agragar las preferencias del canal directo para el usuario user_id=%s, team_id=%s, err=%v" }, @@ -1192,6 +1432,10 @@ "translation": "El enlace para restablecer la contraseña ha expirado" }, { + "id": "api.user.reset_password.method", + "translation": "utilizando el enlace para restablecer contraseña" + }, + { "id": "api.user.reset_password.sso.app_error", "translation": "No se puede restablecer la contraseña para cuentas SSO" }, @@ -1320,11 +1564,695 @@ "translation": "No se pudo actualizar la imagen del perfil. El archivo es muy grande." }, { + "id": "api.web_conn.new_web_conn.last_activity.error", + "translation": "Falla al actualizar LastActivityAt para user_id=%v and session_id=%v, err=%v" + }, + { + "id": "api.web_conn.new_web_conn.last_ping.error", + "translation": "Falla al actualizar LastPingAt para el user_id=%v, err=%v" + }, + { + "id": "api.web_hub.start.stopping.debug", + "translation": "deteniendo todas las conexiones" + }, + { + "id": "api.web_socket.connect.error", + "translation": "conexión al websocket err: %v" + }, + { + "id": "api.web_socket.connect.upgrade.app_error", + "translation": "Falla al actualizar la conexión del websocket" + }, + { + "id": "api.web_socket.init.debug", + "translation": "Inicializando rutas del API para los web socket" + }, + { + "id": "api.web_team_hun.start.debug", + "translation": "deteniendo el hub de equipo para teamId=%v" + }, + { + "id": "api.webhook.create_incoming.disabled.app_errror", + "translation": "Webhooks entrantes han sido deshabilitados por el administrador del sistema." + }, + { + "id": "api.webhook.create_outgoing.disabled.app_error", + "translation": "Webhooks de Salida han sido deshabilitados por el administrador del sistema." + }, + { + "id": "api.webhook.create_outgoing.triggers.app_error", + "translation": "Debe establecerse palabras gatilladoras o un channel_id" + }, + { + "id": "api.webhook.delete_incoming.disabled.app_errror", + "translation": "Webhooks entrantes han sido deshabilitados por el administrador del sistema." + }, + { + "id": "api.webhook.delete_incoming.permissions.app_errror", + "translation": "Permisos inapropiados para eliminar un webhook entrante" + }, + { + "id": "api.webhook.delete_outgoing.disabled.app_error", + "translation": "Webhooks de Salida han sido deshabilitados por el administrador del sistema." + }, + { + "id": "api.webhook.delete_outgoing.permissions.app_error", + "translation": "Permisos inapropiados para eliminar el webhook saliente" + }, + { + "id": "api.webhook.get_incoming.disabled.app_error", + "translation": "Webhooks entrantes han sido deshabilitados por el administrador del sistema." + }, + { + "id": "api.webhook.get_outgoing.disabled.app_error", + "translation": "Webhooks de Salida han sido deshabilitados por el administrador del sistema." + }, + { + "id": "api.webhook.init.debug", + "translation": "Inicializando rutas del API para los Webhooks" + }, + { + "id": "api.webhook.regen_outgoing_token.disabled.app_error", + "translation": "Webhooks de Salida han sido deshabilitados por el administrador del sistema." + }, + { + "id": "api.webhook.regen_outgoing_token.permissions.app_error", + "translation": "Permisos inapropiados para regenerar un token para el Webhook saliente" + }, + { + "id": "manaultesting.get_channel_id.no_found.debug", + "translation": "No pudimos encontrar el canal: %v, búsqueda realizada con estas posibilidades %v" + }, + { + "id": "manaultesting.get_channel_id.unable.debug", + "translation": "No se pudo obtener los canales" + }, + { + "id": "manaultesting.manual_test.create.info", + "translation": "Creando usuario y equipo" + }, + { + "id": "manaultesting.manual_test.parse.app_error", + "translation": "No se pudo analizar el URL" + }, + { + "id": "manaultesting.manual_test.setup.info", + "translation": "Configurando para pruebas manuales..." + }, + { + "id": "manaultesting.manual_test.uid.debug", + "translation": "No hay un uid en el url" + }, + { + "id": "manaultesting.test_autolink.info", + "translation": "Prueba Manual de Enlaces Automáticos" + }, + { + "id": "manaultesting.test_autolink.unable.app_error", + "translation": "No se pudo obtener los canales" + }, + { "id": "mattermost.current_version", "translation": "La versión actual es %v (%v/%v/%v)" }, { + "id": "model.access.is_valid.access_token.app_error", + "translation": "Token de acceso inválido" + }, + { + "id": "model.access.is_valid.auth_code.app_error", + "translation": "Código de autenticación inválido" + }, + { + "id": "model.access.is_valid.redirect_uri.app_error", + "translation": "URI de redireccionamiento inválido" + }, + { + "id": "model.access.is_valid.refresh_token.app_error", + "translation": "Token de refrescamiento inválido" + }, + { + "id": "model.authorize.is_valid.auth_code.app_error", + "translation": "Código de autorización inválido" + }, + { + "id": "model.authorize.is_valid.client_id.app_error", + "translation": "Id de cliente inválido" + }, + { + "id": "model.authorize.is_valid.create_at.app_error", + "translation": "Create debe ser un tiempo válido" + }, + { + "id": "model.authorize.is_valid.expires.app_error", + "translation": "Se debe asignar el tiempo de Expiración" + }, + { + "id": "model.authorize.is_valid.redirect_uri.app_error", + "translation": "URI de redireccionamiento inválido" + }, + { + "id": "model.authorize.is_valid.scope.app_error", + "translation": "Alcance inválido" + }, + { + "id": "model.authorize.is_valid.state.app_error", + "translation": "Estado inválido" + }, + { + "id": "model.authorize.is_valid.user_id.app_error", + "translation": "Usuario id inválido" + }, + { + "id": "model.channel.is_valid.2_or_more.app_error", + "translation": "Debe tener 2 o más caracteres alfanuméricos en minúscula" + }, + { + "id": "model.channel.is_valid.create_at.app_error", + "translation": "Create debe ser un tiempo válido" + }, + { + "id": "model.channel.is_valid.creator_id.app_error", + "translation": "Id del creador inválido" + }, + { + "id": "model.channel.is_valid.display_name.app_error", + "translation": "Nombre a mostrar inválido" + }, + { + "id": "model.channel.is_valid.header.app_error", + "translation": "Encabezado inválido" + }, + { + "id": "model.channel.is_valid.id.app_error", + "translation": "Id inválido" + }, + { + "id": "model.channel.is_valid.name.app_error", + "translation": "Nombre inválido" + }, + { + "id": "model.channel.is_valid.purpose.app_error", + "translation": "Propósito inválido" + }, + { + "id": "model.channel.is_valid.type.app_error", + "translation": "Tipo inválido" + }, + { + "id": "model.channel.is_valid.update_at.app_error", + "translation": "Update debe ser un tiempo válido" + }, + { + "id": "model.channel_member.is_valid.channel_id.app_error", + "translation": "Channel id inválido" + }, + { + "id": "model.channel_member.is_valid.notify_level.app_error", + "translation": "Nivel de notificación inválido" + }, + { + "id": "model.channel_member.is_valid.role.app_error", + "translation": "Rol inválido" + }, + { + "id": "model.channel_member.is_valid.unread_level.app_error", + "translation": "Nivel de marca para no leidos inválido" + }, + { + "id": "model.channel_member.is_valid.user_id.app_error", + "translation": "User id inválido" + }, + { + "id": "model.client.connecting.app_error", + "translation": "Encontramos un error mientras conectabamos al servidor" + }, + { + "id": "model.client.login.app_error", + "translation": "Token de autenticación no coincidió" + }, + { + "id": "model.config.is_valid.email_reset_salt.app_error", + "translation": "Salt para restablecer contraseñas en la configuración de correos es inválido. Debe ser de 32 caracteres o más." + }, + { + "id": "model.config.is_valid.email_salt.app_error", + "translation": "Salt para crear invitaciones en la configuración de correos es inválido. Debe ser de 32 caracteres o más." + }, + { + "id": "model.config.is_valid.email_security.app_error", + "translation": "Configuración inválida de seguridad en la configuración de correos. Debe ser '', 'TLS', o 'STARTTLS'" + }, + { + "id": "model.config.is_valid.encrypt_sql.app_error", + "translation": "Llave de cifrado rest para las configuraciones de SQL inválida. Debe ser de 32 caracteres o más." + }, + { + "id": "model.config.is_valid.file_driver.app_error", + "translation": "Nombre de controlador para la configuración de archivos es inválido. Debe ser 'local' o 'amazons3'" + }, + { + "id": "model.config.is_valid.file_preview_height.app_error", + "translation": "La altura para la previsualización es inválido en la configuración de archivos. Debe ser cero o un número positivo." + }, + { + "id": "model.config.is_valid.file_preview_width.app_error", + "translation": "El ancho para la previsualización es inválido en la configuración de archivos. Debe ser un número positivo." + }, + { + "id": "model.config.is_valid.file_profile_height.app_error", + "translation": "La altura para la imagen de perfil es inválido en la configuración de archivos. Debe ser un número positivo." + }, + { + "id": "model.config.is_valid.file_profile_width.app_error", + "translation": "El ancho para la imagen de perfil es inválido en la configuración de archivos. Debe ser un número positivo." + }, + { + "id": "model.config.is_valid.file_salt.app_error", + "translation": "Salt para crear enlaces públicos en la configuración a archivos es inválido. Debe ser de 32 caracteres o más." + }, + { + "id": "model.config.is_valid.file_thumb_height.app_error", + "translation": "La altura para la imagen de miniatura es inválido en la configuración de archivos. Debe ser un número positivo." + }, + { + "id": "model.config.is_valid.file_thumb_width.app_error", + "translation": "El ancho para la imagen de miniatura es inválido en la configuración de archivos. Debe ser un número positivo." + }, + { + "id": "model.config.is_valid.listen_address.app_error", + "translation": "Dirección dónde se escuchará el servicio en la configuracón del servicio debe ser asignada." + }, + { + "id": "model.config.is_valid.login_attempts.app_error", + "translation": "Número inválido de máximos intentos de inició de sesión en la configuración del servicio. Debe ser un número positivo." + }, + { + "id": "model.config.is_valid.max_users.app_error", + "translation": "Número inválido del máximo de usuarios por equipo en la configuración de equipo. Debe ser un número positivo." + }, + { + "id": "model.config.is_valid.rate_mem.app_error", + "translation": "Tamaño del almacen de memoria inválido en la configuración de lÃmites de velocidad. Debe ser un número positivo." + }, + { + "id": "model.config.is_valid.rate_sec.app_error", + "translation": "Por segundo es inválido en la configuración de lÃmites de velocidad. Debe ser un número positivo." + }, + { + "id": "model.config.is_valid.sql_data_src.app_error", + "translation": "Fuente de datos no válido para la configuración de SQL. Debe ser asignado." + }, + { + "id": "model.config.is_valid.sql_driver.app_error", + "translation": "Nombre del controlador no válido para la configuración de SQL. Debe ser 'mysql' o 'postgres'" + }, + { + "id": "model.config.is_valid.sql_idle.app_error", + "translation": "Inválido máxima de conexión inactiva para la configuración de SQL. Debe ser un número positivo." + }, + { + "id": "model.config.is_valid.sql_max_conn.app_error", + "translation": "Inválida cantidad de conexiones abiertas para la configuración de SQL. Debe ser un número positivo." + }, + { + "id": "model.file_info.get.gif.app_error", + "translation": "No se pudo decodificar el gif." + }, + { + "id": "model.incoming_hook.channel_id.app_error", + "translation": "Channel id inválido" + }, + { + "id": "model.incoming_hook.create_at.app_error", + "translation": "Create debe ser un tiempo válido" + }, + { + "id": "model.incoming_hook.id.app_error", + "translation": "Id inválido" + }, + { + "id": "model.incoming_hook.team_id.app_error", + "translation": "Id del Equipo inválido" + }, + { + "id": "model.incoming_hook.update_at.app_error", + "translation": "Update debe ser un tiempo válido" + }, + { + "id": "model.incoming_hook.user_id.app_error", + "translation": "Id del Usuario inválido" + }, + { + "id": "model.oauth.is_valid.app_id.app_error", + "translation": "Id de la App inválido" + }, + { + "id": "model.oauth.is_valid.callback.app_error", + "translation": "Callback urls inválido" + }, + { + "id": "model.oauth.is_valid.client_secret.app_error", + "translation": "Llave secreta del Cliente no es válida" + }, + { + "id": "model.oauth.is_valid.create_at.app_error", + "translation": "Create debe ser un tiempo válido" + }, + { + "id": "model.oauth.is_valid.creator_id.app_error", + "translation": "Id del credor no es válido" + }, + { + "id": "model.oauth.is_valid.description.app_error", + "translation": "Descripción inválida" + }, + { + "id": "model.oauth.is_valid.homepage.app_error", + "translation": "Página principal inválida" + }, + { + "id": "model.oauth.is_valid.name.app_error", + "translation": "Nombre inválido" + }, + { + "id": "model.oauth.is_valid.update_at.app_error", + "translation": "Update debe ser un tiempo válido" + }, + { + "id": "model.outgoing_hook.is_valid.callback.app_error", + "translation": "Callback urls inválido" + }, + { + "id": "model.outgoing_hook.is_valid.channel_id.app_error", + "translation": "Id del Canal inválido" + }, + { + "id": "model.outgoing_hook.is_valid.create_at.app_error", + "translation": "Create debe ser un tiempo válido" + }, + { + "id": "model.outgoing_hook.is_valid.id.app_error", + "translation": "Id inválido" + }, + { + "id": "model.outgoing_hook.is_valid.team_id.app_error", + "translation": "Id del Equipo inválido" + }, + { + "id": "model.outgoing_hook.is_valid.token.app_error", + "translation": "Token inválido" + }, + { + "id": "model.outgoing_hook.is_valid.update_at.app_error", + "translation": "Update debe ser un tiempo válido" + }, + { + "id": "model.outgoing_hook.is_valid.url.app_error", + "translation": "Callback URLs inválido. Cada uno debe ser un URL válido y que comience con http:// o https://" + }, + { + "id": "model.outgoing_hook.is_valid.user_id.app_error", + "translation": "Id del Usuario inválido" + }, + { + "id": "model.outgoing_hook.is_valid.words.app_error", + "translation": "Palabras gatilladoras inválidas" + }, + { + "id": "model.post.is_valid.channel_id.app_error", + "translation": "Id del Canal inválido" + }, + { + "id": "model.post.is_valid.create_at.app_error", + "translation": "Create debe ser un tiempo válido" + }, + { + "id": "model.post.is_valid.filenames.app_error", + "translation": "Nombre de archivos no válidos" + }, + { + "id": "model.post.is_valid.hashtags.app_error", + "translation": "Hashtags inválidos" + }, + { + "id": "model.post.is_valid.id.app_error", + "translation": "Id inválido" + }, + { + "id": "model.post.is_valid.msg.app_error", + "translation": "Mensaje no es válido" + }, + { + "id": "model.post.is_valid.original_id.app_error", + "translation": "Id Original inválido" + }, + { + "id": "model.post.is_valid.parent_id.app_error", + "translation": "Id del padre no es válido" + }, + { + "id": "model.post.is_valid.props.app_error", + "translation": "Props inválidos" + }, + { + "id": "model.post.is_valid.root_id.app_error", + "translation": "Id de la raÃz no es válido" + }, + { + "id": "model.post.is_valid.root_parent.app_error", + "translation": "Id de la raÃz no es válido, debe ser asignado si el Id del padre fue asignado" + }, + { + "id": "model.post.is_valid.type.app_error", + "translation": "Tipo inválido" + }, + { + "id": "model.post.is_valid.update_at.app_error", + "translation": "Update debe ser un tiempo válido" + }, + { + "id": "model.post.is_valid.user_id.app_error", + "translation": "Id del Usuario inválido" + }, + { + "id": "model.preference.is_valid.category.app_error", + "translation": "CategorÃa inválida" + }, + { + "id": "model.preference.is_valid.id.app_error", + "translation": "Id del Usuario inválido" + }, + { + "id": "model.preference.is_valid.name.app_error", + "translation": "Nombre inválido" + }, + { + "id": "model.preference.is_valid.value.app_error", + "translation": "El valor es muy largo" + }, + { + "id": "model.team.is_valid.characters.app_error", + "translation": "Nombre tiene que ser de 4 o más caracteres alfanuméricos en minúsculas" + }, + { + "id": "model.team.is_valid.company.app_error", + "translation": "Inválido nombre de la empresa" + }, + { + "id": "model.team.is_valid.create_at.app_error", + "translation": "Create debe ser un tiempo válido" + }, + { + "id": "model.team.is_valid.domains.app_error", + "translation": "Dominios permitidos no válidos" + }, + { + "id": "model.team.is_valid.email.app_error", + "translation": "Correo electrónico inválido" + }, + { + "id": "model.team.is_valid.id.app_error", + "translation": "Id inválido" + }, + { + "id": "model.team.is_valid.name.app_error", + "translation": "Nombre inválido" + }, + { + "id": "model.team.is_valid.reserved.app_error", + "translation": "Este URL no está disponible. Por favor, prueba con otro." + }, + { + "id": "model.team.is_valid.type.app_error", + "translation": "Tipo inválido" + }, + { + "id": "model.team.is_valid.update_at.app_error", + "translation": "Update debe ser un tiempo válido" + }, + { + "id": "model.team.is_valid.url.app_error", + "translation": "Identificador del URL es inválido" + }, + { + "id": "model.user.is_valid.auth_data.app_error", + "translation": "Data de auth es inválida" + }, + { + "id": "model.user.is_valid.auth_data_pwd.app_error", + "translation": "Usuario inválido, no pueden ser asignados auth data y la contraseña al mismo tiempo" + }, + { + "id": "model.user.is_valid.auth_data_type.app_error", + "translation": "Usuario inválido, auth data debe ser asignado con un tipo de auth" + }, + { + "id": "model.user.is_valid.create_at.app_error", + "translation": "Create debe ser un tiempo válido" + }, + { + "id": "model.user.is_valid.email.app_error", + "translation": "Correo electrónico inválido" + }, + { + "id": "model.user.is_valid.first_name.app_error", + "translation": "Nombre inválido" + }, + { + "id": "model.user.is_valid.id.app_error", + "translation": "Id del Usuario inválido" + }, + { + "id": "model.user.is_valid.last_name.app_error", + "translation": "Apellido no es válido" + }, + { + "id": "model.user.is_valid.nickname.app_error", + "translation": "Sobrenombre no es válido" + }, + { + "id": "model.user.is_valid.pwd.app_error", + "translation": "Contraseña inválida" + }, + { + "id": "model.user.is_valid.team_id.app_error", + "translation": "Id del Equipo inválido" + }, + { + "id": "model.user.is_valid.theme.app_error", + "translation": "Tema inválido" + }, + { + "id": "model.user.is_valid.update_at.app_error", + "translation": "Update debe ser un tiempo válido" + }, + { + "id": "model.user.is_valid.username.app_error", + "translation": "Nombre de usuario inválido" + }, + { + "id": "model.utils.decode_json.app_error", + "translation": "no se puede decodificar" + }, + { + "id": "utils.config.load_config.decoding.panic", + "translation": "Error decifrando la configuración del archivo={{.Filename}}, err={{.Error}}" + }, + { + "id": "utils.config.load_config.getting.panic", + "translation": "Error obteniendo la iformación de configuración del archivo={{.Filename}}, err={{.Error}}" + }, + { + "id": "utils.config.load_config.opening.panic", + "translation": "Error abriendo la configuración del archivo={{.Filename}}, err={{.Error}}" + }, + { + "id": "utils.config.load_config.validating.panic", + "translation": "Error validando la configuración del archivo={{.Filename}}, err={{.Error}}" + }, + { + "id": "utils.config.save_config.saving.app_error", + "translation": "Ocurrió un error mientras se guardaba el archivo en {{.Filename}}" + }, + { "id": "utils.i18n.loaded", "translation": "Cargada traducciones del sistema para '%v' desde '%v'" + }, + { + "id": "utils.iru.with_evict", + "translation": "Debe proporcionar un tamaño positivo" + }, + { + "id": "utils.license.load_license.invalid.warn", + "translation": "No se encontró una licencia enterprise válida" + }, + { + "id": "utils.license.load_license.open_find.warn", + "translation": "No pudimos encontrar/abrir el achivo de licencia" + }, + { + "id": "utils.license.remove_license.unable.error", + "translation": "No se pudo remover el archivo de la licencia, err=%v" + }, + { + "id": "utils.license.validate_license.decode.error", + "translation": "Encontramos un error decodificando la licencia, err=%v" + }, + { + "id": "utils.license.validate_license.invalid.error", + "translation": "Firma inválida, err=%v" + }, + { + "id": "utils.license.validate_license.not_long.error", + "translation": "La licencia firmada no es suficientemente larga" + }, + { + "id": "utils.license.validate_license.signing.error", + "translation": "Encontramos un error al firmar la licencia, err=%v" + }, + { + "id": "utils.mail.connect_smtp.open.app_error", + "translation": "Falla al abrir conexión" + }, + { + "id": "utils.mail.connect_smtp.open_tls.app_error", + "translation": "Falla al abrir una conexión TLS" + }, + { + "id": "utils.mail.new_client.auth.app_error", + "translation": "Falla autenticando contra el servidor SMTP" + }, + { + "id": "utils.mail.new_client.open.error", + "translation": "Falla al abrir la conexión al servidor SMTP %v" + }, + { + "id": "utils.mail.send_mail.close.app_error", + "translation": "Falla al cerrar la conexión al servidor SMTP" + }, + { + "id": "utils.mail.send_mail.from_address.app_error", + "translation": "Falla al agregar el correo electrónico desde" + }, + { + "id": "utils.mail.send_mail.msg.app_error", + "translation": "Falla al escribir el mensaje del correo electrónico" + }, + { + "id": "utils.mail.send_mail.msg_data.app_error", + "translation": "Falla al agregar la data al mensaje del correo electrónico" + }, + { + "id": "utils.mail.send_mail.sending.debug", + "translation": "enviano correo electrónico a %v con el asunto '%v'" + }, + { + "id": "utils.mail.send_mail.to_address.app_error", + "translation": "Falla al agregar el correo electrónico para" + }, + { + "id": "utils.mail.test.configured.error", + "translation": "El servidor SMTP parece no estar configurado apropiadamente err=%v details=%v" } ]
\ No newline at end of file diff --git a/manualtesting/manual_testing.go b/manualtesting/manual_testing.go index befc835fb..2f1096fd5 100644 --- a/manualtesting/manual_testing.go +++ b/manualtesting/manual_testing.go @@ -32,12 +32,12 @@ func InitManualTesting() { func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) { // Let the world know - l4g.Info("Setting up for manual test...") + l4g.Info(utils.T("manaultesting.manual_test.setup.info")) // URL Parameters params, err := url.ParseQuery(r.URL.RawQuery) if err != nil { - c.Err = model.NewAppError("/manual", "Unable to parse URL", "") + c.Err = model.NewLocAppError("/manual", "manaultesting.manual_test.parse.app_error", nil, "") return } @@ -49,7 +49,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) { hash := hasher.Sum32() rand.Seed(int64(hash)) } else { - l4g.Debug("No uid in url") + l4g.Debug(utils.T("manaultesting.manual_test.uid.debug")) } // Create a client for tests to use @@ -61,7 +61,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) { var teamID string var userID string if ok1 && ok2 { - l4g.Info("Creating user and team") + l4g.Info(utils.T("manaultesting.manual_test.create.info")) // Create team for testing team := &model.Team{ DisplayName: teamDisplayName[0], @@ -153,7 +153,7 @@ func getChannelID(channelname string, teamid string, userid string) (id string, // Grab all the channels result := <-api.Srv.Store.Channel().GetChannels(teamid, userid) if result.Err != nil { - l4g.Debug("Unable to get channels") + l4g.Debug(utils.T("manaultesting.get_channel_id.unable.debug")) return "", false } @@ -164,6 +164,6 @@ func getChannelID(channelname string, teamid string, userid string) (id string, return channel.Id, true } } - l4g.Debug("Could not find channel: " + channelname + ", " + strconv.Itoa(len(data.Channels)) + " possibilites searched") + l4g.Debug(utils.T("manaultesting.get_channel_id.no_found.debug"), channelname, strconv.Itoa(len(data.Channels))) return "", false } diff --git a/manualtesting/test_autolink.go b/manualtesting/test_autolink.go index 16d2d713a..f9f213da1 100644 --- a/manualtesting/test_autolink.go +++ b/manualtesting/test_autolink.go @@ -6,6 +6,7 @@ package manualtesting import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" ) const LINK_POST_TEXT = ` @@ -20,10 +21,10 @@ https://medium.com/@slackhq/11-useful-tips-for-getting-the-most-of-slack-5dfb3d1 ` func testAutoLink(env TestEnvironment) *model.AppError { - l4g.Info("Manual Auto Link Test") + l4g.Info(utils.T("manaultesting.test_autolink.info")) channelID, err := getChannelID(model.DEFAULT_CHANNEL, env.CreatedTeamId, env.CreatedUserId) if err != true { - return model.NewAppError("/manualtest", "Unable to get channels", "") + return model.NewLocAppError("/manualtest", "manaultesting.test_autolink.unable.app_error", nil, "") } post := &model.Post{ diff --git a/mattermost.go b/mattermost.go index 9786a6abd..51a9591db 100644 --- a/mattermost.go +++ b/mattermost.go @@ -66,7 +66,9 @@ func main() { api.InitApi() web.InitWeb() - utils.LoadLicense() + if model.BuildEnterpriseReady == "true" { + utils.LoadLicense() + } if flagRunCmds { runCmds() diff --git a/model/access.go b/model/access.go index 6c9254004..877b3c4f0 100644 --- a/model/access.go +++ b/model/access.go @@ -34,19 +34,19 @@ type AccessResponse struct { func (ad *AccessData) IsValid() *AppError { if len(ad.AuthCode) == 0 || len(ad.AuthCode) > 128 { - return NewAppError("AccessData.IsValid", "Invalid auth code", "") + return NewLocAppError("AccessData.IsValid", "model.access.is_valid.auth_code.app_error", nil, "") } if len(ad.Token) != 26 { - return NewAppError("AccessData.IsValid", "Invalid access token", "") + return NewLocAppError("AccessData.IsValid", "model.access.is_valid.access_token.app_error", nil, "") } if len(ad.RefreshToken) > 26 { - return NewAppError("AccessData.IsValid", "Invalid refresh token", "") + return NewLocAppError("AccessData.IsValid", "model.access.is_valid.refresh_token.app_error", nil, "") } if len(ad.RedirectUri) > 256 { - return NewAppError("AccessData.IsValid", "Invalid redirect uri", "") + return NewLocAppError("AccessData.IsValid", "model.access.is_valid.redirect_uri.app_error", nil, "") } return nil diff --git a/model/authorize.go b/model/authorize.go index 4176a9b89..e0d665bae 100644 --- a/model/authorize.go +++ b/model/authorize.go @@ -29,35 +29,35 @@ type AuthData struct { func (ad *AuthData) IsValid() *AppError { if len(ad.ClientId) != 26 { - return NewAppError("AuthData.IsValid", "Invalid client id", "") + return NewLocAppError("AuthData.IsValid", "model.authorize.is_valid.client_id.app_error", nil, "") } if len(ad.UserId) != 26 { - return NewAppError("AuthData.IsValid", "Invalid user id", "") + return NewLocAppError("AuthData.IsValid", "model.authorize.is_valid.user_id.app_error", nil, "") } if len(ad.Code) == 0 || len(ad.Code) > 128 { - return NewAppError("AuthData.IsValid", "Invalid authorization code", "client_id="+ad.ClientId) + return NewLocAppError("AuthData.IsValid", "model.authorize.is_valid.auth_code.app_error", nil, "client_id="+ad.ClientId) } if ad.ExpiresIn == 0 { - return NewAppError("AuthData.IsValid", "Expires in must be set", "") + return NewLocAppError("AuthData.IsValid", "model.authorize.is_valid.expires.app_error", nil, "") } if ad.CreateAt <= 0 { - return NewAppError("AuthData.IsValid", "Create at must be a valid time", "client_id="+ad.ClientId) + return NewLocAppError("AuthData.IsValid", "model.authorize.is_valid.create_at.app_error", nil, "client_id="+ad.ClientId) } if len(ad.RedirectUri) > 256 { - return NewAppError("AuthData.IsValid", "Invalid redirect uri", "client_id="+ad.ClientId) + return NewLocAppError("AuthData.IsValid", "model.authorize.is_valid.redirect_uri.app_error", nil, "client_id="+ad.ClientId) } if len(ad.State) > 128 { - return NewAppError("AuthData.IsValid", "Invalid state", "client_id="+ad.ClientId) + return NewLocAppError("AuthData.IsValid", "model.authorize.is_valid.state.app_error", nil, "client_id="+ad.ClientId) } if len(ad.Scope) > 128 { - return NewAppError("AuthData.IsValid", "Invalid scope", "client_id="+ad.ClientId) + return NewLocAppError("AuthData.IsValid", "model.authorize.is_valid.scope.app_error", nil, "client_id="+ad.ClientId) } return nil diff --git a/model/channel.go b/model/channel.go index 7109500d4..e7002e3cb 100644 --- a/model/channel.go +++ b/model/channel.go @@ -64,43 +64,43 @@ func (o *Channel) ExtraEtag(memberLimit int) string { func (o *Channel) IsValid() *AppError { if len(o.Id) != 26 { - return NewAppError("Channel.IsValid", "Invalid Id", "") + return NewLocAppError("Channel.IsValid", "model.channel.is_valid.id.app_error", nil, "") } if o.CreateAt == 0 { - return NewAppError("Channel.IsValid", "Create at must be a valid time", "id="+o.Id) + return NewLocAppError("Channel.IsValid", "model.channel.is_valid.create_at.app_error", nil, "id="+o.Id) } if o.UpdateAt == 0 { - return NewAppError("Channel.IsValid", "Update at must be a valid time", "id="+o.Id) + return NewLocAppError("Channel.IsValid", "model.channel.is_valid.update_at.app_error", nil, "id="+o.Id) } if utf8.RuneCountInString(o.DisplayName) > 64 { - return NewAppError("Channel.IsValid", "Invalid display name", "id="+o.Id) + return NewLocAppError("Channel.IsValid", "model.channel.is_valid.display_name.app_error", nil, "id="+o.Id) } if len(o.Name) > 64 { - return NewAppError("Channel.IsValid", "Invalid name", "id="+o.Id) + return NewLocAppError("Channel.IsValid", "model.channel.is_valid.name.app_error", nil, "id="+o.Id) } if !IsValidChannelIdentifier(o.Name) { - return NewAppError("Channel.IsValid", "Name must be 2 or more lowercase alphanumeric characters", "id="+o.Id) + return NewLocAppError("Channel.IsValid", "model.channel.is_valid.2_or_more.app_error", nil, "id="+o.Id) } if !(o.Type == CHANNEL_OPEN || o.Type == CHANNEL_PRIVATE || o.Type == CHANNEL_DIRECT) { - return NewAppError("Channel.IsValid", "Invalid type", "id="+o.Id) + return NewLocAppError("Channel.IsValid", "model.channel.is_valid.type.app_error", nil, "id="+o.Id) } if utf8.RuneCountInString(o.Header) > 1024 { - return NewAppError("Channel.IsValid", "Invalid header", "id="+o.Id) + return NewLocAppError("Channel.IsValid", "model.channel.is_valid.header.app_error", nil, "id="+o.Id) } if utf8.RuneCountInString(o.Purpose) > 128 { - return NewAppError("Channel.IsValid", "Invalid purpose", "id="+o.Id) + return NewLocAppError("Channel.IsValid", "model.channel.is_valid.purpose.app_error", nil, "id="+o.Id) } if len(o.CreatorId) > 26 { - return NewAppError("Channel.IsValid", "Invalid creator id", "") + return NewLocAppError("Channel.IsValid", "model.channel.is_valid.creator_id.app_error", nil, "") } return nil diff --git a/model/channel_member.go b/model/channel_member.go index e822ba443..66e20da64 100644 --- a/model/channel_member.go +++ b/model/channel_member.go @@ -53,27 +53,29 @@ func ChannelMemberFromJson(data io.Reader) *ChannelMember { func (o *ChannelMember) IsValid() *AppError { if len(o.ChannelId) != 26 { - return NewAppError("ChannelMember.IsValid", "Invalid channel id", "") + return NewLocAppError("ChannelMember.IsValid", "model.channel_member.is_valid.channel_id.app_error", nil, "") } if len(o.UserId) != 26 { - return NewAppError("ChannelMember.IsValid", "Invalid user id", "") + return NewLocAppError("ChannelMember.IsValid", "model.channel_member.is_valid.user_id.app_error", nil, "") } for _, role := range strings.Split(o.Roles, " ") { if !(role == "" || role == CHANNEL_ROLE_ADMIN) { - return NewAppError("ChannelMember.IsValid", "Invalid role", "role="+role) + return NewLocAppError("ChannelMember.IsValid", "model.channel_member.is_valid.role.app_error", nil, "role="+role) } } notifyLevel := o.NotifyProps["desktop"] if len(notifyLevel) > 20 || !IsChannelNotifyLevelValid(notifyLevel) { - return NewAppError("ChannelMember.IsValid", "Invalid notify level", "notify_level="+notifyLevel) + return NewLocAppError("ChannelMember.IsValid", "model.channel_member.is_valid.notify_level.app_error", + nil, "notify_level="+notifyLevel) } markUnreadLevel := o.NotifyProps["mark_unread"] if len(markUnreadLevel) > 20 || !IsChannelMarkUnreadLevelValid(markUnreadLevel) { - return NewAppError("ChannelMember.IsValid", "Invalid mark unread level", "mark_unread_level="+markUnreadLevel) + return NewLocAppError("ChannelMember.IsValid", "model.channel_member.is_valid.unread_level.app_error", + nil, "mark_unread_level="+markUnreadLevel) } return nil diff --git a/model/client.go b/model/client.go index a4da6d513..21b8d8f7a 100644 --- a/model/client.go +++ b/model/client.go @@ -56,7 +56,7 @@ func (c *Client) DoPost(url, data, contentType string) (*http.Response, *AppErro rq.Header.Set("Content-Type", contentType) if rp, err := c.HttpClient.Do(rq); err != nil { - return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error()) + return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error()) } else if rp.StatusCode >= 300 { return nil, AppErrorFromJson(rp.Body) } else { @@ -72,7 +72,7 @@ func (c *Client) DoApiPost(url string, data string) (*http.Response, *AppError) } if rp, err := c.HttpClient.Do(rq); err != nil { - return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error()) + return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error()) } else if rp.StatusCode >= 300 { return nil, AppErrorFromJson(rp.Body) } else { @@ -92,7 +92,7 @@ func (c *Client) DoApiGet(url string, data string, etag string) (*http.Response, } if rp, err := c.HttpClient.Do(rq); err != nil { - return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error()) + return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error()) } else if rp.StatusCode == 304 { return rp, nil } else if rp.StatusCode >= 300 { @@ -298,7 +298,7 @@ func (c *Client) login(m map[string]string) (*Result, *AppError) { sessionToken := getCookie(SESSION_COOKIE_TOKEN, r) if c.AuthToken != sessionToken.Value { - NewAppError("/users/login", "Authentication tokens didn't match", "") + NewLocAppError("/users/login", "model.client.login.app_error", nil, "") } return &Result{r.Header.Get(HEADER_REQUEST_ID), @@ -479,7 +479,7 @@ func (c *Client) TestEmail(config *Config) (*Result, *AppError) { } } -func (c *Client) GetAnalytics(teamId, name string) (*Result, *AppError) { +func (c *Client) GetTeamAnalytics(teamId, name string) (*Result, *AppError) { if r, err := c.DoApiGet("/admin/analytics/"+teamId+"/"+name, "", ""); err != nil { return nil, err } else { @@ -488,6 +488,15 @@ func (c *Client) GetAnalytics(teamId, name string) (*Result, *AppError) { } } +func (c *Client) GetSystemAnalytics(name string) (*Result, *AppError) { + if r, err := c.DoApiGet("/admin/analytics/"+name, "", ""); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), AnalyticsRowsFromJson(r.Body)}, nil + } +} + func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) { if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil { return nil, err @@ -735,7 +744,7 @@ func (c *Client) UploadFile(url string, data []byte, contentType string) (*Resul } if rp, err := c.HttpClient.Do(rq); err != nil { - return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error()) + return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error()) } else if rp.StatusCode >= 300 { return nil, AppErrorFromJson(rp.Body) } else { @@ -757,7 +766,7 @@ func (c *Client) GetFile(url string, isFullUrl bool) (*Result, *AppError) { } if rp, err := c.HttpClient.Do(rq); err != nil { - return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error()) + return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error()) } else if rp.StatusCode >= 300 { return nil, AppErrorFromJson(rp.Body) } else { @@ -775,7 +784,7 @@ func (c *Client) GetFileInfo(url string) (*Result, *AppError) { } if rp, err := c.HttpClient.Do(rq); err != nil { - return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error()) + return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error()) } else if rp.StatusCode >= 300 { return nil, AppErrorFromJson(rp.Body) } else { diff --git a/model/config.go b/model/config.go index c0e66d50c..f518e8f8d 100644 --- a/model/config.go +++ b/model/config.go @@ -359,87 +359,87 @@ func (o *Config) SetDefaults() { func (o *Config) IsValid() *AppError { if o.ServiceSettings.MaximumLoginAttempts <= 0 { - return NewAppError("Config.IsValid", "Invalid maximum login attempts for service settings. Must be a positive number.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.login_attempts.app_error", nil, "") } if len(o.ServiceSettings.ListenAddress) == 0 { - return NewAppError("Config.IsValid", "Invalid listen address for service settings Must be set.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.listen_address.app_error", nil, "") } if o.TeamSettings.MaxUsersPerTeam <= 0 { - return NewAppError("Config.IsValid", "Invalid maximum users per team for team settings. Must be a positive number.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.max_users.app_error", nil, "") } if len(o.SqlSettings.AtRestEncryptKey) < 32 { - return NewAppError("Config.IsValid", "Invalid at rest encrypt key for SQL settings. Must be 32 chars or more.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.encrypt_sql.app_error", nil, "") } if !(o.SqlSettings.DriverName == DATABASE_DRIVER_MYSQL || o.SqlSettings.DriverName == DATABASE_DRIVER_POSTGRES) { - return NewAppError("Config.IsValid", "Invalid driver name for SQL settings. Must be 'mysql' or 'postgres'", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.sql_driver.app_error", nil, "") } if o.SqlSettings.MaxIdleConns <= 0 { - return NewAppError("Config.IsValid", "Invalid maximum idle connection for SQL settings. Must be a positive number.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.sql_idle.app_error", nil, "") } if len(o.SqlSettings.DataSource) == 0 { - return NewAppError("Config.IsValid", "Invalid data source for SQL settings. Must be set.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.sql_data_src.app_error", nil, "") } if o.SqlSettings.MaxOpenConns <= 0 { - return NewAppError("Config.IsValid", "Invalid maximum open connection for SQL settings. Must be a positive number.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.sql_max_conn.app_error", nil, "") } if !(o.FileSettings.DriverName == IMAGE_DRIVER_LOCAL || o.FileSettings.DriverName == IMAGE_DRIVER_S3) { - return NewAppError("Config.IsValid", "Invalid driver name for file settings. Must be 'local' or 'amazons3'", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.file_driver.app_error", nil, "") } if o.FileSettings.PreviewHeight < 0 { - return NewAppError("Config.IsValid", "Invalid preview height for file settings. Must be a zero or positive number.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.file_preview_height.app_error", nil, "") } if o.FileSettings.PreviewWidth <= 0 { - return NewAppError("Config.IsValid", "Invalid preview width for file settings. Must be a positive number.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.file_preview_width.app_error", nil, "") } if o.FileSettings.ProfileHeight <= 0 { - return NewAppError("Config.IsValid", "Invalid profile height for file settings. Must be a positive number.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.file_profile_height.app_error", nil, "") } if o.FileSettings.ProfileWidth <= 0 { - return NewAppError("Config.IsValid", "Invalid profile width for file settings. Must be a positive number.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.file_profile_width.app_error", nil, "") } if o.FileSettings.ThumbnailHeight <= 0 { - return NewAppError("Config.IsValid", "Invalid thumbnail height for file settings. Must be a positive number.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.file_thumb_height.app_error", nil, "") } - if o.FileSettings.ThumbnailHeight <= 0 { - return NewAppError("Config.IsValid", "Invalid thumbnail width for file settings. Must be a positive number.", "") + if o.FileSettings.ThumbnailWidth <= 0 { + return NewLocAppError("Config.IsValid", "model.config.is_valid.file_thumb_width.app_error", nil, "") } if len(o.FileSettings.PublicLinkSalt) < 32 { - return NewAppError("Config.IsValid", "Invalid public link salt for file settings. Must be 32 chars or more.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.file_salt.app_error", nil, "") } if !(o.EmailSettings.ConnectionSecurity == CONN_SECURITY_NONE || o.EmailSettings.ConnectionSecurity == CONN_SECURITY_TLS || o.EmailSettings.ConnectionSecurity == CONN_SECURITY_STARTTLS) { - return NewAppError("Config.IsValid", "Invalid connection security for email settings. Must be '', 'TLS', or 'STARTTLS'", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.email_security.app_error", nil, "") } if len(o.EmailSettings.InviteSalt) < 32 { - return NewAppError("Config.IsValid", "Invalid invite salt for email settings. Must be 32 chars or more.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.email_salt.app_error", nil, "") } if len(o.EmailSettings.PasswordResetSalt) < 32 { - return NewAppError("Config.IsValid", "Invalid password reset salt for email settings. Must be 32 chars or more.", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.email_reset_salt.app_error", nil, "") } if o.RateLimitSettings.MemoryStoreSize <= 0 { - return NewAppError("Config.IsValid", "Invalid memory store size for rate limit settings. Must be a positive number", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.rate_mem.app_error", nil, "") } if o.RateLimitSettings.PerSec <= 0 { - return NewAppError("Config.IsValid", "Invalid per sec for rate limit settings. Must be a positive number", "") + return NewLocAppError("Config.IsValid", "model.config.is_valid.rate_sec.app_error", nil, "") } return nil diff --git a/model/file_info.go b/model/file_info.go index 741b4e55d..f785042b3 100644 --- a/model/file_info.go +++ b/model/file_info.go @@ -32,11 +32,16 @@ func GetInfoForBytes(filename string, data []byte) (*FileInfo, *AppError) { mimeType = mime.TypeByExtension(extension) } + if extension != "" && extension[0] == '.' { + // the client expects a file extension without the leading period + extension = extension[1:] + } + hasPreviewImage := isImage if mimeType == "image/gif" { // just show the gif itself instead of a preview image for animated gifs if gifImage, err := gif.DecodeAll(bytes.NewReader(data)); err != nil { - return nil, NewAppError("GetInfoForBytes", "Could not decode gif.", "filename="+filename) + return nil, NewLocAppError("GetInfoForBytes", "model.file_info.get.gif.app_error", nil, "filename="+filename) } else { hasPreviewImage = len(gifImage.Image) == 1 } @@ -45,7 +50,7 @@ func GetInfoForBytes(filename string, data []byte) (*FileInfo, *AppError) { return &FileInfo{ Filename: filename, Size: size, - Extension: extension[1:], + Extension: extension, MimeType: mimeType, HasPreviewImage: hasPreviewImage, }, nil diff --git a/model/file_info_test.go b/model/file_info_test.go index ecf0d509c..e89681626 100644 --- a/model/file_info_test.go +++ b/model/file_info_test.go @@ -19,7 +19,7 @@ func TestGetInfoForBytes(t *testing.T) { } else if info.Size != 1000 { t.Fatalf("Got incorrect size: %v", info.Size) } else if info.Extension != "txt" { - t.Fatalf("Git incorrect file extension: %v", info.Extension) + t.Fatalf("Got incorrect file extension: %v", info.Extension) } else if info.MimeType != "text/plain; charset=utf-8" { t.Fatalf("Got incorrect mime type: %v", info.MimeType) } else if info.HasPreviewImage { @@ -33,7 +33,7 @@ func TestGetInfoForBytes(t *testing.T) { } else if info.Size != 1000 { t.Fatalf("Got incorrect size: %v", info.Size) } else if info.Extension != "png" { - t.Fatalf("Git incorrect file extension: %v", info.Extension) + t.Fatalf("Got incorrect file extension: %v", info.Extension) } else if info.MimeType != "image/png" { t.Fatalf("Got incorrect mime type: %v", info.MimeType) } else if !info.HasPreviewImage { @@ -49,7 +49,7 @@ func TestGetInfoForBytes(t *testing.T) { } else if info.Size != 35 { t.Fatalf("Got incorrect size: %v", info.Size) } else if info.Extension != "gif" { - t.Fatalf("Git incorrect file extension: %v", info.Extension) + t.Fatalf("Got incorrect file extension: %v", info.Extension) } else if info.MimeType != "image/gif" { t.Fatalf("Got incorrect mime type: %v", info.MimeType) } else if !info.HasPreviewImage { @@ -67,10 +67,24 @@ func TestGetInfoForBytes(t *testing.T) { } else if info.Size != 38689 { t.Fatalf("Got incorrect size: %v", info.Size) } else if info.Extension != "gif" { - t.Fatalf("Git incorrect file extension: %v", info.Extension) + t.Fatalf("Got incorrect file extension: %v", info.Extension) } else if info.MimeType != "image/gif" { t.Fatalf("Got incorrect mime type: %v", info.MimeType) } else if info.HasPreviewImage { t.Fatalf("Got HasPreviewImage = true for animated gif") } + + if info, err := GetInfoForBytes("filewithoutextension", fakeFile); err != nil { + t.Fatal(err) + } else if info.Filename != "filewithoutextension" { + t.Fatalf("Got incorrect filename: %v", info.Filename) + } else if info.Size != 1000 { + t.Fatalf("Got incorrect size: %v", info.Size) + } else if info.Extension != "" { + t.Fatalf("Got incorrect file extension: %v", info.Extension) + } else if info.MimeType != "" { + t.Fatalf("Got incorrect mime type: %v", info.MimeType) + } else if info.HasPreviewImage { + t.Fatalf("Got HasPreviewImage = true for non-image file") + } } diff --git a/model/incoming_webhook.go b/model/incoming_webhook.go index 8ead0da9f..8432f5fea 100644 --- a/model/incoming_webhook.go +++ b/model/incoming_webhook.go @@ -76,27 +76,27 @@ func IncomingWebhookListFromJson(data io.Reader) []*IncomingWebhook { func (o *IncomingWebhook) IsValid() *AppError { if len(o.Id) != 26 { - return NewAppError("IncomingWebhook.IsValid", "Invalid Id", "") + return NewLocAppError("IncomingWebhook.IsValid", "model.incoming_hook.id.app_error", nil, "") } if o.CreateAt == 0 { - return NewAppError("IncomingWebhook.IsValid", "Create at must be a valid time", "id="+o.Id) + return NewLocAppError("IncomingWebhook.IsValid", "model.incoming_hook.create_at.app_error", nil, "id="+o.Id) } if o.UpdateAt == 0 { - return NewAppError("IncomingWebhook.IsValid", "Update at must be a valid time", "id="+o.Id) + return NewLocAppError("IncomingWebhook.IsValid", "model.incoming_hook.update_at.app_error", nil, "id="+o.Id) } if len(o.UserId) != 26 { - return NewAppError("IncomingWebhook.IsValid", "Invalid user id", "") + return NewLocAppError("IncomingWebhook.IsValid", "model.incoming_hook.user_id.app_error", nil, "") } if len(o.ChannelId) != 26 { - return NewAppError("IncomingWebhook.IsValid", "Invalid channel id", "") + return NewLocAppError("IncomingWebhook.IsValid", "model.incoming_hook.channel_id.app_error", nil, "") } if len(o.TeamId) != 26 { - return NewAppError("IncomingWebhook.IsValid", "Invalid channel id", "") + return NewLocAppError("IncomingWebhook.IsValid", "model.incoming_hook.team_id.app_error", nil, "") } return nil diff --git a/model/oauth.go b/model/oauth.go index 8336e26ba..c54df107c 100644 --- a/model/oauth.go +++ b/model/oauth.go @@ -34,39 +34,39 @@ type OAuthApp struct { func (a *OAuthApp) IsValid() *AppError { if len(a.Id) != 26 { - return NewAppError("OAuthApp.IsValid", "Invalid app id", "") + return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.app_id.app_error", nil, "") } if a.CreateAt == 0 { - return NewAppError("OAuthApp.IsValid", "Create at must be a valid time", "app_id="+a.Id) + return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.create_at.app_error", nil, "app_id="+a.Id) } if a.UpdateAt == 0 { - return NewAppError("OAuthApp.IsValid", "Update at must be a valid time", "app_id="+a.Id) + return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.update_at.app_error", nil, "app_id="+a.Id) } if len(a.CreatorId) != 26 { - return NewAppError("OAuthApp.IsValid", "Invalid creator id", "app_id="+a.Id) + return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.creator_id.app_error", nil, "app_id="+a.Id) } if len(a.ClientSecret) == 0 || len(a.ClientSecret) > 128 { - return NewAppError("OAuthApp.IsValid", "Invalid client secret", "app_id="+a.Id) + return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.client_secret.app_error", nil, "app_id="+a.Id) } if len(a.Name) == 0 || len(a.Name) > 64 { - return NewAppError("OAuthApp.IsValid", "Invalid name", "app_id="+a.Id) + return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.name.app_error", nil, "app_id="+a.Id) } if len(a.CallbackUrls) == 0 || len(fmt.Sprintf("%s", a.CallbackUrls)) > 1024 { - return NewAppError("OAuthApp.IsValid", "Invalid callback urls", "app_id="+a.Id) + return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "app_id="+a.Id) } if len(a.Homepage) == 0 || len(a.Homepage) > 256 { - return NewAppError("OAuthApp.IsValid", "Invalid homepage", "app_id="+a.Id) + return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.homepage.app_error", nil, "app_id="+a.Id) } if utf8.RuneCountInString(a.Description) > 512 { - return NewAppError("OAuthApp.IsValid", "Invalid description", "app_id="+a.Id) + return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.description.app_error", nil, "app_id="+a.Id) } return nil diff --git a/model/outgoing_webhook.go b/model/outgoing_webhook.go index 0b4fd6bbe..70de4d26e 100644 --- a/model/outgoing_webhook.go +++ b/model/outgoing_webhook.go @@ -65,44 +65,44 @@ func OutgoingWebhookListFromJson(data io.Reader) []*OutgoingWebhook { func (o *OutgoingWebhook) IsValid() *AppError { if len(o.Id) != 26 { - return NewAppError("OutgoingWebhook.IsValid", "Invalid Id", "") + return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.id.app_error", nil, "") } if len(o.Token) != 26 { - return NewAppError("OutgoingWebhook.IsValid", "Invalid token", "") + return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.token.app_error", nil, "") } if o.CreateAt == 0 { - return NewAppError("OutgoingWebhook.IsValid", "Create at must be a valid time", "id="+o.Id) + return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.create_at.app_error", nil, "id="+o.Id) } if o.UpdateAt == 0 { - return NewAppError("OutgoingWebhook.IsValid", "Update at must be a valid time", "id="+o.Id) + return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.update_at.app_error", nil, "id="+o.Id) } if len(o.CreatorId) != 26 { - return NewAppError("OutgoingWebhook.IsValid", "Invalid user id", "") + return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.user_id.app_error", nil, "") } if len(o.ChannelId) != 0 && len(o.ChannelId) != 26 { - return NewAppError("OutgoingWebhook.IsValid", "Invalid channel id", "") + return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.channel_id.app_error", nil, "") } if len(o.TeamId) != 26 { - return NewAppError("OutgoingWebhook.IsValid", "Invalid team id", "") + return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.team_id.app_error", nil, "") } if len(fmt.Sprintf("%s", o.TriggerWords)) > 1024 { - return NewAppError("OutgoingWebhook.IsValid", "Invalid trigger words", "") + return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.words.app_error", nil, "") } if len(o.CallbackURLs) == 0 || len(fmt.Sprintf("%s", o.CallbackURLs)) > 1024 { - return NewAppError("OutgoingWebhook.IsValid", "Invalid callback urls", "") + return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.callback.app_error", nil, "") } for _, callback := range o.CallbackURLs { if !IsValidHttpUrl(callback) { - return NewAppError("OutgoingWebhook.IsValid", "Invalid callback URLs. Each must be a valid URL and start with http:// or https://", "") + return NewLocAppError("OutgoingWebhook.IsValid", "model.outgoing_hook.is_valid.url.app_error", nil, "") } } diff --git a/model/post.go b/model/post.go index 5c86ce70d..f9f5a4d1c 100644 --- a/model/post.go +++ b/model/post.go @@ -62,60 +62,60 @@ func (o *Post) Etag() string { func (o *Post) IsValid() *AppError { if len(o.Id) != 26 { - return NewAppError("Post.IsValid", "Invalid Id", "") + return NewLocAppError("Post.IsValid", "model.post.is_valid.id.app_error", nil, "") } if o.CreateAt == 0 { - return NewAppError("Post.IsValid", "Create at must be a valid time", "id="+o.Id) + return NewLocAppError("Post.IsValid", "model.post.is_valid.create_at.app_error", nil, "id="+o.Id) } if o.UpdateAt == 0 { - return NewAppError("Post.IsValid", "Update at must be a valid time", "id="+o.Id) + return NewLocAppError("Post.IsValid", "model.post.is_valid.update_at.app_error", nil, "id="+o.Id) } if len(o.UserId) != 26 { - return NewAppError("Post.IsValid", "Invalid user id", "") + return NewLocAppError("Post.IsValid", "model.post.is_valid.user_id.app_error", nil, "") } if len(o.ChannelId) != 26 { - return NewAppError("Post.IsValid", "Invalid channel id", "") + return NewLocAppError("Post.IsValid", "model.post.is_valid.channel_id.app_error", nil, "") } if !(len(o.RootId) == 26 || len(o.RootId) == 0) { - return NewAppError("Post.IsValid", "Invalid root id", "") + return NewLocAppError("Post.IsValid", "model.post.is_valid.root_id.app_error", nil, "") } if !(len(o.ParentId) == 26 || len(o.ParentId) == 0) { - return NewAppError("Post.IsValid", "Invalid parent id", "") + return NewLocAppError("Post.IsValid", "model.post.is_valid.parent_id.app_error", nil, "") } if len(o.ParentId) == 26 && len(o.RootId) == 0 { - return NewAppError("Post.IsValid", "Invalid root id must be set if parent id set", "") + return NewLocAppError("Post.IsValid", "model.post.is_valid.root_parent.app_error", nil, "") } if !(len(o.OriginalId) == 26 || len(o.OriginalId) == 0) { - return NewAppError("Post.IsValid", "Invalid original id", "") + return NewLocAppError("Post.IsValid", "model.post.is_valid.original_id.app_error", nil, "") } if utf8.RuneCountInString(o.Message) > 4000 { - return NewAppError("Post.IsValid", "Invalid message", "id="+o.Id) + return NewLocAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id) } if utf8.RuneCountInString(o.Hashtags) > 1000 { - return NewAppError("Post.IsValid", "Invalid hashtags", "id="+o.Id) + return NewLocAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id) } // should be removed once more message types are supported if !(o.Type == POST_DEFAULT || o.Type == POST_JOIN_LEAVE || o.Type == POST_SLACK_ATTACHMENT || o.Type == POST_HEADER_CHANGE) { - return NewAppError("Post.IsValid", "Invalid type", "id="+o.Type) + return NewLocAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type) } if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > 4000 { - return NewAppError("Post.IsValid", "Invalid filenames", "id="+o.Id) + return NewLocAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id) } if utf8.RuneCountInString(StringInterfaceToJson(o.Props)) > 8000 { - return NewAppError("Post.IsValid", "Invalid props", "id="+o.Id) + return NewLocAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id) } return nil diff --git a/model/preference.go b/model/preference.go index e3ad23ed4..b2ec93105 100644 --- a/model/preference.go +++ b/model/preference.go @@ -47,19 +47,19 @@ func PreferenceFromJson(data io.Reader) *Preference { func (o *Preference) IsValid() *AppError { if len(o.UserId) != 26 { - return NewAppError("Preference.IsValid", "Invalid user id", "user_id="+o.UserId) + return NewLocAppError("Preference.IsValid", "model.preference.is_valid.id.app_error", nil, "user_id="+o.UserId) } if len(o.Category) == 0 || len(o.Category) > 32 { - return NewAppError("Preference.IsValid", "Invalid category", "category="+o.Category) + return NewLocAppError("Preference.IsValid", "model.preference.is_valid.category.app_error", nil, "category="+o.Category) } if len(o.Name) == 0 || len(o.Name) > 32 { - return NewAppError("Preference.IsValid", "Invalid name", "name="+o.Name) + return NewLocAppError("Preference.IsValid", "model.preference.is_valid.name.app_error", nil, "name="+o.Name) } if utf8.RuneCountInString(o.Value) > 128 { - return NewAppError("Preference.IsValid", "Value is too long", "value="+o.Value) + return NewLocAppError("Preference.IsValid", "model.preference.is_valid.value.app_error", nil, "value="+o.Value) } return nil diff --git a/model/team.go b/model/team.go index e7dde4766..9e9eaa25f 100644 --- a/model/team.go +++ b/model/team.go @@ -104,51 +104,51 @@ func (o *Team) Etag() string { func (o *Team) IsValid(restrictTeamNames bool) *AppError { if len(o.Id) != 26 { - return NewAppError("Team.IsValid", "Invalid Id", "") + return NewLocAppError("Team.IsValid", "model.team.is_valid.id.app_error", nil, "") } if o.CreateAt == 0 { - return NewAppError("Team.IsValid", "Create at must be a valid time", "id="+o.Id) + return NewLocAppError("Team.IsValid", "model.team.is_valid.create_at.app_error", nil, "id="+o.Id) } if o.UpdateAt == 0 { - return NewAppError("Team.IsValid", "Update at must be a valid time", "id="+o.Id) + return NewLocAppError("Team.IsValid", "model.team.is_valid.update_at.app_error", nil, "id="+o.Id) } if len(o.Email) > 128 { - return NewAppError("Team.IsValid", "Invalid email", "id="+o.Id) + return NewLocAppError("Team.IsValid", "model.team.is_valid.email.app_error", nil, "id="+o.Id) } if len(o.Email) > 0 && !IsValidEmail(o.Email) { - return NewAppError("Team.IsValid", "Invalid email", "id="+o.Id) + return NewLocAppError("Team.IsValid", "model.team.is_valid.email.app_error", nil, "id="+o.Id) } if utf8.RuneCountInString(o.DisplayName) == 0 || utf8.RuneCountInString(o.DisplayName) > 64 { - return NewAppError("Team.IsValid", "Invalid name", "id="+o.Id) + return NewLocAppError("Team.IsValid", "model.team.is_valid.name.app_error", nil, "id="+o.Id) } if len(o.Name) > 64 { - return NewAppError("Team.IsValid", "Invalid URL Identifier", "id="+o.Id) + return NewLocAppError("Team.IsValid", "model.team.is_valid.url.app_error", nil, "id="+o.Id) } if restrictTeamNames && IsReservedTeamName(o.Name) { - return NewAppError("Team.IsValid", "This URL is unavailable. Please try another.", "id="+o.Id) + return NewLocAppError("Team.IsValid", "model.team.is_valid.reserved.app_error", nil, "id="+o.Id) } if !IsValidTeamName(o.Name) { - return NewAppError("Team.IsValid", "Name must be 4 or more lowercase alphanumeric characters", "id="+o.Id) + return NewLocAppError("Team.IsValid", "model.team.is_valid.characters.app_error", nil, "id="+o.Id) } if !(o.Type == TEAM_OPEN || o.Type == TEAM_INVITE) { - return NewAppError("Team.IsValid", "Invalid type", "id="+o.Id) + return NewLocAppError("Team.IsValid", "model.team.is_valid.type.app_error", nil, "id="+o.Id) } if len(o.CompanyName) > 64 { - return NewAppError("Team.IsValid", "Invalid company name", "id="+o.Id) + return NewLocAppError("Team.IsValid", "model.team.is_valid.company.app_error", nil, "id="+o.Id) } if len(o.AllowedDomains) > 500 { - return NewAppError("Team.IsValid", "Invalid allowed domains", "id="+o.Id) + return NewLocAppError("Team.IsValid", "model.team.is_valid.domains.app_error", nil, "id="+o.Id) } return nil diff --git a/model/user.go b/model/user.go index 7744b0073..675a1ded6 100644 --- a/model/user.go +++ b/model/user.go @@ -61,59 +61,59 @@ type User struct { func (u *User) IsValid() *AppError { if len(u.Id) != 26 { - return NewAppError("User.IsValid", "Invalid user id", "") + return NewLocAppError("User.IsValid", "model.user.is_valid.id.app_error", nil, "") } if u.CreateAt == 0 { - return NewAppError("User.IsValid", "Create at must be a valid time", "user_id="+u.Id) + return NewLocAppError("User.IsValid", "model.user.is_valid.create_at.app_error", nil, "user_id="+u.Id) } if u.UpdateAt == 0 { - return NewAppError("User.IsValid", "Update at must be a valid time", "user_id="+u.Id) + return NewLocAppError("User.IsValid", "model.user.is_valid.update_at.app_error", nil, "user_id="+u.Id) } if len(u.TeamId) != 26 { - return NewAppError("User.IsValid", "Invalid team id", "") + return NewLocAppError("User.IsValid", "model.user.is_valid.team_id.app_error", nil, "") } if !IsValidUsername(u.Username) { - return NewAppError("User.IsValid", "Invalid username", "user_id="+u.Id) + return NewLocAppError("User.IsValid", "model.user.is_valid.username.app_error", nil, "user_id="+u.Id) } if len(u.Email) > 128 || len(u.Email) == 0 { - return NewAppError("User.IsValid", "Invalid email", "user_id="+u.Id) + return NewLocAppError("User.IsValid", "model.user.is_valid.email.app_error", nil, "user_id="+u.Id) } if utf8.RuneCountInString(u.Nickname) > 64 { - return NewAppError("User.IsValid", "Invalid nickname", "user_id="+u.Id) + return NewLocAppError("User.IsValid", "model.user.is_valid.nickname.app_error", nil, "user_id="+u.Id) } if utf8.RuneCountInString(u.FirstName) > 64 { - return NewAppError("User.IsValid", "Invalid first name", "user_id="+u.Id) + return NewLocAppError("User.IsValid", "model.user.is_valid.first_name.app_error", nil, "user_id="+u.Id) } if utf8.RuneCountInString(u.LastName) > 64 { - return NewAppError("User.IsValid", "Invalid last name", "user_id="+u.Id) + return NewLocAppError("User.IsValid", "model.user.is_valid.last_name.app_error", nil, "user_id="+u.Id) } if len(u.Password) > 128 { - return NewAppError("User.IsValid", "Invalid password", "user_id="+u.Id) + return NewLocAppError("User.IsValid", "model.user.is_valid.pwd.app_error", nil, "user_id="+u.Id) } if len(u.AuthData) > 128 { - return NewAppError("User.IsValid", "Invalid auth data", "user_id="+u.Id) + return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data.app_error", nil, "user_id="+u.Id) } if len(u.AuthData) > 0 && len(u.AuthService) == 0 { - return NewAppError("User.IsValid", "Invalid user, auth data must be set with auth type", "user_id="+u.Id) + return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_type.app_error", nil, "user_id="+u.Id) } if len(u.Password) > 0 && len(u.AuthData) > 0 { - return NewAppError("User.IsValid", "Invalid user, password and auth data cannot both be set", "user_id="+u.Id) + return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_pwd.app_error", nil, "user_id="+u.Id) } if len(u.ThemeProps) > 2000 { - return NewAppError("User.IsValid", "Invalid theme", "user_id="+u.Id) + return NewLocAppError("User.IsValid", "model.user.is_valid.theme.app_error", nil, "user_id="+u.Id) } return nil @@ -236,7 +236,6 @@ func (u *User) Sanitize(options map[string]bool) { } func (u *User) ClearNonProfileFields() { - u.CreateAt = 0 u.UpdateAt = 0 u.Password = "" u.AuthData = "" diff --git a/model/utils.go b/model/utils.go index 042b5f195..70b7e3bbd 100644 --- a/model/utils.go +++ b/model/utils.go @@ -67,11 +67,7 @@ func AppErrorFromJson(data io.Reader) *AppError { if err == nil { return &er } else { - buf := new(bytes.Buffer) - buf.ReadFrom(data) - s := buf.String() - - return NewAppError("AppErrorFromJson", "could not decode", err.Error()+" "+s) + return NewLocAppError("AppErrorFromJson", "model.utils.decode_json.app_error", nil, err.Error()) } } diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go index 4585647de..7400df8d2 100644 --- a/store/sql_channel_store.go +++ b/store/sql_channel_store.go @@ -582,7 +582,16 @@ func (s SqlChannelStore) GetMemberCount(channelId string) StoreChannel { go func() { result := StoreResult{} - count, err := s.GetReplica().SelectInt("SELECT count(*) FROM ChannelMembers WHERE ChannelId = :ChannelId", map[string]interface{}{"ChannelId": channelId}) + count, err := s.GetReplica().SelectInt(` + SELECT + count(*) + FROM + ChannelMembers, + Users + WHERE + ChannelMembers.UserId = Users.Id + AND ChannelMembers.ChannelId = :ChannelId + AND Users.DeleteAt = 0`, map[string]interface{}{"ChannelId": channelId}) if err != nil { result.Err = model.NewAppError("SqlChannelStore.GetMemberCount", "We couldn't get the channel member count", "channel_id="+channelId+", "+err.Error()) } else { @@ -869,15 +878,13 @@ func (s SqlChannelStore) AnalyticsTypeCount(teamId string, channelType string) S go func() { result := StoreResult{} - v, err := s.GetReplica().SelectInt( - `SELECT - COUNT(Id) AS Value - FROM - Channels - WHERE - TeamId = :TeamId - AND Type = :ChannelType`, - map[string]interface{}{"TeamId": teamId, "ChannelType": channelType}) + query := "SELECT COUNT(Id) AS Value FROM Channels WHERE Type = :ChannelType" + + if len(teamId) > 0 { + query += " AND TeamId = :TeamId" + } + + v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId, "ChannelType": channelType}) if err != nil { result.Err = model.NewAppError("SqlChannelStore.AnalyticsTypeCount", "We couldn't get channel type counts", err.Error()) } else { diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go index 8b22fbb7a..a3b0c2286 100644 --- a/store/sql_channel_store_test.go +++ b/store/sql_channel_store_test.go @@ -750,3 +750,109 @@ func TestChannelStoreIncrementMentionCount(t *testing.T) { t.Fatal("failed to update") } } + +func TestGetMemberCount(t *testing.T) { + Setup() + + teamId := model.NewId() + + c1 := model.Channel{ + TeamId: teamId, + DisplayName: "Channel1", + Name: "a" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } + Must(store.Channel().Save(&c1)) + + c2 := model.Channel{ + TeamId: teamId, + DisplayName: "Channel2", + Name: "a" + model.NewId() + "b", + Type: model.CHANNEL_OPEN, + } + Must(store.Channel().Save(&c2)) + + t.Logf("c1.Id = %v", c1.Id) + + u1 := model.User{ + TeamId: teamId, + Email: model.NewId(), + DeleteAt: 0, + } + Must(store.User().Save(&u1)) + + m1 := model.ChannelMember{ + ChannelId: c1.Id, + UserId: u1.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + Must(store.Channel().SaveMember(&m1)) + + if result := <-store.Channel().GetMemberCount(c1.Id); result.Err != nil { + t.Fatal("failed to get member count: %v", result.Err) + } else if result.Data.(int64) != 1 { + t.Fatal("got incorrect member count %v", result.Data) + } + + u2 := model.User{ + TeamId: teamId, + Email: model.NewId(), + DeleteAt: 0, + } + Must(store.User().Save(&u2)) + + m2 := model.ChannelMember{ + ChannelId: c1.Id, + UserId: u2.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + Must(store.Channel().SaveMember(&m2)) + + if result := <-store.Channel().GetMemberCount(c1.Id); result.Err != nil { + t.Fatal("failed to get member count: %v", result.Err) + } else if result.Data.(int64) != 2 { + t.Fatal("got incorrect member count %v", result.Data) + } + + // make sure members of other channels aren't counted + u3 := model.User{ + TeamId: teamId, + Email: model.NewId(), + DeleteAt: 0, + } + Must(store.User().Save(&u3)) + + m3 := model.ChannelMember{ + ChannelId: c2.Id, + UserId: u3.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + Must(store.Channel().SaveMember(&m3)) + + if result := <-store.Channel().GetMemberCount(c1.Id); result.Err != nil { + t.Fatal("failed to get member count: %v", result.Err) + } else if result.Data.(int64) != 2 { + t.Fatal("got incorrect member count %v", result.Data) + } + + // make sure inactive users aren't counted + u4 := model.User{ + TeamId: teamId, + Email: model.NewId(), + DeleteAt: 10000, + } + Must(store.User().Save(&u4)) + + m4 := model.ChannelMember{ + ChannelId: c1.Id, + UserId: u4.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + } + Must(store.Channel().SaveMember(&m4)) + + if result := <-store.Channel().GetMemberCount(c1.Id); result.Err != nil { + t.Fatal("failed to get member count: %v", result.Err) + } else if result.Data.(int64) != 2 { + t.Fatal("got incorrect member count %v", result.Data) + } +} diff --git a/store/sql_post_store.go b/store/sql_post_store.go index 40dca9930..e332858e4 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -805,9 +805,13 @@ func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChan FROM Posts, Channels WHERE - Posts.ChannelId = Channels.Id - AND Channels.TeamId = :TeamId - AND Posts.CreateAt <= :EndTime + Posts.ChannelId = Channels.Id` + + if len(teamId) > 0 { + query += " AND Channels.TeamId = :TeamId" + } + + query += ` AND Posts.CreateAt <= :EndTime ORDER BY Name DESC) AS t1 GROUP BY Name ORDER BY Name DESC @@ -824,9 +828,13 @@ func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChan FROM Posts, Channels WHERE - Posts.ChannelId = Channels.Id - AND Channels.TeamId = :TeamId - AND Posts.CreateAt <= :EndTime + Posts.ChannelId = Channels.Id` + + if len(teamId) > 0 { + query += " AND Channels.TeamId = :TeamId" + } + + query += ` AND Posts.CreateAt <= :EndTime ORDER BY Name DESC) AS t1 GROUP BY Name ORDER BY Name DESC @@ -869,9 +877,13 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel { FROM Posts, Channels WHERE - Posts.ChannelId = Channels.Id - AND Channels.TeamId = :TeamId - AND Posts.CreateAt <= :EndTime + Posts.ChannelId = Channels.Id` + + if len(teamId) > 0 { + query += " AND Channels.TeamId = :TeamId" + } + + query += ` AND Posts.CreateAt <= :EndTime AND Posts.CreateAt >= :StartTime) AS t1 GROUP BY Name ORDER BY Name DESC @@ -888,9 +900,13 @@ func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel { FROM Posts, Channels WHERE - Posts.ChannelId = Channels.Id - AND Channels.TeamId = :TeamId - AND Posts.CreateAt <= :EndTime + Posts.ChannelId = Channels.Id` + + if len(teamId) > 0 { + query += " AND Channels.TeamId = :TeamId" + } + + query += ` AND Posts.CreateAt <= :EndTime AND Posts.CreateAt >= :StartTime) AS t1 GROUP BY Name ORDER BY Name DESC @@ -924,16 +940,20 @@ func (s SqlPostStore) AnalyticsPostCount(teamId string) StoreChannel { go func() { result := StoreResult{} - v, err := s.GetReplica().SelectInt( + query := `SELECT COUNT(Posts.Id) AS Value FROM Posts, Channels WHERE - Posts.ChannelId = Channels.Id - AND Channels.TeamId = :TeamId`, - map[string]interface{}{"TeamId": teamId}) + Posts.ChannelId = Channels.Id` + + if len(teamId) > 0 { + query += " AND Channels.TeamId = :TeamId" + } + + v, err := s.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}) if err != nil { result.Err = model.NewAppError("SqlPostStore.AnalyticsPostCount", "We couldn't get post counts", err.Error()) } else { diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 0f73f73c3..efd8b7f33 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -600,3 +600,30 @@ func (us SqlUserStore) PermanentDelete(userId string) StoreChannel { return storeChannel } + +func (us SqlUserStore) AnalyticsUniqueUserCount(teamId string) StoreChannel { + + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + query := "SELECT COUNT(DISTINCT Email) FROM Users" + + if len(teamId) > 0 { + query += " WHERE TeamId = :TeamId" + } + + v, err := us.GetReplica().SelectInt(query, map[string]interface{}{"TeamId": teamId}) + if err != nil { + result.Err = model.NewAppError("SqlUserStore.AnalyticsUniqueUserCount", "We couldn't get the unique user count", err.Error()) + } else { + result.Data = v + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} diff --git a/store/store.go b/store/store.go index 3a865d52a..8a362bc8f 100644 --- a/store/store.go +++ b/store/store.go @@ -126,6 +126,7 @@ type UserStore interface { GetTotalActiveUsersCount() StoreChannel GetSystemAdminProfiles() StoreChannel PermanentDelete(userId string) StoreChannel + AnalyticsUniqueUserCount(teamId string) StoreChannel } type SessionStore interface { diff --git a/utils/config.go b/utils/config.go index 1180ed6d4..a2d341cd2 100644 --- a/utils/config.go +++ b/utils/config.go @@ -118,12 +118,14 @@ func GetLogFileLocation(fileLocation string) string { func SaveConfig(fileName string, config *model.Config) *model.AppError { b, err := json.MarshalIndent(config, "", " ") if err != nil { - return model.NewAppError("SaveConfig", "An error occurred while saving the file to "+fileName, err.Error()) + return model.NewLocAppError("SaveConfig", "utils.config.save_config.saving.app_error", + map[string]interface{}{"Filename": fileName}, err.Error()) } err = ioutil.WriteFile(fileName, b, 0644) if err != nil { - return model.NewAppError("SaveConfig", "An error occurred while saving the file to "+fileName, err.Error()) + return model.NewLocAppError("SaveConfig", "utils.config.save_config.saving.app_error", + map[string]interface{}{"Filename": fileName}, err.Error()) } return nil @@ -138,18 +140,21 @@ func LoadConfig(fileName string) { file, err := os.Open(fileName) if err != nil { - panic("Error opening config file=" + fileName + ", err=" + err.Error()) + panic(T("utils.config.load_config.opening.panic", + map[string]interface{}{"Filename": fileName, "Error": err.Error()})) } decoder := json.NewDecoder(file) config := model.Config{} err = decoder.Decode(&config) if err != nil { - panic("Error decoding config file=" + fileName + ", err=" + err.Error()) + panic(T("utils.config.load_config.decoding.panic", + map[string]interface{}{"Filename": fileName, "Error": err.Error()})) } if info, err := file.Stat(); err != nil { - panic("Error getting config info file=" + fileName + ", err=" + err.Error()) + panic(T("utils.config.load_config.getting.panic", + map[string]interface{}{"Filename": fileName, "Error": err.Error()})) } else { CfgLastModified = info.ModTime().Unix() CfgFileName = fileName @@ -158,7 +163,8 @@ func LoadConfig(fileName string) { config.SetDefaults() if err := config.IsValid(); err != nil { - panic("Error validating config file=" + fileName + ", err=" + err.Message) + panic(T("utils.config.load_config.validating.panic", + map[string]interface{}{"Filename": fileName, "Error": err.Message})) } configureLog(&config.LogSettings) diff --git a/utils/config_test.go b/utils/config_test.go index 0b334d36c..6f36b30c3 100644 --- a/utils/config_test.go +++ b/utils/config_test.go @@ -9,4 +9,5 @@ import ( func TestConfig(t *testing.T) { LoadConfig("config.json") + InitTranslations() } diff --git a/utils/license.go b/utils/license.go index 7594e33af..0d1cd597c 100644 --- a/utils/license.go +++ b/utils/license.go @@ -44,7 +44,7 @@ NxpC+5KFhU+xSeeklNqwCgnlOyZ7qSTxmdJHb+60SwuYnnGIYzLJhY4LYDr4J+KR func LoadLicense() { file, err := os.Open(LicenseLocation()) if err != nil { - l4g.Warn("Unable to open/find license file") + l4g.Warn(T("utils.license.load_license.open_find.warn")) return } defer file.Close() @@ -55,9 +55,10 @@ func LoadLicense() { if success, licenseStr := ValidateLicense(buf.Bytes()); success { license := model.LicenseFromJson(strings.NewReader(licenseStr)) SetLicense(license) + return } - l4g.Warn("No valid enterprise license found") + l4g.Warn(T("utils.license.load_license.invalid.warn")) } func SetLicense(license *model.License) bool { @@ -83,7 +84,7 @@ func RemoveLicense() bool { ClientLicense = getClientLicense(License) if err := os.Remove(LicenseLocation()); err != nil { - l4g.Error("Unable to remove license file, err=%v", err.Error()) + l4g.Error(T("utils.license.remove_license.unable.error"), err.Error()) return false } @@ -95,17 +96,17 @@ func ValidateLicense(signed []byte) (bool, string) { _, err := base64.StdEncoding.Decode(decoded, signed) if err != nil { - l4g.Error("Encountered error decoding license, err=%v", err.Error()) + l4g.Error(T("utils.license.validate_license.decode.error"), err.Error()) return false, "" } if len(decoded) <= 256 { - l4g.Error("Signed license not long enough") + l4g.Error(T("utils.license.validate_license.not_long.error")) return false, "" } // remove null terminator - if decoded[len(decoded)-1] == byte(0) { + for decoded[len(decoded)-1] == byte(0) { decoded = decoded[:len(decoded)-1] } @@ -116,7 +117,7 @@ func ValidateLicense(signed []byte) (bool, string) { public, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { - l4g.Error("Encountered error signing license, err=%v", err.Error()) + l4g.Error(T("utils.license.validate_license.signing.error"), err.Error()) return false, "" } @@ -128,7 +129,7 @@ func ValidateLicense(signed []byte) (bool, string) { err = rsa.VerifyPKCS1v15(rsaPublic, crypto.SHA512, d, signature) if err != nil { - l4g.Error("Invalid signature, err=%v", err.Error()) + l4g.Error(T("utils.license.validate_license.invalid.error"), err.Error()) return false, "" } diff --git a/utils/lru.go b/utils/lru.go index 61a515e14..f5f7959d8 100644 --- a/utils/lru.go +++ b/utils/lru.go @@ -38,7 +38,7 @@ func NewLru(size int) *Cache { func NewLruWithEvict(size int, onEvicted func(key interface{}, value interface{})) (*Cache, error) { if size <= 0 { - return nil, errors.New("Must provide a positive size") + return nil, errors.New(T("utils.iru.with_evict")) } c := &Cache{ size: size, diff --git a/utils/mail.go b/utils/mail.go index 2f2c10b61..4a0b987e6 100644 --- a/utils/mail.go +++ b/utils/mail.go @@ -34,12 +34,12 @@ func connectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) { conn, err = tls.Dial("tcp", config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort, tlsconfig) if err != nil { - return nil, model.NewAppError("SendMail", "Failed to open TLS connection", err.Error()) + return nil, model.NewLocAppError("SendMail", "utils.mail.connect_smtp.open_tls.app_error", nil, err.Error()) } } else { conn, err = net.Dial("tcp", config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort) if err != nil { - return nil, model.NewAppError("SendMail", "Failed to open connection", err.Error()) + return nil, model.NewLocAppError("SendMail", "utils.mail.connect_smtp.open.app_error", nil, err.Error()) } } @@ -49,15 +49,15 @@ func connectToSMTPServer(config *model.Config) (net.Conn, *model.AppError) { func newSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.AppError) { c, err := smtp.NewClient(conn, config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort) if err != nil { - l4g.Error("Failed to open a connection to SMTP server %v", err) - return nil, model.NewAppError("SendMail", "Failed to open TLS connection", err.Error()) + l4g.Error(T("utils.mail.new_client.open.error"), err) + return nil, model.NewLocAppError("SendMail", "utils.mail.connect_smtp.open_tls.app_error", nil, err.Error()) } // GO does not support plain auth over a non encrypted connection. // so if not tls then no auth auth := smtp.PlainAuth("", config.EmailSettings.SMTPUsername, config.EmailSettings.SMTPPassword, config.EmailSettings.SMTPServer+":"+config.EmailSettings.SMTPPort) if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_TLS { if err = c.Auth(auth); err != nil { - return nil, model.NewAppError("SendMail", "Failed to authenticate on SMTP server", err.Error()) + return nil, model.NewLocAppError("SendMail", "utils.mail.new_client.auth.app_error", nil, err.Error()) } } else if config.EmailSettings.ConnectionSecurity == model.CONN_SECURITY_STARTTLS { tlsconfig := &tls.Config{ @@ -66,7 +66,7 @@ func newSMTPClient(conn net.Conn, config *model.Config) (*smtp.Client, *model.Ap } c.StartTLS(tlsconfig) if err = c.Auth(auth); err != nil { - return nil, model.NewAppError("SendMail", "Failed to authenticate on SMTP server", err.Error()) + return nil, model.NewLocAppError("SendMail", "utils.mail.new_client.auth.app_error", nil, err.Error()) } } return c, nil @@ -79,14 +79,14 @@ func TestConnection(config *model.Config) { conn, err1 := connectToSMTPServer(config) if err1 != nil { - l4g.Error("SMTP server settings do not appear to be configured properly err=%v details=%v", err1.Message, err1.DetailedError) + l4g.Error(T("utils.mail.test.configured.error"), err1.Message, err1.DetailedError) return } defer conn.Close() c, err2 := newSMTPClient(conn, config) if err2 != nil { - l4g.Error("SMTP connection settings do not appear to be configured properly err=%v details=%v", err2.Message, err2.DetailedError) + l4g.Error(T("utils.mail.test.configured.error"), err2.Message, err2.DetailedError) return } defer c.Quit() @@ -102,7 +102,7 @@ func SendMailUsingConfig(to, subject, body string, config *model.Config) *model. return nil } - l4g.Debug("sending mail to " + to + " with subject of '" + subject + "'") + l4g.Debug(T("utils.mail.send_mail.sending.debug"), to, subject) fromMail := mail.Address{config.EmailSettings.FeedbackName, config.EmailSettings.FeedbackEmail} toMail := mail.Address{"", to} @@ -136,26 +136,26 @@ func SendMailUsingConfig(to, subject, body string, config *model.Config) *model. defer c.Close() if err := c.Mail(fromMail.Address); err != nil { - return model.NewAppError("SendMail", "Failed to add from email address", err.Error()) + return model.NewLocAppError("SendMail", "utils.mail.send_mail.from_address.app_error", nil, err.Error()) } if err := c.Rcpt(toMail.Address); err != nil { - return model.NewAppError("SendMail", "Failed to add to email address", err.Error()) + return model.NewLocAppError("SendMail", "utils.mail.send_mail.to_address.app_error", nil, err.Error()) } w, err := c.Data() if err != nil { - return model.NewAppError("SendMail", "Failed to add email messsage data", err.Error()) + return model.NewLocAppError("SendMail", "utils.mail.send_mail.msg_data.app_error", nil, err.Error()) } _, err = w.Write([]byte(message)) if err != nil { - return model.NewAppError("SendMail", "Failed to write email message", err.Error()) + return model.NewLocAppError("SendMail", "utils.mail.send_mail.msg.app_error", nil, err.Error()) } err = w.Close() if err != nil { - return model.NewAppError("SendMail", "Failed to close connection to SMTP server", err.Error()) + return model.NewLocAppError("SendMail", "utils.mail.send_mail.close.app_error", nil, err.Error()) } return nil diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index f5341c0bc..6a880f0ee 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -100,8 +100,12 @@ export default class ActivityLogModal extends React.Component { if (currentSession.props.platform === 'Windows') { devicePicture = 'fa fa-windows'; - } else if (currentSession.props.platform === 'Macintosh' || currentSession.props.platform === 'iPhone') { + } else if (currentSession.props.platform === 'Macintosh' || + currentSession.props.platform === 'iPhone') { devicePicture = 'fa fa-apple'; + } else if (currentSession.props.platform.browser.indexOf('Mattermost/') === 0) { + devicePicture = 'fa fa-apple'; + devicePlatform = 'iPhone'; } else if (currentSession.props.platform === 'Linux') { if (currentSession.props.os.indexOf('Android') >= 0) { devicePlatform = 'Android'; diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx index 0f85c238d..efd163017 100644 --- a/web/react/components/admin_console/admin_controller.jsx +++ b/web/react/components/admin_console/admin_controller.jsx @@ -1,4 +1,4 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. import AdminSidebar from './admin_sidebar.jsx'; @@ -23,6 +23,7 @@ import TeamUsersTab from './team_users.jsx'; import TeamAnalyticsTab from './team_analytics.jsx'; import LdapSettingsTab from './ldap_settings.jsx'; import LicenseSettingsTab from './license_settings.jsx'; +import SystemAnalyticsTab from './system_analytics.jsx'; export default class AdminController extends React.Component { constructor(props) { @@ -45,7 +46,7 @@ export default class AdminController extends React.Component { config: AdminStore.getConfig(), teams: AdminStore.getAllTeams(), selectedTeams, - selected: props.tab || 'service_settings', + selected: props.tab || 'system_analytics', selectedTeam: props.teamId || null }; @@ -165,6 +166,8 @@ export default class AdminController extends React.Component { if (this.state.teams) { tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]} />; } + } else if (this.state.selected === 'system_analytics') { + tab = <SystemAnalyticsTab />; } } diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx index 5a5eaa055..66f82c55b 100644 --- a/web/react/components/admin_console/admin_sidebar.jsx +++ b/web/react/components/admin_console/admin_sidebar.jsx @@ -196,6 +196,25 @@ export default class AdminSidebar extends React.Component { <li> <h4> <span className='icon fa fa-gear'></span> + <span>{'SITE REPORTS'}</span> + </h4> + </li> + </ul> + <ul className='nav nav__sub-menu padded'> + <li> + <a + href='#' + className={this.isSelected('system_analytics')} + onClick={this.handleClick.bind(this, 'system_analytics', null)} + > + {'View Statistics'} + </a> + </li> + </ul> + <ul className='nav nav__sub-menu'> + <li> + <h4> + <span className='icon fa fa-gear'></span> <span>{'SETTINGS'}</span> </h4> </li> diff --git a/web/react/components/admin_console/analytics.jsx b/web/react/components/admin_console/analytics.jsx new file mode 100644 index 000000000..70ef1ecab --- /dev/null +++ b/web/react/components/admin_console/analytics.jsx @@ -0,0 +1,279 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import * as Utils from '../../utils/utils.jsx'; +import Constants from '../../utils/constants.jsx'; +import LineChart from './line_chart.jsx'; + +var Tooltip = ReactBootstrap.Tooltip; +var OverlayTrigger = ReactBootstrap.OverlayTrigger; + +export default class Analytics extends React.Component { + constructor(props) { + super(props); + + this.state = {}; + } + + render() { // in the future, break down these into smaller components + var serverError = ''; + if (this.props.serverError) { + serverError = <div className='form-group has-error'><label className='control-label'>{this.props.serverError}</label></div>; + } + + var totalCount = ( + <div className='col-sm-3'> + <div className='total-count'> + <div className='title'>{'Total Users'}<i className='fa fa-users'/></div> + <div className='content'>{this.props.uniqueUserCount == null ? 'Loading...' : this.props.uniqueUserCount}</div> + </div> + </div> + ); + + var openChannelCount = ( + <div className='col-sm-3'> + <div className='total-count'> + <div className='title'>{'Public Channels'}<i className='fa fa-globe'/></div> + <div className='content'>{this.props.channelOpenCount == null ? 'Loading...' : this.props.channelOpenCount}</div> + </div> + </div> + ); + + var openPrivateCount = ( + <div className='col-sm-3'> + <div className='total-count'> + <div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div> + <div className='content'>{this.props.channelPrivateCount == null ? 'Loading...' : this.props.channelPrivateCount}</div> + </div> + </div> + ); + + var postCount = ( + <div className='col-sm-3'> + <div className='total-count'> + <div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div> + <div className='content'>{this.props.postCount == null ? 'Loading...' : this.props.postCount}</div> + </div> + </div> + ); + + var postCountsByDay = ( + <div className='col-sm-12'> + <div className='total-count by-day'> + <div className='title'>{'Total Posts'}</div> + <div className='content'>{'Loading...'}</div> + </div> + </div> + ); + + if (this.props.postCountsDay != null) { + let content; + if (this.props.postCountsDay.labels.length === 0) { + content = 'Not enough data for a meaningful representation.'; + } else { + content = ( + <LineChart + data={this.props.postCountsDay} + width='740' + height='225' + /> + ); + } + postCountsByDay = ( + <div className='col-sm-12'> + <div className='total-count by-day'> + <div className='title'>{'Total Posts'}</div> + <div className='content'> + {content} + </div> + </div> + </div> + ); + } + + var usersWithPostsByDay = ( + <div className='col-sm-12'> + <div className='total-count by-day'> + <div className='title'>{'Active Users With Posts'}</div> + <div className='content'>{'Loading...'}</div> + </div> + </div> + ); + + if (this.props.userCountsWithPostsDay != null) { + let content; + if (this.props.userCountsWithPostsDay.labels.length === 0) { + content = 'Not enough data for a meaningful representation.'; + } else { + content = ( + <LineChart + data={this.props.userCountsWithPostsDay} + width='740' + height='225' + /> + ); + } + usersWithPostsByDay = ( + <div className='col-sm-12'> + <div className='total-count by-day'> + <div className='title'>{'Active Users With Posts'}</div> + <div className='content'> + {content} + </div> + </div> + </div> + ); + } + + let recentActiveUser; + if (this.props.recentActiveUsers != null) { + let content; + if (this.props.recentActiveUsers.length === 0) { + content = 'Loading...'; + } else { + content = ( + <table> + <tbody> + { + this.props.recentActiveUsers.map((user) => { + const tooltip = ( + <Tooltip id={'recent-user-email-tooltip-' + user.id}> + {user.email} + </Tooltip> + ); + + return ( + <tr key={'recent-user-table-entry-' + user.id}> + <td> + <OverlayTrigger + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='top' + overlay={tooltip} + > + <time> + {user.username} + </time> + </OverlayTrigger> + </td> + <td>{Utils.displayDateTime(user.last_activity_at)}</td> + </tr> + ); + }) + } + </tbody> + </table> + ); + } + recentActiveUser = ( + <div className='col-sm-6'> + <div className='total-count recent-active-users'> + <div className='title'>{'Recent Active Users'}</div> + <div className='content'> + {content} + </div> + </div> + </div> + ); + } + + let newUsers; + if (this.props.newlyCreatedUsers != null) { + let content; + if (this.props.newlyCreatedUsers.length === 0) { + content = 'Loading...'; + } else { + content = ( + <table> + <tbody> + { + this.props.newlyCreatedUsers.map((user) => { + const tooltip = ( + <Tooltip id={'new-user-email-tooltip-' + user.id}> + {user.email} + </Tooltip> + ); + + return ( + <tr key={'new-user-table-entry-' + user.id}> + <td> + <OverlayTrigger + delayShow={Constants.OVERLAY_TIME_DELAY} + placement='top' + overlay={tooltip} + > + <time> + {user.username} + </time> + </OverlayTrigger> + </td> + <td>{Utils.displayDateTime(user.create_at)}</td> + </tr> + ); + }) + } + </tbody> + </table> + ); + } + newUsers = ( + <div className='col-sm-6'> + <div className='total-count recent-active-users'> + <div className='title'>{'Newly Created Users'}</div> + <div className='content'> + {content} + </div> + </div> + </div> + ); + } + + return ( + <div className='wrapper--fixed team_statistics'> + <h3>{'Statistics for ' + this.props.title}</h3> + {serverError} + <div className='row'> + {totalCount} + {postCount} + {openChannelCount} + {openPrivateCount} + </div> + <div className='row'> + {postCountsByDay} + </div> + <div className='row'> + {usersWithPostsByDay} + </div> + <div className='row'> + {recentActiveUser} + {newUsers} + </div> + </div> + ); + } +} + +Analytics.defaultProps = { + title: null, + channelOpenCount: null, + channelPrivateCount: null, + postCount: null, + postCountsDay: null, + userCountsWithPostsDay: null, + recentActiveUsers: null, + newlyCreatedUsers: null, + uniqueUserCount: null, + serverError: null +}; + +Analytics.propTypes = { + title: React.PropTypes.string, + channelOpenCount: React.PropTypes.number, + channelPrivateCount: React.PropTypes.number, + postCount: React.PropTypes.number, + postCountsDay: React.PropTypes.object, + userCountsWithPostsDay: React.PropTypes.object, + recentActiveUsers: React.PropTypes.array, + newlyCreatedUsers: React.PropTypes.array, + uniqueUserCount: React.PropTypes.number, + serverError: React.PropTypes.string +}; diff --git a/web/react/components/admin_console/system_analytics.jsx b/web/react/components/admin_console/system_analytics.jsx new file mode 100644 index 000000000..f54813a94 --- /dev/null +++ b/web/react/components/admin_console/system_analytics.jsx @@ -0,0 +1,161 @@ +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import Analytics from './analytics.jsx'; +import * as Client from '../../utils/client.jsx'; + +export default class SystemAnalytics extends React.Component { + constructor(props) { + super(props); + + this.getData = this.getData.bind(this); + + this.state = { // most of this state should be from a store in the future + users: null, + serverError: null, + channel_open_count: null, + channel_private_count: null, + post_count: null, + post_counts_day: null, + user_counts_with_posts_day: null, + recent_active_users: null, + newly_created_users: null, + unique_user_count: null + }; + } + + componentDidMount() { + this.getData(); + } + + getData() { // should be moved to an action creator eventually + Client.getSystemAnalytics( + 'standard', + (data) => { + for (var index in data) { + if (data[index].name === 'channel_open_count') { + this.setState({channel_open_count: data[index].value}); + } + + if (data[index].name === 'channel_private_count') { + this.setState({channel_private_count: data[index].value}); + } + + if (data[index].name === 'post_count') { + this.setState({post_count: data[index].value}); + } + + if (data[index].name === 'unique_user_count') { + this.setState({unique_user_count: data[index].value}); + } + } + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + + Client.getSystemAnalytics( + 'post_counts_day', + (data) => { + data.reverse(); + + var chartData = { + labels: [], + datasets: [{ + label: 'Total Posts', + fillColor: 'rgba(151,187,205,0.2)', + strokeColor: 'rgba(151,187,205,1)', + pointColor: 'rgba(151,187,205,1)', + pointStrokeColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStroke: 'rgba(151,187,205,1)', + data: [] + }] + }; + + for (var index in data) { + if (data[index]) { + var row = data[index]; + chartData.labels.push(row.name); + chartData.datasets[0].data.push(row.value); + } + } + + this.setState({post_counts_day: chartData}); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + + Client.getSystemAnalytics( + 'user_counts_with_posts_day', + (data) => { + data.reverse(); + + var chartData = { + labels: [], + datasets: [{ + label: 'Active Users With Posts', + fillColor: 'rgba(151,187,205,0.2)', + strokeColor: 'rgba(151,187,205,1)', + pointColor: 'rgba(151,187,205,1)', + pointStrokeColor: '#fff', + pointHighlightFill: '#fff', + pointHighlightStroke: 'rgba(151,187,205,1)', + data: [] + }] + }; + + for (var index in data) { + if (data[index]) { + var row = data[index]; + chartData.labels.push(row.name); + chartData.datasets[0].data.push(row.value); + } + } + + this.setState({user_counts_with_posts_day: chartData}); + }, + (err) => { + this.setState({serverError: err.message}); + } + ); + } + + componentWillReceiveProps() { + this.setState({ + serverError: null, + channel_open_count: null, + channel_private_count: null, + post_count: null, + post_counts_day: null, + user_counts_with_posts_day: null, + unique_user_count: null + }); + + this.getData(); + } + + render() { + return ( + <div> + <Analytics + title={'the System'} + channelOpenCount={this.state.channel_open_count} + channelPrivateCount={this.state.channel_private_count} + postCount={this.state.post_count} + postCountsDay={this.state.post_counts_day} + userCountsWithPostsDay={this.state.user_counts_with_posts_day} + uniqueUserCount={this.state.unique_user_count} + serverError={this.state.serverError} + /> + </div> + ); + } +} + +SystemAnalytics.propTypes = { + team: React.PropTypes.object +}; diff --git a/web/react/components/admin_console/team_analytics.jsx b/web/react/components/admin_console/team_analytics.jsx index fe7230946..c164dd98c 100644 --- a/web/react/components/admin_console/team_analytics.jsx +++ b/web/react/components/admin_console/team_analytics.jsx @@ -1,13 +1,8 @@ -// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import Analytics from './analytics.jsx'; import * as Client from '../../utils/client.jsx'; -import * as Utils from '../../utils/utils.jsx'; -import Constants from '../../utils/constants.jsx'; -import LineChart from './line_chart.jsx'; - -var Tooltip = ReactBootstrap.Tooltip; -var OverlayTrigger = ReactBootstrap.OverlayTrigger; export default class TeamAnalytics extends React.Component { constructor(props) { @@ -15,7 +10,7 @@ export default class TeamAnalytics extends React.Component { this.getData = this.getData.bind(this); - this.state = { + this.state = { // most of this state should be from a store in the future users: null, serverError: null, channel_open_count: null, @@ -24,7 +19,8 @@ export default class TeamAnalytics extends React.Component { post_counts_day: null, user_counts_with_posts_day: null, recent_active_users: null, - newly_created_users: null + newly_created_users: null, + unique_user_count: null }; } @@ -32,8 +28,8 @@ export default class TeamAnalytics extends React.Component { this.getData(this.props.team.id); } - getData(teamId) { - Client.getAnalytics( + getData(teamId) { // should be moved to an action creator eventually + Client.getTeamAnalytics( teamId, 'standard', (data) => { @@ -49,6 +45,10 @@ export default class TeamAnalytics extends React.Component { if (data[index].name === 'post_count') { this.setState({post_count: data[index].value}); } + + if (data[index].name === 'unique_user_count') { + this.setState({unique_user_count: data[index].value}); + } } }, (err) => { @@ -56,7 +56,7 @@ export default class TeamAnalytics extends React.Component { } ); - Client.getAnalytics( + Client.getTeamAnalytics( teamId, 'post_counts_day', (data) => { @@ -91,7 +91,7 @@ export default class TeamAnalytics extends React.Component { } ); - Client.getAnalytics( + Client.getTeamAnalytics( teamId, 'user_counts_with_posts_day', (data) => { @@ -152,6 +152,10 @@ export default class TeamAnalytics extends React.Component { var recentActive = []; for (let i = 0; i < usersList.length; i++) { + if (usersList[i].last_activity_at == null) { + continue; + } + recentActive.push(usersList[i]); if (i > 19) { break; @@ -198,227 +202,29 @@ export default class TeamAnalytics extends React.Component { post_counts_day: null, user_counts_with_posts_day: null, recent_active_users: null, - newly_created_users: null + newly_created_users: null, + unique_user_count: null }); this.getData(newProps.team.id); } - componentWillUnmount() { - } - render() { - var serverError = ''; - if (this.state.serverError) { - serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>; - } - - var totalCount = ( - <div className='col-sm-3'> - <div className='total-count'> - <div className='title'>{'Total Users'}<i className='fa fa-users'/></div> - <div className='content'>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div> - </div> - </div> - ); - - var openChannelCount = ( - <div className='col-sm-3'> - <div className='total-count'> - <div className='title'>{'Public Channels'}<i className='fa fa-globe'/></div> - <div className='content'>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div> - </div> - </div> - ); - - var openPrivateCount = ( - <div className='col-sm-3'> - <div className='total-count'> - <div className='title'>{'Private Groups'}<i className='fa fa-lock'/></div> - <div className='content'>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div> - </div> - </div> - ); - - var postCount = ( - <div className='col-sm-3'> - <div className='total-count'> - <div className='title'>{'Total Posts'}<i className='fa fa-comment'/></div> - <div className='content'>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div> - </div> - </div> - ); - - var postCountsByDay = ( - <div className='col-sm-12'> - <div className='total-count by-day'> - <div className='title'>{'Total Posts'}</div> - <div className='content'>{'Loading...'}</div> - </div> - </div> - ); - - if (this.state.post_counts_day != null) { - postCountsByDay = ( - <div className='col-sm-12'> - <div className='total-count by-day'> - <div className='title'>{'Total Posts'}</div> - <div className='content'> - <LineChart - data={this.state.post_counts_day} - width='740' - height='225' - /> - </div> - </div> - </div> - ); - } - - var usersWithPostsByDay = ( - <div className='col-sm-12'> - <div className='total-count by-day'> - <div className='title'>{'Total Posts'}</div> - <div>{'Loading...'}</div> - </div> - </div> - ); - - if (this.state.user_counts_with_posts_day != null) { - usersWithPostsByDay = ( - <div className='col-sm-12'> - <div className='total-count by-day'> - <div className='title'>{'Active Users With Posts'}</div> - <div className='content'> - <LineChart - data={this.state.user_counts_with_posts_day} - width='740' - height='225' - /> - </div> - </div> - </div> - ); - } - - var recentActiveUser = ( - <div className='recent-active-users'> - <div>{'Recent Active Users'}</div> - <div>{'Loading...'}</div> - </div> - ); - - if (this.state.recent_active_users != null) { - recentActiveUser = ( - <div className='col-sm-6'> - <div className='total-count recent-active-users'> - <div className='title'>{'Recent Active Users'}</div> - <div className='content'> - <table> - <tbody> - { - this.state.recent_active_users.map((user) => { - const tooltip = ( - <Tooltip id={'recent-user-email-tooltip-' + user.id}> - {user.email} - </Tooltip> - ); - - return ( - <tr key={'recent-user-table-entry-' + user.id}> - <td> - <OverlayTrigger - delayShow={Constants.OVERLAY_TIME_DELAY} - placement='top' - overlay={tooltip} - > - <time> - {user.username} - </time> - </OverlayTrigger> - </td> - <td>{Utils.displayDateTime(user.last_activity_at)}</td> - </tr> - ); - }) - } - </tbody> - </table> - </div> - </div> - </div> - ); - } - - var newUsers = ( - <div className='recent-active-users'> - <div>{'Newly Created Users'}</div> - <div>{'Loading...'}</div> - </div> - ); - - if (this.state.newly_created_users != null) { - newUsers = ( - <div className='col-sm-6'> - <div className='total-count recent-active-users'> - <div className='title'>{'Newly Created Users'}</div> - <div className='content'> - <table> - <tbody> - { - this.state.newly_created_users.map((user) => { - const tooltip = ( - <Tooltip id={'new-user-email-tooltip-' + user.id}> - {user.email} - </Tooltip> - ); - - return ( - <tr key={'new-user-table-entry-' + user.id}> - <td> - <OverlayTrigger - delayShow={Constants.OVERLAY_TIME_DELAY} - placement='top' - overlay={tooltip} - > - <time> - {user.username} - </time> - </OverlayTrigger> - </td> - <td>{Utils.displayDateTime(user.create_at)}</td> - </tr> - ); - }) - } - </tbody> - </table> - </div> - </div> - </div> - ); - } - return ( - <div className='wrapper--fixed team_statistics'> - <h3>{'Statistics for ' + this.props.team.name}</h3> - {serverError} - <div className='row'> - {totalCount} - {postCount} - {openChannelCount} - {openPrivateCount} - </div> - <div className='row'> - {postCountsByDay} - </div> - <div className='row'> - {usersWithPostsByDay} - </div> - <div className='row'> - {recentActiveUser} - {newUsers} - </div> + <div> + <Analytics + title={this.props.team.name} + users={this.state.users} + channelOpenCount={this.state.channel_open_count} + channelPrivateCount={this.state.channel_private_count} + postCount={this.state.post_count} + postCountsDay={this.state.post_counts_day} + userCountsWithPostsDay={this.state.user_counts_with_posts_day} + recentActiveUsers={this.state.recent_active_users} + newlyCreatedUsers={this.state.newly_created_users} + uniqueUserCount={this.state.unique_user_count} + serverError={this.state.serverError} + /> </div> ); } diff --git a/web/react/components/delete_post_modal.jsx b/web/react/components/delete_post_modal.jsx index 827654e1b..4cde5feed 100644 --- a/web/react/components/delete_post_modal.jsx +++ b/web/react/components/delete_post_modal.jsx @@ -23,7 +23,7 @@ export default class DeletePostModal extends React.Component { this.selectedList = null; this.state = { - show: true, + show: false, post: null, commentCount: 0, error: '' @@ -40,6 +40,14 @@ export default class DeletePostModal extends React.Component { ModalStore.removeModalListener(ActionTypes.TOGGLE_DELETE_POST_MODAL, this.handleToggle); } + componentDidUpdate(prevProps, prevState) { + if (this.state.show && !prevState.show) { + setTimeout(() => { + $(ReactDOM.findDOMNode(this.refs.deletePostBtn)).focus(); + }, 0); + } + } + handleDelete() { Client.deletePost( this.state.post.channel_id, @@ -149,10 +157,10 @@ export default class DeletePostModal extends React.Component { {'Cancel'} </button> <button + ref='deletePostBtn' type='button' className='btn btn-danger' onClick={this.handleDelete} - autoFocus='autofocus' > {'Delete'} </button> diff --git a/web/react/components/file_info_preview.jsx b/web/react/components/file_info_preview.jsx index 4b76cd162..45d89007f 100644 --- a/web/react/components/file_info_preview.jsx +++ b/web/react/components/file_info_preview.jsx @@ -5,11 +5,16 @@ import * as Utils from '../utils/utils.jsx'; export default function FileInfoPreview({filename, fileUrl, fileInfo}) { // non-image files include a section providing details about the file - let infoString = 'File type ' + fileInfo.extension.toUpperCase(); - if (fileInfo.size > 0) { - infoString += ', Size ' + Utils.fileSizeToString(fileInfo.size); + const infoParts = []; + + if (fileInfo.extension !== '') { + infoParts.push('File type ' + fileInfo.extension.toUpperCase()); } + infoParts.push('Size ' + Utils.fileSizeToString(fileInfo.size)); + + const infoString = infoParts.join(', '); + const name = decodeURIComponent(Utils.getFileName(filename)); return ( diff --git a/web/react/components/suggestion/at_mention_provider.jsx b/web/react/components/suggestion/at_mention_provider.jsx index 8c2893448..e502c981d 100644 --- a/web/react/components/suggestion/at_mention_provider.jsx +++ b/web/react/components/suggestion/at_mention_provider.jsx @@ -5,6 +5,8 @@ import SuggestionStore from '../../stores/suggestion_store.jsx'; import UserStore from '../../stores/user_store.jsx'; import * as Utils from '../../utils/utils.jsx'; +const MaxUserSuggestions = 40; + class AtMentionSuggestion extends React.Component { render() { const {item, isSelection, onClick} = this.props; @@ -78,6 +80,10 @@ export default class AtMentionProvider { if (user.username.startsWith(usernamePrefix)) { filtered.push(user); } + + if (filtered.length >= MaxUserSuggestions) { + break; + } } // add dummy users to represent the @all and @channel special mentions diff --git a/web/react/components/team_signup_welcome_page.jsx b/web/react/components/team_signup_welcome_page.jsx index aa91a1329..a374dd363 100644 --- a/web/react/components/team_signup_welcome_page.jsx +++ b/web/react/components/team_signup_welcome_page.jsx @@ -59,7 +59,13 @@ export default class TeamSignupWelcomePage extends React.Component { } }.bind(this), function error(err) { - this.setState({serverError: err.message}); + let errorMsg = err.message; + + if (err.detailed_error.indexOf('Invalid RCPT TO address provided') >= 0) { + errorMsg = 'Please enter a valid email address'; + } + + this.setState({emailError: '', serverError: errorMsg}); }.bind(this) ); } diff --git a/web/react/pages/admin_console.jsx b/web/react/pages/admin_console.jsx index cbd2bd80d..3f4c39934 100644 --- a/web/react/pages/admin_console.jsx +++ b/web/react/pages/admin_console.jsx @@ -4,25 +4,68 @@ import ErrorBar from '../components/error_bar.jsx'; import SelectTeamModal from '../components/admin_console/select_team_modal.jsx'; import AdminController from '../components/admin_console/admin_controller.jsx'; +import * as Client from '../utils/client.jsx'; -export function setupAdminConsolePage(props) { - ReactDOM.render( - <AdminController - tab={props.ActiveTab} - teamId={props.TeamId} - />, - document.getElementById('admin_controller') - ); +var IntlProvider = ReactIntl.IntlProvider; - ReactDOM.render( - <SelectTeamModal />, - document.getElementById('select_team_modal') - ); +class Root extends React.Component { + constructor() { + super(); + this.state = { + translations: null, + loaded: false + }; + } - ReactDOM.render( - <ErrorBar/>, - document.getElementById('error_bar') - ); + static propTypes() { + return { + map: React.PropTypes.object.isRequired + }; + } + + componentWillMount() { + Client.getTranslations( + this.props.map.Locale, + (data) => { + this.setState({ + translations: data, + loaded: true + }); + }, + () => { + this.setState({ + loaded: true + }); + } + ); + } + + render() { + if (!this.state.loaded) { + return <div></div>; + } + + return ( + <IntlProvider + locale={this.props.map.Locale} + messages={this.state.translations} + > + <div> + <ErrorBar/> + <AdminController + tab={this.props.map.ActiveTab} + teamId={this.props.map.TeamId} + /> + <SelectTeamModal /> + </div> + </IntlProvider> + ); + } } -global.window.setup_admin_console_page = setupAdminConsolePage; +global.window.setup_admin_console_page = function setup(props) { + ReactDOM.render( + <Root map={props} />, + document.getElementById('admin_controller') + ); +}; diff --git a/web/react/pages/authorize.jsx b/web/react/pages/authorize.jsx index 71f17d007..7474332ce 100644 --- a/web/react/pages/authorize.jsx +++ b/web/react/pages/authorize.jsx @@ -2,20 +2,69 @@ // See License.txt for license information. import Authorize from '../components/authorize.jsx'; +import * as Client from '../utils/client.jsx'; -function setupAuthorizePage(props) { +var IntlProvider = ReactIntl.IntlProvider; + +class Root extends React.Component { + constructor() { + super(); + this.state = { + translations: null, + loaded: false + }; + } + + static propTypes() { + return { + map: React.PropTypes.object.isRequired + }; + } + + componentWillMount() { + Client.getTranslations( + this.props.map.Locale, + (data) => { + this.setState({ + translations: data, + loaded: true + }); + }, + () => { + this.setState({ + loaded: true + }); + } + ); + } + + render() { + if (!this.state.loaded) { + return <div></div>; + } + + return ( + <IntlProvider + locale={this.props.map.Locale} + messages={this.state.translations} + > + <Authorize + teamName={this.props.map.TeamName} + appName={this.props.map.AppName} + responseType={this.props.map.ResponseType} + clientId={this.props.map.ClientId} + redirectUri={this.props.map.RedirectUri} + scope={this.props.map.Scope} + state={this.props.map.State} + /> + </IntlProvider> + ); + } +} + +global.window.setup_authorize_page = function setup(props) { ReactDOM.render( - <Authorize - teamName={props.TeamName} - appName={props.AppName} - responseType={props.ResponseType} - clientId={props.ClientId} - redirectUri={props.RedirectUri} - scope={props.Scope} - state={props.State} - />, + <Root map={props} />, document.getElementById('authorize') ); -} - -global.window.setup_authorize_page = setupAuthorizePage; +}; diff --git a/web/react/pages/claim_account.jsx b/web/react/pages/claim_account.jsx index bca203d96..7c6af73ca 100644 --- a/web/react/pages/claim_account.jsx +++ b/web/react/pages/claim_account.jsx @@ -2,18 +2,67 @@ // See License.txt for license information. import ClaimAccount from '../components/claim/claim_account.jsx'; +import * as Client from '../utils/client.jsx'; -function setupClaimAccountPage(props) { +var IntlProvider = ReactIntl.IntlProvider; + +class Root extends React.Component { + constructor() { + super(); + this.state = { + translations: null, + loaded: false + }; + } + + static propTypes() { + return { + map: React.PropTypes.object.isRequired + }; + } + + componentWillMount() { + Client.getTranslations( + this.props.map.Locale, + (data) => { + this.setState({ + translations: data, + loaded: true + }); + }, + () => { + this.setState({ + loaded: true + }); + } + ); + } + + render() { + if (!this.state.loaded) { + return <div></div>; + } + + return ( + <IntlProvider + locale={this.props.map.Locale} + messages={this.state.translations} + > + <ClaimAccount + email={this.props.map.Email} + currentType={this.props.map.CurrentType} + newType={this.props.map.NewType} + teamName={this.props.map.TeamName} + teamDisplayName={this.props.map.TeamDisplayName} + /> + </IntlProvider> + ); + } +} + +global.window.setup_claim_account_page = function setup(props) { ReactDOM.render( - <ClaimAccount - email={props.Email} - currentType={props.CurrentType} - newType={props.NewType} - teamName={props.TeamName} - teamDisplayName={props.TeamDisplayName} - />, + <Root map={props} />, document.getElementById('claim') ); -} - -global.window.setup_claim_account_page = setupClaimAccountPage; +};
\ No newline at end of file diff --git a/web/react/pages/docs.jsx b/web/react/pages/docs.jsx index 74d9c2d19..2f5d4db55 100644 --- a/web/react/pages/docs.jsx +++ b/web/react/pages/docs.jsx @@ -2,15 +2,63 @@ // See License.txt for license information. import Docs from '../components/docs.jsx'; +import * as Client from '../utils/client.jsx'; -function setupDocumentationPage(props) { - ReactDOM.render( - <Docs - site={props.Site} - />, - document.getElementById('docs') - ); +var IntlProvider = ReactIntl.IntlProvider; + +class Root extends React.Component { + constructor() { + super(); + this.state = { + translations: null, + loaded: false + }; + } + + static propTypes() { + return { + map: React.PropTypes.object.isRequired + }; + } + + componentWillMount() { + Client.getTranslations( + this.props.map.Locale, + (data) => { + this.setState({ + translations: data, + loaded: true + }); + }, + () => { + this.setState({ + loaded: true + }); + } + ); + } + + render() { + if (!this.state.loaded) { + return <div></div>; + } + + return ( + <IntlProvider + locale={this.props.map.Locale} + messages={this.state.translations} + > + <Docs site={this.props.map.Site} /> + </IntlProvider> + ); + } } global.window.mm_user = global.window.mm_user || {}; -global.window.setup_documentation_page = setupDocumentationPage; + +global.window.setup_documentation_page = function setup(props) { + ReactDOM.render( + <Root map={props} />, + document.getElementById('docs') + ); +}; diff --git a/web/react/pages/password_reset.jsx b/web/react/pages/password_reset.jsx index 4a6f1dcb0..23bbf2691 100644 --- a/web/react/pages/password_reset.jsx +++ b/web/react/pages/password_reset.jsx @@ -2,18 +2,67 @@ // See License.txt for license information. import PasswordReset from '../components/password_reset.jsx'; +import * as Client from '../utils/client.jsx'; -function setupPasswordResetPage(props) { +var IntlProvider = ReactIntl.IntlProvider; + +class Root extends React.Component { + constructor() { + super(); + this.state = { + translations: null, + loaded: false + }; + } + + static propTypes() { + return { + map: React.PropTypes.object.isRequired + }; + } + + componentWillMount() { + Client.getTranslations( + this.props.map.Locale, + (data) => { + this.setState({ + translations: data, + loaded: true + }); + }, + () => { + this.setState({ + loaded: true + }); + } + ); + } + + render() { + if (!this.state.loaded) { + return <div></div>; + } + + return ( + <IntlProvider + locale={this.props.map.Locale} + messages={this.state.translations} + > + <PasswordReset + isReset={this.props.map.IsReset} + teamDisplayName={this.props.map.TeamDisplayName} + teamName={this.props.map.TeamName} + hash={this.props.map.Hash} + data={this.props.map.Data} + /> + </IntlProvider> + ); + } +} + +global.window.setup_password_reset_page = function setup(props) { ReactDOM.render( - <PasswordReset - isReset={props.IsReset} - teamDisplayName={props.TeamDisplayName} - teamName={props.TeamName} - hash={props.Hash} - data={props.Data} - />, + <Root map={props} />, document.getElementById('reset') ); -} - -global.window.setup_password_reset_page = setupPasswordResetPage; +}; diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx index 08ea45000..8f4f86a7c 100644 --- a/web/react/pages/signup_team.jsx +++ b/web/react/pages/signup_team.jsx @@ -2,8 +2,60 @@ // See License.txt for license information. import SignupTeam from '../components/signup_team.jsx'; +import * as Client from '../utils/client.jsx'; -function setupSignupTeamPage(props) { +var IntlProvider = ReactIntl.IntlProvider; + +class Root extends React.Component { + constructor() { + super(); + this.state = { + translations: null, + loaded: false + }; + } + + static propTypes() { + return { + map: React.PropTypes.object.isRequired, + teams: React.PropTypes.object.isRequired + }; + } + + componentWillMount() { + Client.getTranslations( + this.props.map.Locale, + (data) => { + this.setState({ + translations: data, + loaded: true + }); + }, + () => { + this.setState({ + loaded: true + }); + } + ); + } + + render() { + if (!this.state.loaded) { + return <div></div>; + } + + return ( + <IntlProvider + locale={this.props.map.Locale} + messages={this.state.translations} + > + <SignupTeam teams={this.props.teams} /> + </IntlProvider> + ); + } +} + +global.window.setup_signup_team_page = function setup(props) { var teams = []; for (var prop in props) { @@ -15,9 +67,10 @@ function setupSignupTeamPage(props) { } ReactDOM.render( - <SignupTeam teams={teams} />, + <Root + map={props} + teams={teams} + />, document.getElementById('signup-team') ); -} - -global.window.setup_signup_team_page = setupSignupTeamPage; +};
\ No newline at end of file diff --git a/web/react/pages/signup_team_complete.jsx b/web/react/pages/signup_team_complete.jsx index d5ed144a1..1bee4e598 100644 --- a/web/react/pages/signup_team_complete.jsx +++ b/web/react/pages/signup_team_complete.jsx @@ -2,16 +2,65 @@ // See License.txt for license information. import SignupTeamComplete from '../components/signup_team_complete.jsx'; +import * as Client from '../utils/client.jsx'; -function setupSignupTeamCompletePage(props) { +var IntlProvider = ReactIntl.IntlProvider; + +class Root extends React.Component { + constructor() { + super(); + this.state = { + translations: null, + loaded: false + }; + } + + static propTypes() { + return { + map: React.PropTypes.object.isRequired + }; + } + + componentWillMount() { + Client.getTranslations( + this.props.map.Locale, + (data) => { + this.setState({ + translations: data, + loaded: true + }); + }, + () => { + this.setState({ + loaded: true + }); + } + ); + } + + render() { + if (!this.state.loaded) { + return <div></div>; + } + + return ( + <IntlProvider + locale={this.props.map.Locale} + messages={this.state.translations} + > + <SignupTeamComplete + email={this.props.map.Email} + hash={this.props.map.Hash} + data={this.props.map.Data} + /> + </IntlProvider> + ); + } +} + +global.window.setup_signup_team_complete_page = function setup(props) { ReactDOM.render( - <SignupTeamComplete - email={props.Email} - hash={props.Hash} - data={props.Data} - />, + <Root map={props} />, document.getElementById('signup-team-complete') ); -} - -global.window.setup_signup_team_complete_page = setupSignupTeamCompletePage; +};
\ No newline at end of file diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx index de2c48443..6c761c1ee 100644 --- a/web/react/pages/signup_user_complete.jsx +++ b/web/react/pages/signup_user_complete.jsx @@ -2,19 +2,68 @@ // See License.txt for license information. import SignupUserComplete from '../components/signup_user_complete.jsx'; +import * as Client from '../utils/client.jsx'; -function setupSignupUserCompletePage(props) { +var IntlProvider = ReactIntl.IntlProvider; + +class Root extends React.Component { + constructor() { + super(); + this.state = { + translations: null, + loaded: false + }; + } + + static propTypes() { + return { + map: React.PropTypes.object.isRequired + }; + } + + componentWillMount() { + Client.getTranslations( + this.props.map.Locale, + (data) => { + this.setState({ + translations: data, + loaded: true + }); + }, + () => { + this.setState({ + loaded: true + }); + } + ); + } + + render() { + if (!this.state.loaded) { + return <div></div>; + } + + return ( + <IntlProvider + locale={this.props.map.Locale} + messages={this.state.translations} + > + <SignupUserComplete + teamId={this.props.map.TeamId} + teamName={this.props.map.TeamName} + teamDisplayName={this.props.map.TeamDisplayName} + email={this.props.map.Email} + hash={this.props.map.Hash} + data={this.props.map.Data} + /> + </IntlProvider> + ); + } +} + +global.window.setup_signup_user_complete_page = function setup(props) { ReactDOM.render( - <SignupUserComplete - teamId={props.TeamId} - teamName={props.TeamName} - teamDisplayName={props.TeamDisplayName} - email={props.Email} - hash={props.Hash} - data={props.Data} - />, + <Root map={props} />, document.getElementById('signup-user-complete') ); -} - -global.window.setup_signup_user_complete_page = setupSignupUserCompletePage; +};
\ No newline at end of file diff --git a/web/react/pages/verify.jsx b/web/react/pages/verify.jsx index d4ce4844d..2fc619e58 100644 --- a/web/react/pages/verify.jsx +++ b/web/react/pages/verify.jsx @@ -2,15 +2,66 @@ // See License.txt for license information. import EmailVerify from '../components/email_verify.jsx'; +import * as Client from '../utils/client.jsx'; -global.window.setupVerifyPage = function setupVerifyPage(props) { +var IntlProvider = ReactIntl.IntlProvider; + +class Root extends React.Component { + constructor() { + super(); + this.state = { + translations: null, + loaded: false + }; + } + + static propTypes() { + return { + map: React.PropTypes.object.isRequired + }; + } + + componentWillMount() { + Client.getTranslations( + this.props.map.Locale, + (data) => { + this.setState({ + translations: data, + loaded: true + }); + }, + () => { + this.setState({ + loaded: true + }); + } + ); + } + + render() { + if (!this.state.loaded) { + return <div></div>; + } + + return ( + <IntlProvider + locale={this.props.map.Locale} + messages={this.state.translations} + > + <EmailVerify + isVerified={this.props.map.IsVerified} + teamURL={this.props.map.TeamURL} + userEmail={this.props.map.UserEmail} + resendSuccess={this.props.map.ResendSuccess} + /> + </IntlProvider> + ); + } +} + +global.window.setupVerifyPage = function setup(props) { ReactDOM.render( - <EmailVerify - isVerified={props.IsVerified} - teamURL={props.TeamURL} - userEmail={props.UserEmail} - resendSuccess={props.ResendSuccess} - />, + <Root map={props} />, document.getElementById('verify') ); }; diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index 80b29da4e..855de3fc2 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -399,7 +399,7 @@ export function getConfig(success, error) { }); } -export function getAnalytics(teamId, name, success, error) { +export function getTeamAnalytics(teamId, name, success, error) { $.ajax({ url: '/api/v1/admin/analytics/' + teamId + '/' + name, dataType: 'json', @@ -407,7 +407,21 @@ export function getAnalytics(teamId, name, success, error) { type: 'GET', success, error: (xhr, status, err) => { - var e = handleError('getAnalytics', xhr, status, err); + var e = handleError('getTeamAnalytics', xhr, status, err); + error(e); + } + }); +} + +export function getSystemAnalytics(name, success, error) { + $.ajax({ + url: '/api/v1/admin/analytics/' + name, + dataType: 'json', + contentType: 'application/json', + type: 'GET', + success, + error: (xhr, status, err) => { + var e = handleError('getSystemAnalytics', xhr, status, err); error(e); } }); diff --git a/web/templates/admin_console.html b/web/templates/admin_console.html index 0e37a4660..08c90493e 100644 --- a/web/templates/admin_console.html +++ b/web/templates/admin_console.html @@ -6,11 +6,7 @@ <body> <script src="/static/js/Chart.min.js"></script> -<div id='error_bar'></div> - -<div id='admin_controller' class='container-fluid'></div> - -<div id='select_team_modal'></div> +<div id='admin_controller'></div> <script> window.setup_admin_console_page({{ .Props }}); diff --git a/web/web.go b/web/web.go index f73860b67..48755f94f 100644 --- a/web/web.go +++ b/web/web.go @@ -565,9 +565,9 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { user := result.Data.(*model.User) if user.LastActivityAt > 0 { - api.SendEmailChangeVerifyEmailAndForget(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + api.SendEmailChangeVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) } else { - api.SendVerifyEmailAndForget(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) + api.SendVerifyEmailAndForget(c, user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) } newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1) |