Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify and simplify TrN for i18n #18141

Merged
merged 11 commits into from
Jan 2, 2022
36 changes: 24 additions & 12 deletions modules/csv/csv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"bytes"
"encoding/csv"
"io"
"strconv"
"strings"
"testing"

Expand All @@ -21,14 +22,21 @@ func TestCreateReader(t *testing.T) {
assert.Equal(t, ',', rd.Comma)
}

//nolint
func decodeSlashes(t *testing.T, s string) string {
s = strings.ReplaceAll(s, "\n", "\\n")
s = strings.ReplaceAll(s, "\"", "\\\"")
decoded, err := strconv.Unquote(`"` + s + `"`)
assert.NoError(t, err, "unable to decode string")
return decoded
}

func TestCreateReaderAndDetermineDelimiter(t *testing.T) {
var cases = []struct {
csv string
expectedRows [][]string
expectedDelimiter rune
}{
// case 0 - semicolon delmited
// case 0 - semicolon delimited
{
csv: `a;b;c
1;2;3
Expand All @@ -47,11 +55,11 @@ a, b c
e f
g h i
j l
m n,
m n,\t
6543 marked this conversation as resolved.
Show resolved Hide resolved
p q r
u
v w x
y
y\t\t
`,
expectedRows: [][]string{
{"col1", "col2", "col3"},
Expand All @@ -74,7 +82,7 @@ y
a, b, c
d,e,f
,h, i
j, ,
j, ,\x20
, , `,
expectedRows: [][]string{
{"col1", "col2", "col3"},
Expand All @@ -89,7 +97,7 @@ j, ,
}

for n, c := range cases {
rd, err := CreateReaderAndDetermineDelimiter(nil, strings.NewReader(c.csv))
rd, err := CreateReaderAndDetermineDelimiter(nil, strings.NewReader(decodeSlashes(t, c.csv)))
assert.NoError(t, err, "case %d: should not throw error: %v\n", n, err)
assert.EqualValues(t, c.expectedDelimiter, rd.Comma, "case %d: delimiter should be '%c', got '%c'", n, c.expectedDelimiter, rd.Comma)
rows, err := rd.ReadAll()
Expand Down Expand Up @@ -222,7 +230,7 @@ John Doe john@doe.com This,note,had,a,lot,of,commas,to,test,delimters`,
}

for n, c := range cases {
delimiter := determineDelimiter(&markup.RenderContext{Filename: c.filename}, []byte(c.csv))
delimiter := determineDelimiter(&markup.RenderContext{Filename: c.filename}, []byte(decodeSlashes(t, c.csv)))
assert.EqualValues(t, c.expectedDelimiter, delimiter, "case %d: delimiter should be equal, expected '%c' got '%c'", n, c.expectedDelimiter, delimiter)
}
}
Expand Down Expand Up @@ -287,7 +295,7 @@ abc | |123
}

for n, c := range cases {
modifiedText := removeQuotedString(c.text)
modifiedText := removeQuotedString(decodeSlashes(t, c.text))
assert.EqualValues(t, c.expectedText, modifiedText, "case %d: modified text should be equal", n)
}
}
Expand Down Expand Up @@ -353,7 +361,7 @@ John Doe john@doe.com This,note,had,a,lot,of,commas,to,test,delimters`,
quoted,
text," a
2 "some,
quoted,
quoted,\t
text," b
3 "some,
quoted,
Expand Down Expand Up @@ -442,7 +450,7 @@ jkl`,
}

for n, c := range cases {
delimiter := guessDelimiter([]byte(c.csv))
delimiter := guessDelimiter([]byte(decodeSlashes(t, c.csv)))
assert.EqualValues(t, c.expectedDelimiter, delimiter, "case %d: delimiter should be equal, expected '%c' got '%c'", n, c.expectedDelimiter, delimiter)
}
}
Expand All @@ -459,7 +467,7 @@ func TestGuessFromBeforeAfterQuotes(t *testing.T) {
quoted,
text," a
2 "some,
quoted,
quoted,\t
text," b
3 "some,
quoted,
Expand Down Expand Up @@ -534,7 +542,7 @@ a|"he said, ""here I am"""`,
}

for n, c := range cases {
delimiter := guessFromBeforeAfterQuotes([]byte(c.csv))
delimiter := guessFromBeforeAfterQuotes([]byte(decodeSlashes(t, c.csv)))
assert.EqualValues(t, c.expectedDelimiter, delimiter, "case %d: delimiter should be equal, expected '%c' got '%c'", n, c.expectedDelimiter, delimiter)
}
}
Expand All @@ -549,6 +557,10 @@ func (l mockLocale) Tr(s string, _ ...interface{}) string {
return s
}

func (l mockLocale) TrN(_cnt interface{}, key1, _keyN string, _args ...interface{}) string {
return key1
}

func TestFormatError(t *testing.T) {
var cases = []struct {
err error
Expand Down
64 changes: 0 additions & 64 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,6 @@ func NewFuncMap() []template.FuncMap {
"DisableImportLocal": func() bool {
return !setting.ImportLocalPaths
},
"TrN": TrN,
"Dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
Expand Down Expand Up @@ -857,69 +856,6 @@ func DiffLineTypeToStr(diffType int) string {
return "same"
}

// Language specific rules for translating plural texts
var trNLangRules = map[string]func(int64) int{
"en-US": func(cnt int64) int {
if cnt == 1 {
return 0
}
return 1
},
"lv-LV": func(cnt int64) int {
if cnt%10 == 1 && cnt%100 != 11 {
return 0
}
return 1
},
"ru-RU": func(cnt int64) int {
if cnt%10 == 1 && cnt%100 != 11 {
return 0
}
return 1
},
"zh-CN": func(cnt int64) int {
return 0
},
"zh-HK": func(cnt int64) int {
return 0
},
"zh-TW": func(cnt int64) int {
return 0
},
"fr-FR": func(cnt int64) int {
if cnt > -2 && cnt < 2 {
return 0
}
return 1
},
}

// TrN returns key to be used for plural text translation
func TrN(lang string, cnt interface{}, key1, keyN string) string {
var c int64
if t, ok := cnt.(int); ok {
c = int64(t)
} else if t, ok := cnt.(int16); ok {
c = int64(t)
} else if t, ok := cnt.(int32); ok {
c = int64(t)
} else if t, ok := cnt.(int64); ok {
c = t
} else {
return keyN
}

ruleFunc, ok := trNLangRules[lang]
if !ok {
ruleFunc = trNLangRules["en-US"]
}

if ruleFunc(c) == 0 {
return key1
}
return keyN
}

// MigrationIcon returns a SVG name matching the service an issue/comment was migrated from
func MigrationIcon(hostname string) string {
switch hostname {
Expand Down
4 changes: 4 additions & 0 deletions modules/test/context_tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ func (l mockLocale) Tr(s string, _ ...interface{}) string {
return s
}

func (l mockLocale) TrN(_cnt interface{}, key1, _keyN string, _args ...interface{}) string {
return key1
}

type mockResponseWriter struct {
httptest.ResponseRecorder
size int
Expand Down
65 changes: 65 additions & 0 deletions modules/translation/translation.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
type Locale interface {
Language() string
Tr(string, ...interface{}) string
TrN(cnt interface{}, key1, keyN string, args ...interface{}) string
}

// LangType represents a lang type
Expand Down Expand Up @@ -99,3 +100,67 @@ func (l *locale) Language() string {
func (l *locale) Tr(format string, args ...interface{}) string {
return i18n.Tr(l.Lang, format, args...)
}

// Language specific rules for translating plural texts
var trNLangRules = map[string]func(int64) int{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Off-Topic:

I didn't knew about this one, is this documented somewhere for translators?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea about is this documented somewhere for translators.

IMO it's mostly a knowledge for developers. If a developer write "text_1" and "text_n", the translators could just translate them without knowing TrN

// the default rule is "en-US" if a language isn't listed here
"en-US": func(cnt int64) int {
6543 marked this conversation as resolved.
Show resolved Hide resolved
if cnt == 1 {
return 0
}
return 1
},
"lv-LV": func(cnt int64) int {
if cnt%10 == 1 && cnt%100 != 11 {
return 0
}
return 1
},
"ru-RU": func(cnt int64) int {
if cnt%10 == 1 && cnt%100 != 11 {
return 0
}
return 1
},
"zh-CN": func(cnt int64) int {
return 0
},
"zh-HK": func(cnt int64) int {
return 0
},
"zh-TW": func(cnt int64) int {
return 0
},
"fr-FR": func(cnt int64) int {
if cnt > -2 && cnt < 2 {
return 0
}
return 1
},
}

// TrN returns translated message for plural text translation
func (l *locale) TrN(cnt interface{}, key1, keyN string, args ...interface{}) string {
var c int64
if t, ok := cnt.(int); ok {
c = int64(t)
} else if t, ok := cnt.(int16); ok {
c = int64(t)
} else if t, ok := cnt.(int32); ok {
c = int64(t)
} else if t, ok := cnt.(int64); ok {
c = t
} else {
return l.Tr(keyN, args...)
}
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved

ruleFunc, ok := trNLangRules[l.Lang]
if !ok {
ruleFunc = trNLangRules["en-US"]
}

if ruleFunc(c) == 0 {
return l.Tr(key1, args...)
}
return l.Tr(keyN, args...)
}
7 changes: 1 addition & 6 deletions routers/web/repo/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,8 @@ func handleMigrateError(ctx *context.Context, owner *user_model.User, err error,
case migrations.IsTwoFactorAuthError(err):
ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form)
case repo_model.IsErrReachLimitOfRepo(err):
var msg string
maxCreationLimit := owner.MaxCreationLimit()
if maxCreationLimit == 1 {
msg = ctx.Tr("repo.form.reach_limit_of_creation_1", maxCreationLimit)
} else {
msg = ctx.Tr("repo.form.reach_limit_of_creation_n", maxCreationLimit)
}
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
ctx.RenderWithErr(msg, tpl, form)
case repo_model.IsErrRepoAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
Expand Down
7 changes: 1 addition & 6 deletions routers/web/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,8 @@ func Create(ctx *context.Context) {
func handleCreateError(ctx *context.Context, owner *user_model.User, err error, name string, tpl base.TplName, form interface{}) {
switch {
case repo_model.IsErrReachLimitOfRepo(err):
var msg string
maxCreationLimit := owner.MaxCreationLimit()
if maxCreationLimit == 1 {
msg = ctx.Tr("repo.form.reach_limit_of_creation_1", maxCreationLimit)
} else {
msg = ctx.Tr("repo.form.reach_limit_of_creation_n", maxCreationLimit)
}
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
ctx.RenderWithErr(msg, tpl, form)
case repo_model.IsErrRepoAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
Expand Down
7 changes: 2 additions & 5 deletions routers/web/repo/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,11 +610,8 @@ func SettingsPost(ctx *context.Context) {

if !ctx.Repo.Owner.CanCreateRepo() {
maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit()
if maxCreationLimit == 1 {
ctx.Flash.Error(ctx.Tr("repo.form.reach_limit_of_creation_1", maxCreationLimit))
} else {
ctx.Flash.Error(ctx.Tr("repo.form.reach_limit_of_creation_n", maxCreationLimit))
}
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
ctx.Flash.Error(msg)
ctx.Redirect(repo.Link() + "/settings")
return
}
Expand Down
5 changes: 0 additions & 5 deletions services/mailer/mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, s
// helper
"i18n": locale,
"Str2html": templates.Str2html,
"TrN": templates.TrN,
}

var content bytes.Buffer
Expand Down Expand Up @@ -129,7 +128,6 @@ func SendActivateEmailMail(u *user_model.User, email *user_model.EmailAddress) {
// helper
"i18n": locale,
"Str2html": templates.Str2html,
"TrN": templates.TrN,
}

var content bytes.Buffer
Expand Down Expand Up @@ -160,7 +158,6 @@ func SendRegisterNotifyMail(u *user_model.User) {
// helper
"i18n": locale,
"Str2html": templates.Str2html,
"TrN": templates.TrN,
}

var content bytes.Buffer
Expand Down Expand Up @@ -194,7 +191,6 @@ func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository)
// helper
"i18n": locale,
"Str2html": templates.Str2html,
"TrN": templates.TrN,
}

var content bytes.Buffer
Expand Down Expand Up @@ -278,7 +274,6 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
// helper
"i18n": locale,
"Str2html": templates.Str2html,
"TrN": templates.TrN,
}

var mailSubject bytes.Buffer
Expand Down
1 change: 0 additions & 1 deletion services/mailer/mail_release.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ func mailNewRelease(lang string, tos []string, rel *models.Release) {
// helper
"i18n": locale,
"Str2html": templates.Str2html,
"TrN": templates.TrN,
}

var mailBody bytes.Buffer
Expand Down
1 change: 0 additions & 1 deletion services/mailer/mail_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U
// helper
"i18n": locale,
"Str2html": templates.Str2html,
"TrN": templates.TrN,
}

if err := bodyTemplates.ExecuteTemplate(&content, string(mailRepoTransferNotify), data); err != nil {
Expand Down
Loading