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:
wxiaoguang 2022-04-03 17:46:48 +08:00 committed by GitHub
parent 27c34dd011
commit d242511e86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 758 additions and 42 deletions

View File

@ -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,മലയാളം

View File

@ -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**

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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 {

View File

@ -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) {

View File

@ -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 {

View File

@ -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) {

View File

@ -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 {

View File

@ -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) {

View File

@ -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, ""},
} }

View File

@ -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) {

View File

@ -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")
} }

View File

@ -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"
) )

View 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
}

View 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())
})
}

View File

@ -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

View File

@ -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

View 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...)
}

View 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)
}

View File

@ -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)

View File

@ -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
} }

View File

@ -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

View File

@ -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 (

View File

@ -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