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.
tokarchuk/v1.17
wxiaoguang 3 years ago committed by GitHub
parent 27c34dd011
commit d242511e86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      custom/conf/app.example.ini
  2. 3
      docs/content/doc/advanced/config-cheat-sheet.en-us.md
  3. 2
      docs/content/doc/advanced/customizing-gitea.en-us.md
  4. 2
      go.mod
  5. 5
      go.sum
  6. 2
      integrations/auth_ldap_test.go
  7. 3
      integrations/branches_test.go
  8. 2
      integrations/pull_merge_test.go
  9. 2
      integrations/release_test.go
  10. 2
      integrations/repo_branch_test.go
  11. 2
      integrations/signin_test.go
  12. 8
      integrations/signup_test.go
  13. 2
      integrations/user_test.go
  14. 9
      modules/context/pagination.go
  15. 3
      modules/markup/markdown/toc.go
  16. 203
      modules/paginator/paginator.go
  17. 311
      modules/paginator/paginator_test.go
  18. 3
      modules/timeutil/since.go
  19. 2
      modules/timeutil/since_test.go
  20. 143
      modules/translation/i18n/i18n.go
  21. 56
      modules/translation/i18n/i18n_test.go
  22. 21
      modules/translation/translation.go
  23. 6
      modules/web/middleware/locale.go
  24. 2
      routers/web/repo/issue_content_history.go
  25. 3
      routers/web/user/setting/profile.go
  26. 2
      services/cron/setting.go

@ -2117,6 +2117,7 @@ PATH =
;[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
;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`)
- `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
## U2F (`U2F`) **DEPRECATED**

@ -299,6 +299,8 @@ LANGS = en-US,foo-BAR
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.
### Readmes

@ -78,8 +78,6 @@ require (
github.com/stretchr/testify v1.7.0
github.com/syndtr/goleveldb 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/urfave/cli v1.22.5
github.com/xanzy/go-gitlab v0.58.0

@ -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 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/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/go.mod h1:cK4RSTTVdND5j9EYEc0LAMOvdG11JeiKjyjfyZRvV2w=
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/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.46.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.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=

@ -16,10 +16,10 @@ import (
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/translation/i18n"
"code.gitea.io/gitea/services/auth"
"github.com/stretchr/testify/assert"
"github.com/unknwon/i18n"
)
type ldapUser struct {

@ -9,8 +9,9 @@ import (
"net/url"
"testing"
"code.gitea.io/gitea/modules/translation/i18n"
"github.com/stretchr/testify/assert"
"github.com/unknwon/i18n"
)
func TestViewBranches(t *testing.T) {

@ -24,10 +24,10 @@ import (
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/translation/i18n"
"code.gitea.io/gitea/services/pull"
"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 {

@ -14,10 +14,10 @@ import (
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/translation/i18n"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/unknwon/i18n"
)
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/test"
"code.gitea.io/gitea/modules/translation/i18n"
"github.com/stretchr/testify/assert"
"github.com/unknwon/i18n"
)
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"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/translation/i18n"
"github.com/stretchr/testify/assert"
"github.com/unknwon/i18n"
)
func testLoginFailed(t *testing.T, username, password, message string) {

@ -13,9 +13,9 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation/i18n"
"github.com/stretchr/testify/assert"
"github.com/unknwon/i18n"
)
func TestSignup(t *testing.T) {
@ -68,9 +68,9 @@ func TestSignupEmail(t *testing.T) {
wantStatus int
wantMsg string
}{
{"exampleUser@example.com\r\n", http.StatusOK, i18n.Tr("en", "form.email_invalid", nil)},
{"exampleUser@example.com\r", http.StatusOK, i18n.Tr("en", "form.email_invalid", nil)},
{"exampleUser@example.com\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")},
{"exampleUser@example.com\n", http.StatusOK, i18n.Tr("en", "form.email_invalid")},
{"exampleUser@example.com", http.StatusSeeOther, ""},
}

@ -11,9 +11,9 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/translation/i18n"
"github.com/stretchr/testify/assert"
"github.com/unknwon/i18n"
)
func TestViewUser(t *testing.T) {

@ -10,19 +10,19 @@ import (
"net/url"
"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 {
Paginater *paginater.Paginater
Paginater *paginator.Paginator
urlParams []string
}
// NewPagination creates a new instance of the Pagination struct
func NewPagination(total, page, issueNum, numPages int) *Pagination {
p := &Pagination{}
p.Paginater = paginater.New(total, page, issueNum, numPages)
p.Paginater = paginator.New(total, page, issueNum, numPages)
return p
}
@ -53,5 +53,6 @@ func (p *Pagination) SetDefaultParams(ctx *Context) {
p.AddParam(ctx, "sort", "SortType")
p.AddParam(ctx, "q", "Keyword")
p.AddParam(ctx, "tab", "TabName")
// do not add any more uncommon params here!
p.AddParam(ctx, "t", "queryType")
}

@ -8,7 +8,8 @@ import (
"fmt"
"net/url"
"github.com/unknwon/i18n"
"code.gitea.io/gitea/modules/translation/i18n"
"github.com/yuin/goldmark/ast"
)

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

@ -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"
"code.gitea.io/gitea/modules/setting"
"github.com/unknwon/i18n"
"code.gitea.io/gitea/modules/translation/i18n"
)
// Seconds-based time units

@ -12,9 +12,9 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/translation/i18n"
"github.com/stretchr/testify/assert"
"github.com/unknwon/i18n"
)
var BaseDate time.Time

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

@ -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/options"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation/i18n"
"github.com/unknwon/i18n"
"golang.org/x/text/language"
)
@ -54,13 +54,13 @@ func TryTr(lang, format string, args ...interface{}) (string, bool) {
// InitLocales loads the locales
func InitLocales() {
i18n.Reset()
i18n.ResetDefaultLocales()
localeNames, err := options.Dir("locale")
if err != nil {
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 {
localFiles[name], err = options.Locale(name)
if err != nil {
@ -76,16 +76,21 @@ func InitLocales() {
matcher = language.NewMatcher(supportedTags)
for i := range setting.Names {
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)
}
}
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{}
langs := i18n.ListLangs()
descs := i18n.ListLangDescs()
for i, v := range langs {
l := &LangType{v, descs[i]}
allLangs = append(allLangs, l)

@ -9,8 +9,8 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/translation/i18n"
"github.com/unknwon/i18n"
"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.
if lang != "" && !i18n.IsExist(lang) {
// Check again in case someone changes the supported language list.
if lang != "" && !i18n.DefaultLocales.HasLang(lang) {
lang = ""
changeLang = false
}

@ -18,9 +18,9 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation/i18n"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/unknwon/i18n"
)
// GetContentHistoryOverview get overview

@ -24,6 +24,7 @@ import (
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation/i18n"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
@ -31,8 +32,6 @@ import (
"code.gitea.io/gitea/services/agit"
"code.gitea.io/gitea/services/forms"
user_service "code.gitea.io/gitea/services/user"
"github.com/unknwon/i18n"
)
const (

@ -7,7 +7,7 @@ package cron
import (
"time"
"github.com/unknwon/i18n"
"code.gitea.io/gitea/modules/translation/i18n"
)
// Config represents a basic configuration interface that cron task

Loading…
Cancel
Save