Added user language setting (#3875)
* Added user language setting * Added translation string for setting * Fixed import order + typo * improved checking if the user has a language saved in the db * The current saved language is now set a default inside the dropdown * fmt * When a user signs in and doesn't have a language saved, the current browser language is saved * updated gitea-sdk * Merge branch 'master' of https://github.com/go-gitea/gitea into save-user-language # Conflicts: # models/migrations/migrations.go # models/migrations/v62.go * Made tests work again * trigger CI * trigger CI * fmt * re-trigger that FUCKING CI SO IT REALLY PICKS UP THE LATEST COMMIT ISTEAD OF PREDENDING TO DO SO * re-trigger that FUCKING CI SO IT REALLY PICKS UP THE LATEST COMMIT ISTEAD OF PREDENDING TO DO SO * When loggin in, only the language col gets updated instead of everything
This commit is contained in:
		
							parent
							
								
									795dcc8ecf
								
							
						
					
					
						commit
						1fdf560678
					
				
					 13 changed files with 81 additions and 10 deletions
				
			
		|  | @ -27,9 +27,10 @@ func TestRenameUsername(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	session := loginUser(t, "user2") | 	session := loginUser(t, "user2") | ||||||
| 	req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ | 	req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ | ||||||
| 		"_csrf": GetCSRF(t, session, "/user/settings"), | 		"_csrf":    GetCSRF(t, session, "/user/settings"), | ||||||
| 		"name":  "newUsername", | 		"name":     "newUsername", | ||||||
| 		"email": "user2@example.com", | 		"email":    "user2@example.com", | ||||||
|  | 		"language": "en-us", | ||||||
| 	}) | 	}) | ||||||
| 	session.MakeRequest(t, req, http.StatusFound) | 	session.MakeRequest(t, req, http.StatusFound) | ||||||
| 
 | 
 | ||||||
|  | @ -81,9 +82,10 @@ func TestRenameReservedUsername(t *testing.T) { | ||||||
| 	for _, reservedUsername := range reservedUsernames { | 	for _, reservedUsername := range reservedUsernames { | ||||||
| 		t.Logf("Testing username %s", reservedUsername) | 		t.Logf("Testing username %s", reservedUsername) | ||||||
| 		req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ | 		req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ | ||||||
| 			"_csrf": GetCSRF(t, session, "/user/settings"), | 			"_csrf":    GetCSRF(t, session, "/user/settings"), | ||||||
| 			"name":  reservedUsername, | 			"name":     reservedUsername, | ||||||
| 			"email": "user2@example.com", | 			"email":    "user2@example.com", | ||||||
|  | 			"language": "en-us", | ||||||
| 		}) | 		}) | ||||||
| 		resp := session.MakeRequest(t, req, http.StatusFound) | 		resp := session.MakeRequest(t, req, http.StatusFound) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ func TestXSSUserFullName(t *testing.T) { | ||||||
| 		"name":      user.Name, | 		"name":      user.Name, | ||||||
| 		"full_name": fullName, | 		"full_name": fullName, | ||||||
| 		"email":     user.Email, | 		"email":     user.Email, | ||||||
|  | 		"language":  "en-us", | ||||||
| 	}) | 	}) | ||||||
| 	session.MakeRequest(t, req, http.StatusFound) | 	session.MakeRequest(t, req, http.StatusFound) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -178,6 +178,8 @@ var migrations = []Migration{ | ||||||
| 	NewMigration("add size column for attachments", addSizeToAttachment), | 	NewMigration("add size column for attachments", addSizeToAttachment), | ||||||
| 	// v62 -> v63
 | 	// v62 -> v63
 | ||||||
| 	NewMigration("add last used passcode column for TOTP", addLastUsedPasscodeTOTP), | 	NewMigration("add last used passcode column for TOTP", addLastUsedPasscodeTOTP), | ||||||
|  | 	// v63 -> v64
 | ||||||
|  | 	NewMigration("add language column for user setting", addLanguageSetting), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Migrate database to current version
 | // Migrate database to current version
 | ||||||
|  |  | ||||||
							
								
								
									
										23
									
								
								models/migrations/v63.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								models/migrations/v63.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | ||||||
|  | // Copyright 2018 The Gitea Authors. All rights reserved.
 | ||||||
|  | // Use of this source code is governed by a MIT-style
 | ||||||
|  | // license that can be found in the LICENSE file.
 | ||||||
|  | 
 | ||||||
|  | package migrations | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/go-xorm/xorm" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func addLanguageSetting(x *xorm.Engine) error { | ||||||
|  | 	type User struct { | ||||||
|  | 		Language string `xorm:"VARCHAR(5)"` | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := x.Sync2(new(User)); err != nil { | ||||||
|  | 		return fmt.Errorf("Sync2: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | @ -94,6 +94,7 @@ type User struct { | ||||||
| 	Website          string | 	Website          string | ||||||
| 	Rands            string `xorm:"VARCHAR(10)"` | 	Rands            string `xorm:"VARCHAR(10)"` | ||||||
| 	Salt             string `xorm:"VARCHAR(10)"` | 	Salt             string `xorm:"VARCHAR(10)"` | ||||||
|  | 	Language         string `xorm:"VARCHAR(5)"` | ||||||
| 
 | 
 | ||||||
| 	CreatedUnix   util.TimeStamp `xorm:"INDEX created"` | 	CreatedUnix   util.TimeStamp `xorm:"INDEX created"` | ||||||
| 	UpdatedUnix   util.TimeStamp `xorm:"INDEX updated"` | 	UpdatedUnix   util.TimeStamp `xorm:"INDEX updated"` | ||||||
|  | @ -185,6 +186,7 @@ func (u *User) APIFormat() *api.User { | ||||||
| 		FullName:  u.FullName, | 		FullName:  u.FullName, | ||||||
| 		Email:     u.getEmail(), | 		Email:     u.getEmail(), | ||||||
| 		AvatarURL: u.AvatarLink(), | 		AvatarURL: u.AvatarLink(), | ||||||
|  | 		Language:  u.Language, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -109,6 +109,7 @@ type UpdateProfileForm struct { | ||||||
| 	KeepEmailPrivate bool | 	KeepEmailPrivate bool | ||||||
| 	Website          string `binding:"ValidUrl;MaxSize(255)"` | 	Website          string `binding:"ValidUrl;MaxSize(255)"` | ||||||
| 	Location         string `binding:"MaxSize(50)"` | 	Location         string `binding:"MaxSize(50)"` | ||||||
|  | 	Language         string `binding:"Size(5)"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Validate validates the fields
 | // Validate validates the fields
 | ||||||
|  |  | ||||||
|  | @ -331,6 +331,7 @@ change_username = Your username has been changed. | ||||||
| change_username_prompt = Note: username changes also change your account URL. | change_username_prompt = Note: username changes also change your account URL. | ||||||
| continue = Continue | continue = Continue | ||||||
| cancel = Cancel | cancel = Cancel | ||||||
|  | language = Language | ||||||
| 
 | 
 | ||||||
| lookup_avatar_by_mail = Look Up Avatar by Email Address | lookup_avatar_by_mail = Look Up Avatar by Email Address | ||||||
| federated_avatar_lookup = Federated Avatar Lookup | federated_avatar_lookup = Federated Avatar Lookup | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								public/swagger.v1.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								public/swagger.v1.json
									
									
									
									
										vendored
									
									
								
							|  | @ -7318,6 +7318,11 @@ | ||||||
|           "format": "int64", |           "format": "int64", | ||||||
|           "x-go-name": "ID" |           "x-go-name": "ID" | ||||||
|         }, |         }, | ||||||
|  |         "language": { | ||||||
|  |           "description": "User locale", | ||||||
|  |           "type": "string", | ||||||
|  |           "x-go-name": "Language" | ||||||
|  |         }, | ||||||
|         "login": { |         "login": { | ||||||
|           "description": "the user's username", |           "description": "the user's username", | ||||||
|           "type": "string", |           "type": "string", | ||||||
|  |  | ||||||
|  | @ -339,6 +339,18 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR | ||||||
| 	ctx.Session.Set("uid", u.ID) | 	ctx.Session.Set("uid", u.ID) | ||||||
| 	ctx.Session.Set("uname", u.Name) | 	ctx.Session.Set("uname", u.Name) | ||||||
| 
 | 
 | ||||||
|  | 	// Language setting of the user overwrites the one previously set
 | ||||||
|  | 	// If the user does not have a locale set, we save the current one.
 | ||||||
|  | 	if len(u.Language) == 0 { | ||||||
|  | 		u.Language = ctx.Locale.Language() | ||||||
|  | 		if err := models.UpdateUserCols(u, "language"); err != nil { | ||||||
|  | 			log.Error(4, fmt.Sprintf("Error updating user language [user: %d, locale: %s]", u.ID, u.Language)) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ctx.SetCookie("lang", u.Language, nil, setting.AppSubURL) | ||||||
|  | 
 | ||||||
| 	// Clear whatever CSRF has right now, force to generate a new one
 | 	// Clear whatever CSRF has right now, force to generate a new one
 | ||||||
| 	ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL) | 	ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL) | ||||||
| 
 | 
 | ||||||
|  | @ -704,6 +716,7 @@ func SignOut(ctx *context.Context) { | ||||||
| 	ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubURL) | 	ctx.SetCookie(setting.CookieUserName, "", -1, setting.AppSubURL) | ||||||
| 	ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL) | 	ctx.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubURL) | ||||||
| 	ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL) | 	ctx.SetCookie(setting.CSRFCookieName, "", -1, setting.AppSubURL) | ||||||
|  | 	ctx.SetCookie("lang", "", -1, setting.AppSubURL) // Setting the lang cookie will trigger the middleware to reset the language ot previous state.
 | ||||||
| 	ctx.Redirect(setting.AppSubURL + "/") | 	ctx.Redirect(setting.AppSubURL + "/") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ import ( | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/Unknwon/com" | 	"github.com/Unknwon/com" | ||||||
|  | 	"github.com/Unknwon/i18n" | ||||||
| 	"github.com/pquerna/otp" | 	"github.com/pquerna/otp" | ||||||
| 	"github.com/pquerna/otp/totp" | 	"github.com/pquerna/otp/totp" | ||||||
| 
 | 
 | ||||||
|  | @ -105,6 +106,7 @@ func SettingsPost(ctx *context.Context, form auth.UpdateProfileForm) { | ||||||
| 	ctx.User.KeepEmailPrivate = form.KeepEmailPrivate | 	ctx.User.KeepEmailPrivate = form.KeepEmailPrivate | ||||||
| 	ctx.User.Website = form.Website | 	ctx.User.Website = form.Website | ||||||
| 	ctx.User.Location = form.Location | 	ctx.User.Location = form.Location | ||||||
|  | 	ctx.User.Language = form.Language | ||||||
| 	if err := models.UpdateUserSetting(ctx.User); err != nil { | 	if err := models.UpdateUserSetting(ctx.User); err != nil { | ||||||
| 		if _, ok := err.(models.ErrEmailAlreadyUsed); ok { | 		if _, ok := err.(models.ErrEmailAlreadyUsed); ok { | ||||||
| 			ctx.Flash.Error(ctx.Tr("form.email_been_used")) | 			ctx.Flash.Error(ctx.Tr("form.email_been_used")) | ||||||
|  | @ -115,8 +117,11 @@ func SettingsPost(ctx *context.Context, form auth.UpdateProfileForm) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Update the language to the one we just set
 | ||||||
|  | 	ctx.SetCookie("lang", ctx.User.Language, nil, setting.AppSubURL) | ||||||
|  | 
 | ||||||
| 	log.Trace("User settings updated: %s", ctx.User.Name) | 	log.Trace("User settings updated: %s", ctx.User.Name) | ||||||
| 	ctx.Flash.Success(ctx.Tr("settings.update_profile_success")) | 	ctx.Flash.Success(i18n.Tr(ctx.User.Language, "settings.update_profile_success")) | ||||||
| 	ctx.Redirect(setting.AppSubURL + "/user/settings") | 	ctx.Redirect(setting.AppSubURL + "/user/settings") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -40,6 +40,20 @@ | ||||||
| 					<input id="location" name="location"  value="{{.SignedUser.Location}}"> | 					<input id="location" name="location"  value="{{.SignedUser.Location}}"> | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
|  | 					<div class="field"> | ||||||
|  | 						<label for="language">{{.i18n.Tr "settings.language"}}</label> | ||||||
|  | 						<div class="ui language selection dropdown" id="language"> | ||||||
|  | 							<input name="language" type="hidden"> | ||||||
|  | 							<i class="dropdown icon"></i> | ||||||
|  | 							<div class="text">{{range .AllLangs}}{{if eq $.SignedUser.Language .Lang}}{{.Name}}{{end}}{{end}}</div> | ||||||
|  | 							<div class="menu"> | ||||||
|  | 							{{range .AllLangs}} | ||||||
|  | 								<div class="item{{if eq $.SignedUser.Language .Lang}} active selected{{end}}" data-value="{{.Lang}}">{{.Name}}</div> | ||||||
|  | 							{{end}} | ||||||
|  | 							</div> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 
 | ||||||
| 				<div class="field"> | 				<div class="field"> | ||||||
| 					<button class="ui green button">{{$.i18n.Tr "settings.update_profile"}}</button> | 					<button class="ui green button">{{$.i18n.Tr "settings.update_profile"}}</button> | ||||||
| 				</div> | 				</div> | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								vendor/code.gitea.io/sdk/gitea/user.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								vendor/code.gitea.io/sdk/gitea/user.go
									
									
									
										generated
									
									
										vendored
									
									
								
							|  | @ -22,6 +22,8 @@ type User struct { | ||||||
| 	Email string `json:"email"` | 	Email string `json:"email"` | ||||||
| 	// URL to the user's avatar
 | 	// URL to the user's avatar
 | ||||||
| 	AvatarURL string `json:"avatar_url"` | 	AvatarURL string `json:"avatar_url"` | ||||||
|  | 	// User locale
 | ||||||
|  | 	Language string `json:"language"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // MarshalJSON implements the json.Marshaler interface for User, adding field(s) for backward compatibility
 | // MarshalJSON implements the json.Marshaler interface for User, adding field(s) for backward compatibility
 | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								vendor/vendor.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								vendor/vendor.json
									
									
									
									
										vendored
									
									
								
							|  | @ -9,10 +9,10 @@ | ||||||
| 			"revisionTime": "2018-04-21T01:08:19Z" | 			"revisionTime": "2018-04-21T01:08:19Z" | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"checksumSHA1": "xXzi8Xx7HA3M0z3lR/1wr1Vz1fc=", | 			"checksumSHA1": "WMD6+Qh2+5hd9uiq910pF/Ihylw=", | ||||||
| 			"path": "code.gitea.io/sdk/gitea", | 			"path": "code.gitea.io/sdk/gitea", | ||||||
| 			"revision": "142acef5ce79f78585afcce31748af46c72a3dea", | 			"revision": "1c8d12f79a51605ed91587aa6b86cf38fc0f987f", | ||||||
| 			"revisionTime": "2018-04-17T00:54:29Z" | 			"revisionTime": "2018-05-01T11:15:19Z" | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"checksumSHA1": "bOODD4Gbw3GfcuQPU2dI40crxxk=", | 			"checksumSHA1": "bOODD4Gbw3GfcuQPU2dI40crxxk=", | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue