mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:56:10 +01:00 
			
		
		
		
	Remove legacy unmaintained packages, refactor to support change default locale (#19308)
Remove two unmaintained vendor packages `i18n` and `paginater`. Changes: * Rewrite `i18n` package with a more clear fallback mechanism. Fix an unstable `Tr` behavior, add more tests. * Refactor the legacy `Paginater` to `Paginator`, test cases are kept unchanged. Trivial enhancement (no breaking for end users): * Use the first locale in LANGS setting option as the default, add a log to prevent from surprising users.
This commit is contained in:
		| @@ -2117,6 +2117,7 @@ PATH = | |||||||
| ;[i18n] | ;[i18n] | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||||||
|  | ;; The first locale will be used as the default if user browser's language doesn't match any locale in the list. | ||||||
| ;LANGS = en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,uk-UA,ja-JP,es-ES,pt-BR,pt-PT,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR,el-GR,fa-IR,hu-HU,id-ID,ml-IN | ;LANGS = en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,uk-UA,ja-JP,es-ES,pt-BR,pt-PT,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR,el-GR,fa-IR,hu-HU,id-ID,ml-IN | ||||||
| ;NAMES = English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,Українська,日本語,español,português do Brasil,Português de Portugal,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어,ελληνικά,فارسی,magyar nyelv,bahasa Indonesia,മലയാളം | ;NAMES = English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,Українська,日本語,español,português do Brasil,Português de Portugal,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어,ελληνικά,فارسی,magyar nyelv,bahasa Indonesia,മലയാളം | ||||||
|  |  | ||||||
|   | |||||||
| @@ -997,7 +997,8 @@ Default templates for project boards: | |||||||
|  |  | ||||||
| ## i18n (`i18n`) | ## i18n (`i18n`) | ||||||
|  |  | ||||||
| - `LANGS`: **en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,ja-JP,es-ES,pt-BR,pt-PT,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR,el-GR,fa-IR,hu-HU,id-ID,ml-IN**: List of locales shown in language selector | - `LANGS`: **en-US,zh-CN,zh-HK,zh-TW,de-DE,fr-FR,nl-NL,lv-LV,ru-RU,ja-JP,es-ES,pt-BR,pt-PT,pl-PL,bg-BG,it-IT,fi-FI,tr-TR,cs-CZ,sr-SP,sv-SE,ko-KR,el-GR,fa-IR,hu-HU,id-ID,ml-IN**: | ||||||
|  |      List of locales shown in language selector. The first locale will be used as the default if user browser's language doesn't match any locale in the list. | ||||||
| - `NAMES`: **English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,日本語,español,português do Brasil,Português de Portugal,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어,ελληνικά,فارسی,magyar nyelv,bahasa Indonesia,മലയാളം**: Visible names corresponding to the locales | - `NAMES`: **English,简体中文,繁體中文(香港),繁體中文(台灣),Deutsch,français,Nederlands,latviešu,русский,日本語,español,português do Brasil,Português de Portugal,polski,български,italiano,suomi,Türkçe,čeština,српски,svenska,한국어,ελληνικά,فارسی,magyar nyelv,bahasa Indonesia,മലയാളം**: Visible names corresponding to the locales | ||||||
|  |  | ||||||
| ## U2F (`U2F`) **DEPRECATED** | ## U2F (`U2F`) **DEPRECATED** | ||||||
|   | |||||||
| @@ -299,6 +299,8 @@ LANGS = en-US,foo-BAR | |||||||
| NAMES = English,FooBar | NAMES = English,FooBar | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | The first locale will be used as the default if user browser's language doesn't match any locale in the list. | ||||||
|  |  | ||||||
| Locales may change between versions, so keeping track of your customized locales is highly encouraged. | Locales may change between versions, so keeping track of your customized locales is highly encouraged. | ||||||
|  |  | ||||||
| ### Readmes | ### Readmes | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @@ -78,8 +78,6 @@ require ( | |||||||
| 	github.com/stretchr/testify v1.7.0 | 	github.com/stretchr/testify v1.7.0 | ||||||
| 	github.com/syndtr/goleveldb v1.0.0 | 	github.com/syndtr/goleveldb v1.0.0 | ||||||
| 	github.com/tstranex/u2f v1.0.0 | 	github.com/tstranex/u2f v1.0.0 | ||||||
| 	github.com/unknwon/i18n v0.0.0-20210904045753-ff3a8617e361 |  | ||||||
| 	github.com/unknwon/paginater v0.0.0-20200328080006-042474bd0eae |  | ||||||
| 	github.com/unrolled/render v1.4.1 | 	github.com/unrolled/render v1.4.1 | ||||||
| 	github.com/urfave/cli v1.22.5 | 	github.com/urfave/cli v1.22.5 | ||||||
| 	github.com/xanzy/go-gitlab v0.58.0 | 	github.com/xanzy/go-gitlab v0.58.0 | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1503,10 +1503,6 @@ github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0o | |||||||
| github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= | github.com/unknwon/com v0.0.0-20190804042917-757f69c95f3e/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= | ||||||
| github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= | github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= | ||||||
| github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= | github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= | ||||||
| github.com/unknwon/i18n v0.0.0-20210904045753-ff3a8617e361 h1:4Ij5sX4JEzCCY/CCl8trJHey1tPsIDomYTZf145GKk0= |  | ||||||
| github.com/unknwon/i18n v0.0.0-20210904045753-ff3a8617e361/go.mod h1:+5rDk6sDGpl3azws3O+f+GpFSyN9GVr0K8cvQLQM2ZQ= |  | ||||||
| github.com/unknwon/paginater v0.0.0-20200328080006-042474bd0eae h1:ihaXiJkaca54IaCSnEXtE/uSZOmPxKZhDfVLrzZLFDs= |  | ||||||
| github.com/unknwon/paginater v0.0.0-20200328080006-042474bd0eae/go.mod h1:1fdkY6xxl6ExVs2QFv7R0F5IRZHKA8RahhB9fMC9RvM= |  | ||||||
| github.com/unrolled/render v1.4.1 h1:VdpMc2YkAOWzbmC/P2yoHhRDXgsaCQHcTJ1KK6SNCA4= | github.com/unrolled/render v1.4.1 h1:VdpMc2YkAOWzbmC/P2yoHhRDXgsaCQHcTJ1KK6SNCA4= | ||||||
| github.com/unrolled/render v1.4.1/go.mod h1:cK4RSTTVdND5j9EYEc0LAMOvdG11JeiKjyjfyZRvV2w= | github.com/unrolled/render v1.4.1/go.mod h1:cK4RSTTVdND5j9EYEc0LAMOvdG11JeiKjyjfyZRvV2w= | ||||||
| github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= | ||||||
| @@ -2272,7 +2268,6 @@ gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AW | |||||||
| gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= | ||||||
| gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= | ||||||
| gopkg.in/ini.v1 v1.44.2/go.mod h1:M3Cogqpuv0QCi3ExAY5V4uOt4qb/R3xZubo9m8lK5wg= | gopkg.in/ini.v1 v1.44.2/go.mod h1:M3Cogqpuv0QCi3ExAY5V4uOt4qb/R3xZubo9m8lK5wg= | ||||||
| gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= |  | ||||||
| gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||||
| gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||||
| gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||||
|   | |||||||
| @@ -16,10 +16,10 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/organization" | 	"code.gitea.io/gitea/models/organization" | ||||||
| 	"code.gitea.io/gitea/models/unittest" | 	"code.gitea.io/gitea/models/unittest" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
| 	"code.gitea.io/gitea/services/auth" | 	"code.gitea.io/gitea/services/auth" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/unknwon/i18n" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type ldapUser struct { | type ldapUser struct { | ||||||
|   | |||||||
| @@ -9,8 +9,9 @@ import ( | |||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/unknwon/i18n" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestViewBranches(t *testing.T) { | func TestViewBranches(t *testing.T) { | ||||||
|   | |||||||
| @@ -24,10 +24,10 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	api "code.gitea.io/gitea/modules/structs" | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/modules/test" | 	"code.gitea.io/gitea/modules/test" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
| 	"code.gitea.io/gitea/services/pull" | 	"code.gitea.io/gitea/services/pull" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/unknwon/i18n" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle repo_model.MergeStyle) *httptest.ResponseRecorder { | func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string, mergeStyle repo_model.MergeStyle) *httptest.ResponseRecorder { | ||||||
|   | |||||||
| @@ -14,10 +14,10 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/unittest" | 	"code.gitea.io/gitea/models/unittest" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/test" | 	"code.gitea.io/gitea/modules/test" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
|  |  | ||||||
| 	"github.com/PuerkitoBio/goquery" | 	"github.com/PuerkitoBio/goquery" | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/unknwon/i18n" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func createNewRelease(t *testing.T, session *TestSession, repoURL, tag, title string, preRelease, draft bool) { | func createNewRelease(t *testing.T, session *TestSession, repoURL, tag, title string, preRelease, draft bool) { | ||||||
|   | |||||||
| @@ -13,9 +13,9 @@ import ( | |||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/test" | 	"code.gitea.io/gitea/modules/test" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/unknwon/i18n" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func testCreateBranch(t testing.TB, session *TestSession, user, repo, oldRefSubURL, newBranchName string, expectedStatus int) string { | func testCreateBranch(t testing.TB, session *TestSession, user, repo, oldRefSubURL, newBranchName string, expectedStatus int) string { | ||||||
|   | |||||||
| @@ -11,9 +11,9 @@ import ( | |||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/unittest" | 	"code.gitea.io/gitea/models/unittest" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/unknwon/i18n" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func testLoginFailed(t *testing.T, username, password, message string) { | func testLoginFailed(t *testing.T, username, password, message string) { | ||||||
|   | |||||||
| @@ -13,9 +13,9 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/unittest" | 	"code.gitea.io/gitea/models/unittest" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/unknwon/i18n" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestSignup(t *testing.T) { | func TestSignup(t *testing.T) { | ||||||
| @@ -68,9 +68,9 @@ func TestSignupEmail(t *testing.T) { | |||||||
| 		wantStatus int | 		wantStatus int | ||||||
| 		wantMsg    string | 		wantMsg    string | ||||||
| 	}{ | 	}{ | ||||||
| 		{"exampleUser@example.com\r\n", http.StatusOK, i18n.Tr("en", "form.email_invalid", nil)}, | 		{"exampleUser@example.com\r\n", http.StatusOK, i18n.Tr("en", "form.email_invalid")}, | ||||||
| 		{"exampleUser@example.com\r", http.StatusOK, i18n.Tr("en", "form.email_invalid", nil)}, | 		{"exampleUser@example.com\r", http.StatusOK, i18n.Tr("en", "form.email_invalid")}, | ||||||
| 		{"exampleUser@example.com\n", http.StatusOK, i18n.Tr("en", "form.email_invalid", nil)}, | 		{"exampleUser@example.com\n", http.StatusOK, i18n.Tr("en", "form.email_invalid")}, | ||||||
| 		{"exampleUser@example.com", http.StatusSeeOther, ""}, | 		{"exampleUser@example.com", http.StatusSeeOther, ""}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,9 +11,9 @@ import ( | |||||||
| 	"code.gitea.io/gitea/models/unittest" | 	"code.gitea.io/gitea/models/unittest" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/test" | 	"code.gitea.io/gitea/modules/test" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/unknwon/i18n" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestViewUser(t *testing.T) { | func TestViewUser(t *testing.T) { | ||||||
|   | |||||||
| @@ -10,19 +10,19 @@ import ( | |||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/unknwon/paginater" | 	"code.gitea.io/gitea/modules/paginator" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Pagination provides a pagination via Paginater and additional configurations for the link params used in rendering | // Pagination provides a pagination via paginator.Paginator and additional configurations for the link params used in rendering | ||||||
| type Pagination struct { | type Pagination struct { | ||||||
| 	Paginater *paginater.Paginater | 	Paginater *paginator.Paginator | ||||||
| 	urlParams []string | 	urlParams []string | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewPagination creates a new instance of the Pagination struct | // NewPagination creates a new instance of the Pagination struct | ||||||
| func NewPagination(total, page, issueNum, numPages int) *Pagination { | func NewPagination(total, page, issueNum, numPages int) *Pagination { | ||||||
| 	p := &Pagination{} | 	p := &Pagination{} | ||||||
| 	p.Paginater = paginater.New(total, page, issueNum, numPages) | 	p.Paginater = paginator.New(total, page, issueNum, numPages) | ||||||
| 	return p | 	return p | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -53,5 +53,6 @@ func (p *Pagination) SetDefaultParams(ctx *Context) { | |||||||
| 	p.AddParam(ctx, "sort", "SortType") | 	p.AddParam(ctx, "sort", "SortType") | ||||||
| 	p.AddParam(ctx, "q", "Keyword") | 	p.AddParam(ctx, "q", "Keyword") | ||||||
| 	p.AddParam(ctx, "tab", "TabName") | 	p.AddParam(ctx, "tab", "TabName") | ||||||
|  | 	// do not add any more uncommon params here! | ||||||
| 	p.AddParam(ctx, "t", "queryType") | 	p.AddParam(ctx, "t", "queryType") | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,7 +8,8 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  |  | ||||||
| 	"github.com/unknwon/i18n" | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
|  |  | ||||||
| 	"github.com/yuin/goldmark/ast" | 	"github.com/yuin/goldmark/ast" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										203
									
								
								modules/paginator/paginator.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								modules/paginator/paginator.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,203 @@ | |||||||
|  | // Copyright 2022 The Gitea Authors. | ||||||
|  | // Copyright 2015 Unknwon. Licensed under the Apache License, Version 2.0 | ||||||
|  |  | ||||||
|  | package paginator | ||||||
|  |  | ||||||
|  | /* | ||||||
|  | In template: | ||||||
|  |  | ||||||
|  | ```html | ||||||
|  | {{if not .Page.IsFirst}}[First](1){{end}} | ||||||
|  | {{if .Page.HasPrevious}}[Previous]({{.Page.Previous}}){{end}} | ||||||
|  |  | ||||||
|  | {{range .Page.Pages}} | ||||||
|  | 	{{if eq .Num -1}} | ||||||
|  | 	... | ||||||
|  | 	{{else}} | ||||||
|  | 	{{.Num}}{{if .IsCurrent}}(current){{end}} | ||||||
|  | 	{{end}} | ||||||
|  | {{end}} | ||||||
|  |  | ||||||
|  | {{if .Page.HasNext}}[Next]({{.Page.Next}}){{end}} | ||||||
|  | {{if not .Page.IsLast}}[Last]({{.Page.TotalPages}}){{end}} | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Output: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | [First](1) [Previous](2) ... 2 3(current) 4 ... [Next](4) [Last](5) | ||||||
|  | ``` | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | // Paginator represents a set of results of pagination calculations. | ||||||
|  | type Paginator struct { | ||||||
|  | 	total     int // total rows count | ||||||
|  | 	pagingNum int // how many rows in one page | ||||||
|  | 	current   int // current page number | ||||||
|  | 	numPages  int // how many pages to show on the UI | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // New initialize a new pagination calculation and returns a Paginator as result. | ||||||
|  | func New(total, pagingNum, current, numPages int) *Paginator { | ||||||
|  | 	if pagingNum <= 0 { | ||||||
|  | 		pagingNum = 1 | ||||||
|  | 	} | ||||||
|  | 	if current <= 0 { | ||||||
|  | 		current = 1 | ||||||
|  | 	} | ||||||
|  | 	p := &Paginator{total, pagingNum, current, numPages} | ||||||
|  | 	if p.current > p.TotalPages() { | ||||||
|  | 		p.current = p.TotalPages() | ||||||
|  | 	} | ||||||
|  | 	return p | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsFirst returns true if current page is the first page. | ||||||
|  | func (p *Paginator) IsFirst() bool { | ||||||
|  | 	return p.current == 1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // HasPrevious returns true if there is a previous page relative to current page. | ||||||
|  | func (p *Paginator) HasPrevious() bool { | ||||||
|  | 	return p.current > 1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *Paginator) Previous() int { | ||||||
|  | 	if !p.HasPrevious() { | ||||||
|  | 		return p.current | ||||||
|  | 	} | ||||||
|  | 	return p.current - 1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // HasNext returns true if there is a next page relative to current page. | ||||||
|  | func (p *Paginator) HasNext() bool { | ||||||
|  | 	return p.total > p.current*p.pagingNum | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *Paginator) Next() int { | ||||||
|  | 	if !p.HasNext() { | ||||||
|  | 		return p.current | ||||||
|  | 	} | ||||||
|  | 	return p.current + 1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsLast returns true if current page is the last page. | ||||||
|  | func (p *Paginator) IsLast() bool { | ||||||
|  | 	if p.total == 0 { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  | 	return p.total > (p.current-1)*p.pagingNum && !p.HasNext() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Total returns number of total rows. | ||||||
|  | func (p *Paginator) Total() int { | ||||||
|  | 	return p.total | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TotalPages returns number of total pages. | ||||||
|  | func (p *Paginator) TotalPages() int { | ||||||
|  | 	if p.total == 0 { | ||||||
|  | 		return 1 | ||||||
|  | 	} | ||||||
|  | 	return (p.total + p.pagingNum - 1) / p.pagingNum | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Current returns current page number. | ||||||
|  | func (p *Paginator) Current() int { | ||||||
|  | 	return p.current | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // PagingNum returns number of page size. | ||||||
|  | func (p *Paginator) PagingNum() int { | ||||||
|  | 	return p.pagingNum | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Page presents a page in the paginator. | ||||||
|  | type Page struct { | ||||||
|  | 	num       int | ||||||
|  | 	isCurrent bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *Page) Num() int { | ||||||
|  | 	return p.num | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (p *Page) IsCurrent() bool { | ||||||
|  | 	return p.isCurrent | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getMiddleIdx(numPages int) int { | ||||||
|  | 	return (numPages + 1) / 2 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Pages returns a list of nearby page numbers relative to current page. | ||||||
|  | // If value is -1 means "..." that more pages are not showing. | ||||||
|  | func (p *Paginator) Pages() []*Page { | ||||||
|  | 	if p.numPages == 0 { | ||||||
|  | 		return []*Page{} | ||||||
|  | 	} else if p.numPages == 1 && p.TotalPages() == 1 { | ||||||
|  | 		// Only show current page. | ||||||
|  | 		return []*Page{{1, true}} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Total page number is less or equal. | ||||||
|  | 	if p.TotalPages() <= p.numPages { | ||||||
|  | 		pages := make([]*Page, p.TotalPages()) | ||||||
|  | 		for i := range pages { | ||||||
|  | 			pages[i] = &Page{i + 1, i+1 == p.current} | ||||||
|  | 		} | ||||||
|  | 		return pages | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	numPages := p.numPages | ||||||
|  | 	offsetIdx := 0 | ||||||
|  | 	hasMoreNext := false | ||||||
|  |  | ||||||
|  | 	// Check more previous and next pages. | ||||||
|  | 	previousNum := getMiddleIdx(p.numPages) - 1 | ||||||
|  | 	if previousNum > p.current-1 { | ||||||
|  | 		previousNum -= previousNum - (p.current - 1) | ||||||
|  | 	} | ||||||
|  | 	nextNum := p.numPages - previousNum - 1 | ||||||
|  | 	if p.current+nextNum > p.TotalPages() { | ||||||
|  | 		delta := nextNum - (p.TotalPages() - p.current) | ||||||
|  | 		nextNum -= delta | ||||||
|  | 		previousNum += delta | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	offsetVal := p.current - previousNum | ||||||
|  | 	if offsetVal > 1 { | ||||||
|  | 		numPages++ | ||||||
|  | 		offsetIdx = 1 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if p.current+nextNum < p.TotalPages() { | ||||||
|  | 		numPages++ | ||||||
|  | 		hasMoreNext = true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pages := make([]*Page, numPages) | ||||||
|  |  | ||||||
|  | 	// There are more previous pages. | ||||||
|  | 	if offsetIdx == 1 { | ||||||
|  | 		pages[0] = &Page{-1, false} | ||||||
|  | 	} | ||||||
|  | 	// There are more next pages. | ||||||
|  | 	if hasMoreNext { | ||||||
|  | 		pages[len(pages)-1] = &Page{-1, false} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check previous pages. | ||||||
|  | 	for i := 0; i < previousNum; i++ { | ||||||
|  | 		pages[offsetIdx+i] = &Page{i + offsetVal, false} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pages[offsetIdx+previousNum] = &Page{p.current, true} | ||||||
|  |  | ||||||
|  | 	// Check next pages. | ||||||
|  | 	for i := 1; i <= nextNum; i++ { | ||||||
|  | 		pages[offsetIdx+previousNum+i] = &Page{p.current + i, false} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return pages | ||||||
|  | } | ||||||
							
								
								
									
										311
									
								
								modules/paginator/paginator_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										311
									
								
								modules/paginator/paginator_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,311 @@ | |||||||
|  | // Copyright 2022 The Gitea Authors. | ||||||
|  | // Copyright 2015 Unknwon. Licensed under the Apache License, Version 2.0 | ||||||
|  |  | ||||||
|  | package paginator | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestPaginator(t *testing.T) { | ||||||
|  | 	t.Run("Basic logics", func(t *testing.T) { | ||||||
|  | 		p := New(0, -1, -1, 0) | ||||||
|  | 		assert.Equal(t, 1, p.PagingNum()) | ||||||
|  | 		assert.True(t, p.IsFirst()) | ||||||
|  | 		assert.False(t, p.HasPrevious()) | ||||||
|  | 		assert.Equal(t, 1, p.Previous()) | ||||||
|  | 		assert.False(t, p.HasNext()) | ||||||
|  | 		assert.Equal(t, 1, p.Next()) | ||||||
|  | 		assert.True(t, p.IsLast()) | ||||||
|  | 		assert.Equal(t, 0, p.Total()) | ||||||
|  |  | ||||||
|  | 		p = New(1, 10, 2, 0) | ||||||
|  | 		assert.Equal(t, 10, p.PagingNum()) | ||||||
|  | 		assert.True(t, p.IsFirst()) | ||||||
|  | 		assert.False(t, p.HasPrevious()) | ||||||
|  | 		assert.False(t, p.HasNext()) | ||||||
|  | 		assert.True(t, p.IsLast()) | ||||||
|  |  | ||||||
|  | 		p = New(10, 10, 1, 0) | ||||||
|  | 		assert.Equal(t, 10, p.PagingNum()) | ||||||
|  | 		assert.True(t, p.IsFirst()) | ||||||
|  | 		assert.False(t, p.HasPrevious()) | ||||||
|  | 		assert.False(t, p.HasNext()) | ||||||
|  | 		assert.True(t, p.IsLast()) | ||||||
|  |  | ||||||
|  | 		p = New(11, 10, 1, 0) | ||||||
|  | 		assert.Equal(t, 10, p.PagingNum()) | ||||||
|  | 		assert.True(t, p.IsFirst()) | ||||||
|  | 		assert.False(t, p.HasPrevious()) | ||||||
|  | 		assert.True(t, p.HasNext()) | ||||||
|  | 		assert.Equal(t, 2, p.Next()) | ||||||
|  | 		assert.False(t, p.IsLast()) | ||||||
|  |  | ||||||
|  | 		p = New(11, 10, 2, 0) | ||||||
|  | 		assert.Equal(t, 10, p.PagingNum()) | ||||||
|  | 		assert.False(t, p.IsFirst()) | ||||||
|  | 		assert.True(t, p.HasPrevious()) | ||||||
|  | 		assert.Equal(t, 1, p.Previous()) | ||||||
|  | 		assert.False(t, p.HasNext()) | ||||||
|  | 		assert.True(t, p.IsLast()) | ||||||
|  |  | ||||||
|  | 		p = New(20, 10, 2, 0) | ||||||
|  | 		assert.Equal(t, 10, p.PagingNum()) | ||||||
|  | 		assert.False(t, p.IsFirst()) | ||||||
|  | 		assert.True(t, p.HasPrevious()) | ||||||
|  | 		assert.False(t, p.HasNext()) | ||||||
|  | 		assert.True(t, p.IsLast()) | ||||||
|  |  | ||||||
|  | 		p = New(25, 10, 2, 0) | ||||||
|  | 		assert.Equal(t, 10, p.PagingNum()) | ||||||
|  | 		assert.False(t, p.IsFirst()) | ||||||
|  | 		assert.True(t, p.HasPrevious()) | ||||||
|  | 		assert.True(t, p.HasNext()) | ||||||
|  | 		assert.False(t, p.IsLast()) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("Generate pages", func(t *testing.T) { | ||||||
|  | 		p := New(0, 10, 1, 0) | ||||||
|  | 		pages := p.Pages() | ||||||
|  | 		assert.Equal(t, 0, len(pages)) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("Only current page", func(t *testing.T) { | ||||||
|  | 		p := New(0, 10, 1, 1) | ||||||
|  | 		pages := p.Pages() | ||||||
|  | 		assert.Equal(t, 1, len(pages)) | ||||||
|  | 		assert.Equal(t, 1, pages[0].Num()) | ||||||
|  | 		assert.True(t, pages[0].IsCurrent()) | ||||||
|  |  | ||||||
|  | 		p = New(1, 10, 1, 1) | ||||||
|  | 		pages = p.Pages() | ||||||
|  | 		assert.Equal(t, 1, len(pages)) | ||||||
|  | 		assert.Equal(t, 1, pages[0].Num()) | ||||||
|  | 		assert.True(t, pages[0].IsCurrent()) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("Total page number is less or equal", func(t *testing.T) { | ||||||
|  | 		p := New(1, 10, 1, 2) | ||||||
|  | 		pages := p.Pages() | ||||||
|  | 		assert.Equal(t, 1, len(pages)) | ||||||
|  | 		assert.Equal(t, 1, pages[0].Num()) | ||||||
|  | 		assert.True(t, pages[0].IsCurrent()) | ||||||
|  |  | ||||||
|  | 		p = New(11, 10, 1, 2) | ||||||
|  | 		pages = p.Pages() | ||||||
|  | 		assert.Equal(t, 2, len(pages)) | ||||||
|  | 		assert.Equal(t, 1, pages[0].Num()) | ||||||
|  | 		assert.True(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 2, pages[1].Num()) | ||||||
|  | 		assert.False(t, pages[1].IsCurrent()) | ||||||
|  |  | ||||||
|  | 		p = New(11, 10, 2, 2) | ||||||
|  | 		pages = p.Pages() | ||||||
|  | 		assert.Equal(t, 2, len(pages)) | ||||||
|  | 		assert.Equal(t, 1, pages[0].Num()) | ||||||
|  | 		assert.False(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 2, pages[1].Num()) | ||||||
|  | 		assert.True(t, pages[1].IsCurrent()) | ||||||
|  |  | ||||||
|  | 		p = New(25, 10, 2, 3) | ||||||
|  | 		pages = p.Pages() | ||||||
|  | 		assert.Equal(t, 3, len(pages)) | ||||||
|  | 		assert.Equal(t, 1, pages[0].Num()) | ||||||
|  | 		assert.False(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 2, pages[1].Num()) | ||||||
|  | 		assert.True(t, pages[1].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 3, pages[2].Num()) | ||||||
|  | 		assert.False(t, pages[2].IsCurrent()) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("Has more previous pages ", func(t *testing.T) { | ||||||
|  | 		// ... 2 | ||||||
|  | 		p := New(11, 10, 2, 1) | ||||||
|  | 		pages := p.Pages() | ||||||
|  | 		assert.Equal(t, 2, len(pages)) | ||||||
|  | 		assert.Equal(t, -1, pages[0].Num()) | ||||||
|  | 		assert.False(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 2, pages[1].Num()) | ||||||
|  | 		assert.True(t, pages[1].IsCurrent()) | ||||||
|  |  | ||||||
|  | 		// ... 2 3 | ||||||
|  | 		p = New(21, 10, 2, 2) | ||||||
|  | 		pages = p.Pages() | ||||||
|  | 		assert.Equal(t, 3, len(pages)) | ||||||
|  | 		assert.Equal(t, -1, pages[0].Num()) | ||||||
|  | 		assert.False(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 2, pages[1].Num()) | ||||||
|  | 		assert.True(t, pages[1].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 3, pages[2].Num()) | ||||||
|  | 		assert.False(t, pages[2].IsCurrent()) | ||||||
|  |  | ||||||
|  | 		// ... 2 3 4 | ||||||
|  | 		p = New(31, 10, 3, 3) | ||||||
|  | 		pages = p.Pages() | ||||||
|  | 		assert.Equal(t, 4, len(pages)) | ||||||
|  | 		assert.Equal(t, -1, pages[0].Num()) | ||||||
|  | 		assert.False(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 2, pages[1].Num()) | ||||||
|  | 		assert.False(t, pages[1].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 3, pages[2].Num()) | ||||||
|  | 		assert.True(t, pages[2].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 4, pages[3].Num()) | ||||||
|  | 		assert.False(t, pages[3].IsCurrent()) | ||||||
|  |  | ||||||
|  | 		// ... 3 4 5 | ||||||
|  | 		p = New(41, 10, 4, 3) | ||||||
|  | 		pages = p.Pages() | ||||||
|  | 		assert.Equal(t, 4, len(pages)) | ||||||
|  | 		assert.Equal(t, -1, pages[0].Num()) | ||||||
|  | 		assert.False(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 3, pages[1].Num()) | ||||||
|  | 		assert.False(t, pages[1].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 4, pages[2].Num()) | ||||||
|  | 		assert.True(t, pages[2].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 5, pages[3].Num()) | ||||||
|  | 		assert.False(t, pages[3].IsCurrent()) | ||||||
|  |  | ||||||
|  | 		// ... 4 5 6 7 8 9 10 | ||||||
|  | 		p = New(100, 10, 9, 7) | ||||||
|  | 		pages = p.Pages() | ||||||
|  | 		assert.Equal(t, 8, len(pages)) | ||||||
|  | 		assert.Equal(t, -1, pages[0].Num()) | ||||||
|  | 		assert.False(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 4, pages[1].Num()) | ||||||
|  | 		assert.False(t, pages[1].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 5, pages[2].Num()) | ||||||
|  | 		assert.False(t, pages[2].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 6, pages[3].Num()) | ||||||
|  | 		assert.False(t, pages[3].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 7, pages[4].Num()) | ||||||
|  | 		assert.False(t, pages[4].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 8, pages[5].Num()) | ||||||
|  | 		assert.False(t, pages[5].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 9, pages[6].Num()) | ||||||
|  | 		assert.True(t, pages[6].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 10, pages[7].Num()) | ||||||
|  | 		assert.False(t, pages[7].IsCurrent()) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("Has more next pages", func(t *testing.T) { | ||||||
|  | 		// 1 ... | ||||||
|  | 		p := New(21, 10, 1, 1) | ||||||
|  | 		pages := p.Pages() | ||||||
|  | 		assert.Equal(t, 2, len(pages)) | ||||||
|  | 		assert.Equal(t, 1, pages[0].Num()) | ||||||
|  | 		assert.True(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, -1, pages[1].Num()) | ||||||
|  | 		assert.False(t, pages[1].IsCurrent()) | ||||||
|  |  | ||||||
|  | 		// 1 2 ... | ||||||
|  | 		p = New(21, 10, 1, 2) | ||||||
|  | 		pages = p.Pages() | ||||||
|  | 		assert.Equal(t, 3, len(pages)) | ||||||
|  | 		assert.Equal(t, 1, pages[0].Num()) | ||||||
|  | 		assert.True(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 2, pages[1].Num()) | ||||||
|  | 		assert.False(t, pages[1].IsCurrent()) | ||||||
|  | 		assert.Equal(t, -1, pages[2].Num()) | ||||||
|  | 		assert.False(t, pages[2].IsCurrent()) | ||||||
|  |  | ||||||
|  | 		// 1 2 3 ... | ||||||
|  | 		p = New(31, 10, 2, 3) | ||||||
|  | 		pages = p.Pages() | ||||||
|  | 		assert.Equal(t, 4, len(pages)) | ||||||
|  | 		assert.Equal(t, 1, pages[0].Num()) | ||||||
|  | 		assert.False(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 2, pages[1].Num()) | ||||||
|  | 		assert.True(t, pages[1].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 3, pages[2].Num()) | ||||||
|  | 		assert.False(t, pages[2].IsCurrent()) | ||||||
|  | 		assert.Equal(t, -1, pages[3].Num()) | ||||||
|  | 		assert.False(t, pages[3].IsCurrent()) | ||||||
|  |  | ||||||
|  | 		// 1 2 3 ... | ||||||
|  | 		p = New(41, 10, 2, 3) | ||||||
|  | 		pages = p.Pages() | ||||||
|  | 		assert.Equal(t, 4, len(pages)) | ||||||
|  | 		assert.Equal(t, 1, pages[0].Num()) | ||||||
|  | 		assert.False(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 2, pages[1].Num()) | ||||||
|  | 		assert.True(t, pages[1].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 3, pages[2].Num()) | ||||||
|  | 		assert.False(t, pages[2].IsCurrent()) | ||||||
|  | 		assert.Equal(t, -1, pages[3].Num()) | ||||||
|  | 		assert.False(t, pages[3].IsCurrent()) | ||||||
|  |  | ||||||
|  | 		// 1 2 3 4 5 6 7 ... | ||||||
|  | 		p = New(100, 10, 1, 7) | ||||||
|  | 		pages = p.Pages() | ||||||
|  | 		assert.Equal(t, 8, len(pages)) | ||||||
|  | 		assert.Equal(t, 1, pages[0].Num()) | ||||||
|  | 		assert.True(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 2, pages[1].Num()) | ||||||
|  | 		assert.False(t, pages[1].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 3, pages[2].Num()) | ||||||
|  | 		assert.False(t, pages[2].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 4, pages[3].Num()) | ||||||
|  | 		assert.False(t, pages[3].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 5, pages[4].Num()) | ||||||
|  | 		assert.False(t, pages[4].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 6, pages[5].Num()) | ||||||
|  | 		assert.False(t, pages[5].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 7, pages[6].Num()) | ||||||
|  | 		assert.False(t, pages[6].IsCurrent()) | ||||||
|  | 		assert.Equal(t, -1, pages[7].Num()) | ||||||
|  | 		assert.False(t, pages[7].IsCurrent()) | ||||||
|  |  | ||||||
|  | 		// 1 2 3 4 5 6 7 ... | ||||||
|  | 		p = New(100, 10, 2, 7) | ||||||
|  | 		pages = p.Pages() | ||||||
|  | 		assert.Equal(t, 8, len(pages)) | ||||||
|  | 		assert.Equal(t, 1, pages[0].Num()) | ||||||
|  | 		assert.False(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 2, pages[1].Num()) | ||||||
|  | 		assert.True(t, pages[1].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 3, pages[2].Num()) | ||||||
|  | 		assert.False(t, pages[2].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 4, pages[3].Num()) | ||||||
|  | 		assert.False(t, pages[3].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 5, pages[4].Num()) | ||||||
|  | 		assert.False(t, pages[4].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 6, pages[5].Num()) | ||||||
|  | 		assert.False(t, pages[5].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 7, pages[6].Num()) | ||||||
|  | 		assert.False(t, pages[6].IsCurrent()) | ||||||
|  | 		assert.Equal(t, -1, pages[7].Num()) | ||||||
|  | 		assert.False(t, pages[7].IsCurrent()) | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("Has both more previous and next pages", func(t *testing.T) { | ||||||
|  | 		// ... 2 3 ... | ||||||
|  | 		p := New(35, 10, 2, 2) | ||||||
|  | 		pages := p.Pages() | ||||||
|  | 		assert.Equal(t, 4, len(pages)) | ||||||
|  | 		assert.Equal(t, -1, pages[0].Num()) | ||||||
|  | 		assert.False(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 2, pages[1].Num()) | ||||||
|  | 		assert.True(t, pages[1].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 3, pages[2].Num()) | ||||||
|  | 		assert.False(t, pages[2].IsCurrent()) | ||||||
|  | 		assert.Equal(t, -1, pages[3].Num()) | ||||||
|  | 		assert.False(t, pages[3].IsCurrent()) | ||||||
|  |  | ||||||
|  | 		// ... 2 3 4 ... | ||||||
|  | 		p = New(49, 10, 3, 3) | ||||||
|  | 		pages = p.Pages() | ||||||
|  | 		assert.Equal(t, 5, len(pages)) | ||||||
|  | 		assert.Equal(t, -1, pages[0].Num()) | ||||||
|  | 		assert.False(t, pages[0].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 2, pages[1].Num()) | ||||||
|  | 		assert.False(t, pages[1].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 3, pages[2].Num()) | ||||||
|  | 		assert.True(t, pages[2].IsCurrent()) | ||||||
|  | 		assert.Equal(t, 4, pages[3].Num()) | ||||||
|  | 		assert.False(t, pages[3].IsCurrent()) | ||||||
|  | 		assert.Equal(t, -1, pages[4].Num()) | ||||||
|  | 		assert.False(t, pages[4].IsCurrent()) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
| @@ -12,8 +12,7 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
| 	"github.com/unknwon/i18n" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Seconds-based time units | // Seconds-based time units | ||||||
|   | |||||||
| @@ -12,9 +12,9 @@ import ( | |||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/translation" | 	"code.gitea.io/gitea/modules/translation" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/unknwon/i18n" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var BaseDate time.Time | var BaseDate time.Time | ||||||
|   | |||||||
							
								
								
									
										143
									
								
								modules/translation/i18n/i18n.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								modules/translation/i18n/i18n.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,143 @@ | |||||||
|  | // Copyright 2022 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 i18n | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"reflect" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
|  |  | ||||||
|  | 	"gopkg.in/ini.v1" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	ErrLocaleAlreadyExist = errors.New("lang already exists") | ||||||
|  |  | ||||||
|  | 	DefaultLocales = NewLocaleStore() | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type locale struct { | ||||||
|  | 	store    *LocaleStore | ||||||
|  | 	langName string | ||||||
|  | 	langDesc string | ||||||
|  | 	messages *ini.File | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type LocaleStore struct { | ||||||
|  | 	// at the moment, all these fields are readonly after initialization | ||||||
|  | 	langNames   []string | ||||||
|  | 	langDescs   []string | ||||||
|  | 	localeMap   map[string]*locale | ||||||
|  | 	defaultLang string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewLocaleStore() *LocaleStore { | ||||||
|  | 	return &LocaleStore{localeMap: make(map[string]*locale)} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AddLocaleByIni adds locale by ini into the store | ||||||
|  | func (ls *LocaleStore) AddLocaleByIni(langName, langDesc string, localeFile interface{}, otherLocaleFiles ...interface{}) error { | ||||||
|  | 	if _, ok := ls.localeMap[langName]; ok { | ||||||
|  | 		return ErrLocaleAlreadyExist | ||||||
|  | 	} | ||||||
|  | 	iniFile, err := ini.LoadSources(ini.LoadOptions{ | ||||||
|  | 		IgnoreInlineComment:         true, | ||||||
|  | 		UnescapeValueCommentSymbols: true, | ||||||
|  | 	}, localeFile, otherLocaleFiles...) | ||||||
|  | 	if err == nil { | ||||||
|  | 		iniFile.BlockMode = false | ||||||
|  | 		lc := &locale{store: ls, langName: langName, langDesc: langDesc, messages: iniFile} | ||||||
|  | 		ls.langNames = append(ls.langNames, lc.langName) | ||||||
|  | 		ls.langDescs = append(ls.langDescs, lc.langDesc) | ||||||
|  | 		ls.localeMap[lc.langName] = lc | ||||||
|  | 	} | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (ls *LocaleStore) HasLang(langName string) bool { | ||||||
|  | 	_, ok := ls.localeMap[langName] | ||||||
|  | 	return ok | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (ls *LocaleStore) ListLangNameDesc() (names, desc []string) { | ||||||
|  | 	return ls.langNames, ls.langDescs | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetDefaultLang sets default language as a fallback | ||||||
|  | func (ls *LocaleStore) SetDefaultLang(lang string) { | ||||||
|  | 	ls.defaultLang = lang | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Tr translates content to target language. fall back to default language. | ||||||
|  | func (ls *LocaleStore) Tr(lang, trKey string, trArgs ...interface{}) string { | ||||||
|  | 	l, ok := ls.localeMap[lang] | ||||||
|  | 	if !ok { | ||||||
|  | 		l, ok = ls.localeMap[ls.defaultLang] | ||||||
|  | 	} | ||||||
|  | 	if ok { | ||||||
|  | 		return l.Tr(trKey, trArgs...) | ||||||
|  | 	} | ||||||
|  | 	return trKey | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Tr translates content to locale language. fall back to default language. | ||||||
|  | func (l *locale) Tr(trKey string, trArgs ...interface{}) string { | ||||||
|  | 	var section string | ||||||
|  |  | ||||||
|  | 	idx := strings.IndexByte(trKey, '.') | ||||||
|  | 	if idx > 0 { | ||||||
|  | 		section = trKey[:idx] | ||||||
|  | 		trKey = trKey[idx+1:] | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	trMsg := trKey | ||||||
|  | 	if trIni, err := l.messages.Section(section).GetKey(trKey); err == nil { | ||||||
|  | 		trMsg = trIni.Value() | ||||||
|  | 	} else if l.store.defaultLang != "" && l.langName != l.store.defaultLang { | ||||||
|  | 		// try to fall back to default | ||||||
|  | 		if defaultLocale, ok := l.store.localeMap[l.store.defaultLang]; ok { | ||||||
|  | 			if trIni, err = defaultLocale.messages.Section(section).GetKey(trKey); err == nil { | ||||||
|  | 				trMsg = trIni.Value() | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(trArgs) > 0 { | ||||||
|  | 		fmtArgs := make([]interface{}, 0, len(trArgs)) | ||||||
|  | 		for _, arg := range trArgs { | ||||||
|  | 			val := reflect.ValueOf(arg) | ||||||
|  | 			if val.Kind() == reflect.Slice { | ||||||
|  | 				// before, it can accept Tr(lang, key, a, [b, c], d, [e, f]) as Sprintf(msg, a, b, c, d, e, f), it's an unstable behavior | ||||||
|  | 				// now, we restrict the strange behavior and only support: | ||||||
|  | 				// 1. Tr(lang, key, [slice-items]) as Sprintf(msg, items...) | ||||||
|  | 				// 2. Tr(lang, key, args...) as Sprintf(msg, args...) | ||||||
|  | 				if len(trArgs) == 1 { | ||||||
|  | 					for i := 0; i < val.Len(); i++ { | ||||||
|  | 						fmtArgs = append(fmtArgs, val.Index(i).Interface()) | ||||||
|  | 					} | ||||||
|  | 				} else { | ||||||
|  | 					log.Error("the args for i18n shouldn't contain uncertain slices, key=%q, args=%v", trKey, trArgs) | ||||||
|  | 					break | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				fmtArgs = append(fmtArgs, arg) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return fmt.Sprintf(trMsg, fmtArgs...) | ||||||
|  | 	} | ||||||
|  | 	return trMsg | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ResetDefaultLocales() { | ||||||
|  | 	DefaultLocales = NewLocaleStore() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Tr use default locales to translate content to target language. | ||||||
|  | func Tr(lang, trKey string, trArgs ...interface{}) string { | ||||||
|  | 	return DefaultLocales.Tr(lang, trKey, trArgs...) | ||||||
|  | } | ||||||
							
								
								
									
										56
									
								
								modules/translation/i18n/i18n_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								modules/translation/i18n/i18n_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | // Copyright 2022 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 i18n | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func Test_Tr(t *testing.T) { | ||||||
|  | 	testData1 := []byte(` | ||||||
|  | .dot.name = Dot Name | ||||||
|  | fmt = %[1]s %[2]s | ||||||
|  |  | ||||||
|  | [section] | ||||||
|  | sub = Sub String | ||||||
|  | mixed = test value; <span style="color: red\; background: none;">more text</span> | ||||||
|  | `) | ||||||
|  |  | ||||||
|  | 	testData2 := []byte(` | ||||||
|  | fmt = %[2]s %[1]s | ||||||
|  |  | ||||||
|  | [section] | ||||||
|  | sub = Changed Sub String | ||||||
|  | `) | ||||||
|  |  | ||||||
|  | 	ls := NewLocaleStore() | ||||||
|  | 	assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1)) | ||||||
|  | 	assert.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2)) | ||||||
|  | 	ls.SetDefaultLang("lang1") | ||||||
|  |  | ||||||
|  | 	result := ls.Tr("lang1", "fmt", "a", "b") | ||||||
|  | 	assert.Equal(t, "a b", result) | ||||||
|  |  | ||||||
|  | 	result = ls.Tr("lang2", "fmt", "a", "b") | ||||||
|  | 	assert.Equal(t, "b a", result) | ||||||
|  |  | ||||||
|  | 	result = ls.Tr("lang1", "section.sub") | ||||||
|  | 	assert.Equal(t, "Sub String", result) | ||||||
|  |  | ||||||
|  | 	result = ls.Tr("lang2", "section.sub") | ||||||
|  | 	assert.Equal(t, "Changed Sub String", result) | ||||||
|  |  | ||||||
|  | 	result = ls.Tr("", ".dot.name") | ||||||
|  | 	assert.Equal(t, "Dot Name", result) | ||||||
|  |  | ||||||
|  | 	result = ls.Tr("lang2", "section.mixed") | ||||||
|  | 	assert.Equal(t, `test value; <span style="color: red; background: none;">more text</span>`, result) | ||||||
|  |  | ||||||
|  | 	langs, descs := ls.ListLangNameDesc() | ||||||
|  | 	assert.Equal(t, []string{"lang1", "lang2"}, langs) | ||||||
|  | 	assert.Equal(t, []string{"Lang1", "Lang2"}, descs) | ||||||
|  | } | ||||||
| @@ -11,8 +11,8 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/options" | 	"code.gitea.io/gitea/modules/options" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
|  |  | ||||||
| 	"github.com/unknwon/i18n" |  | ||||||
| 	"golang.org/x/text/language" | 	"golang.org/x/text/language" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -54,13 +54,13 @@ func TryTr(lang, format string, args ...interface{}) (string, bool) { | |||||||
|  |  | ||||||
| // InitLocales loads the locales | // InitLocales loads the locales | ||||||
| func InitLocales() { | func InitLocales() { | ||||||
| 	i18n.Reset() | 	i18n.ResetDefaultLocales() | ||||||
| 	localeNames, err := options.Dir("locale") | 	localeNames, err := options.Dir("locale") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal("Failed to list locale files: %v", err) | 		log.Fatal("Failed to list locale files: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	localFiles := make(map[string][]byte) | 	localFiles := make(map[string][]byte, len(localeNames)) | ||||||
| 	for _, name := range localeNames { | 	for _, name := range localeNames { | ||||||
| 		localFiles[name], err = options.Locale(name) | 		localFiles[name], err = options.Locale(name) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @@ -76,16 +76,21 @@ func InitLocales() { | |||||||
| 	matcher = language.NewMatcher(supportedTags) | 	matcher = language.NewMatcher(supportedTags) | ||||||
| 	for i := range setting.Names { | 	for i := range setting.Names { | ||||||
| 		key := "locale_" + setting.Langs[i] + ".ini" | 		key := "locale_" + setting.Langs[i] + ".ini" | ||||||
| 		if err = i18n.SetMessageWithDesc(setting.Langs[i], setting.Names[i], localFiles[key]); err != nil { | 		if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], localFiles[key]); err != nil { | ||||||
| 			log.Error("Failed to set messages to %s: %v", setting.Langs[i], err) | 			log.Error("Failed to set messages to %s: %v", setting.Langs[i], err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	i18n.SetDefaultLang("en-US") | 	if len(setting.Langs) != 0 { | ||||||
|  | 		defaultLangName := setting.Langs[0] | ||||||
|  | 		if defaultLangName != "en-US" { | ||||||
|  | 			log.Info("Use the first locale (%s) in LANGS setting option as default", defaultLangName) | ||||||
|  | 		} | ||||||
|  | 		i18n.DefaultLocales.SetDefaultLang(defaultLangName) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	allLangs = make([]*LangType, 0, i18n.Count()) | 	langs, descs := i18n.DefaultLocales.ListLangNameDesc() | ||||||
|  | 	allLangs = make([]*LangType, 0, len(langs)) | ||||||
| 	allLangMap = map[string]*LangType{} | 	allLangMap = map[string]*LangType{} | ||||||
| 	langs := i18n.ListLangs() |  | ||||||
| 	descs := i18n.ListLangDescs() |  | ||||||
| 	for i, v := range langs { | 	for i, v := range langs { | ||||||
| 		l := &LangType{v, descs[i]} | 		l := &LangType{v, descs[i]} | ||||||
| 		allLangs = append(allLangs, l) | 		allLangs = append(allLangs, l) | ||||||
|   | |||||||
| @@ -9,8 +9,8 @@ import ( | |||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/translation" | 	"code.gitea.io/gitea/modules/translation" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
|  |  | ||||||
| 	"github.com/unknwon/i18n" |  | ||||||
| 	"golang.org/x/text/language" | 	"golang.org/x/text/language" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -28,8 +28,8 @@ func Locale(resp http.ResponseWriter, req *http.Request) translation.Locale { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check again in case someone modify by purpose. | 	// Check again in case someone changes the supported language list. | ||||||
| 	if lang != "" && !i18n.IsExist(lang) { | 	if lang != "" && !i18n.DefaultLocales.HasLang(lang) { | ||||||
| 		lang = "" | 		lang = "" | ||||||
| 		changeLang = false | 		changeLang = false | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -18,9 +18,9 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/timeutil" | 	"code.gitea.io/gitea/modules/timeutil" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
|  |  | ||||||
| 	"github.com/sergi/go-diff/diffmatchpatch" | 	"github.com/sergi/go-diff/diffmatchpatch" | ||||||
| 	"github.com/unknwon/i18n" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // GetContentHistoryOverview get overview | // GetContentHistoryOverview get overview | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
| 	"code.gitea.io/gitea/modules/typesniffer" | 	"code.gitea.io/gitea/modules/typesniffer" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
| @@ -31,8 +32,6 @@ import ( | |||||||
| 	"code.gitea.io/gitea/services/agit" | 	"code.gitea.io/gitea/services/agit" | ||||||
| 	"code.gitea.io/gitea/services/forms" | 	"code.gitea.io/gitea/services/forms" | ||||||
| 	user_service "code.gitea.io/gitea/services/user" | 	user_service "code.gitea.io/gitea/services/user" | ||||||
|  |  | ||||||
| 	"github.com/unknwon/i18n" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ package cron | |||||||
| import ( | import ( | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/unknwon/i18n" | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Config represents a basic configuration interface that cron task | // Config represents a basic configuration interface that cron task | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user