mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-08-05 09:55:20 +02:00
Replace the 'relative-time' element scripting with custom, translatable rewrite (#6154)
This is my take to fix #6078 Should also resolve #6111 As far as I can tell, Forgejo uses only a subset of the relative-time functionality, and as far as I can see, this subset can be implemented using browser built-in date conversion and arithmetic. So I wrote a JavaScript to format the relative-time element accordingly, and a Go binding to generate the translated elements. This is my first time writing Go code, and my first time coding for a large-scale server application, so please tell me if I'm doing something wrong, or if the whole approach is not acceptable. --- Screenshot: Localized times in Low German  Screenshot: The same with Forgejo in English  --- ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [x] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] I do not want this change to show in the release notes. - [ ] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/6154 Reviewed-by: 0ko <0ko@noreply.codeberg.org> Reviewed-by: Michael Kriese <michael.kriese@gmx.de> Co-authored-by: Benedikt Straub <benedikt-straub@web.de> Co-committed-by: Benedikt Straub <benedikt-straub@web.de>
This commit is contained in:
parent
37d566bdb0
commit
cf03286b5b
19 changed files with 698 additions and 35 deletions
|
@ -15,6 +15,10 @@ type KeyLocale struct{}
|
|||
|
||||
var _ Locale = (*KeyLocale)(nil)
|
||||
|
||||
func (k *KeyLocale) Language() string {
|
||||
return "dummy"
|
||||
}
|
||||
|
||||
// HasKey implements Locale.
|
||||
func (k *KeyLocale) HasKey(trKey string) bool {
|
||||
return true
|
||||
|
@ -35,6 +39,11 @@ func (k *KeyLocale) TrPluralString(count any, trKey string, trArgs ...any) templ
|
|||
return template.HTML(FormatDummy(trKey, PrepareArgsForHTML(trArgs...)...))
|
||||
}
|
||||
|
||||
// TrPluralStringAllForms implements Locale.
|
||||
func (k *KeyLocale) TrPluralStringAllForms(trKey string) ([]string, []string) {
|
||||
return []string{trKey}, nil
|
||||
}
|
||||
|
||||
func FormatDummy(trKey string, args ...any) string {
|
||||
if len(args) == 0 {
|
||||
return fmt.Sprintf("(%s)", trKey)
|
||||
|
|
|
@ -25,6 +25,7 @@ const (
|
|||
var DefaultLocales = NewLocaleStore()
|
||||
|
||||
type Locale interface {
|
||||
Language() string
|
||||
// TrString translates a given key and arguments for a language
|
||||
TrString(trKey string, trArgs ...any) string
|
||||
// TrPluralString translates a given pluralized key and arguments for a language.
|
||||
|
@ -34,6 +35,9 @@ type Locale interface {
|
|||
TrHTML(trKey string, trArgs ...any) template.HTML
|
||||
// HasKey reports if a locale has a translation for a given key
|
||||
HasKey(trKey string) bool
|
||||
// TrPluralStringAllForms returns all plural form variants for a given string, and also
|
||||
// the fallbacks for the default language if the translation is incomplete.
|
||||
TrPluralStringAllForms(trKey string) ([]string, []string)
|
||||
}
|
||||
|
||||
// LocaleStore provides the functions common to all locale stores
|
||||
|
@ -42,6 +46,8 @@ type LocaleStore interface {
|
|||
|
||||
// SetDefaultLang sets the default language to fall back to
|
||||
SetDefaultLang(lang string)
|
||||
// GetDefaultLang returns the name of the default language to fall back to
|
||||
GetDefaultLang() string
|
||||
// ListLangNameDesc provides paired slices of language names to descriptors
|
||||
ListLangNameDesc() (names, desc []string)
|
||||
// Locale return the locale for the provided language or the default language if not found
|
||||
|
@ -49,7 +55,7 @@ type LocaleStore interface {
|
|||
// HasLang returns whether a given language is present in the store
|
||||
HasLang(langName string) bool
|
||||
// AddLocaleByIni adds a new old-style language to the store
|
||||
AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, source, moreSource []byte) error
|
||||
AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, usedPluralForms []PluralFormIndex, source, moreSource []byte) error
|
||||
// AddLocaleByJSON adds new-style content to an existing language to the store
|
||||
AddToLocaleFromJSON(langName string, source []byte) error
|
||||
}
|
||||
|
|
|
@ -32,6 +32,11 @@ var MockPluralRuleEnglish PluralFormRule = func(n int64) PluralFormIndex {
|
|||
return PluralFormOther
|
||||
}
|
||||
|
||||
var (
|
||||
UsedPluralFormsEnglish = []PluralFormIndex{PluralFormOne, PluralFormOther}
|
||||
UsedPluralFormsMock = []PluralFormIndex{PluralFormZero, PluralFormOne, PluralFormFew, PluralFormOther}
|
||||
)
|
||||
|
||||
func TestLocaleStore(t *testing.T) {
|
||||
testData1 := []byte(`
|
||||
.dot.name = Dot Name
|
||||
|
@ -85,8 +90,8 @@ commits = fallback value for commits
|
|||
`)
|
||||
|
||||
ls := NewLocaleStore()
|
||||
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRuleEnglish, testData1, nil))
|
||||
require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", MockPluralRule, testData2, nil))
|
||||
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRuleEnglish, UsedPluralFormsEnglish, testData1, nil))
|
||||
require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", MockPluralRule, UsedPluralFormsMock, testData2, nil))
|
||||
require.NoError(t, ls.AddToLocaleFromJSON("lang1", testDataJSON1))
|
||||
require.NoError(t, ls.AddToLocaleFromJSON("lang2", testDataJSON2))
|
||||
ls.SetDefaultLang("lang1")
|
||||
|
@ -182,7 +187,7 @@ c=22
|
|||
`)
|
||||
|
||||
ls := NewLocaleStore()
|
||||
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, testData1, testData2))
|
||||
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, UsedPluralFormsMock, testData1, testData2))
|
||||
lang1, _ := ls.Locale("lang1")
|
||||
assert.Equal(t, "11", lang1.TrString("a"))
|
||||
assert.Equal(t, "21", lang1.TrString("b"))
|
||||
|
@ -223,7 +228,7 @@ func (e *errorPointerReceiver) Error() string {
|
|||
|
||||
func TestLocaleWithTemplate(t *testing.T) {
|
||||
ls := NewLocaleStore()
|
||||
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, []byte(`key=<a>%s</a>`), nil))
|
||||
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, UsedPluralFormsMock, []byte(`key=<a>%s</a>`), nil))
|
||||
lang1, _ := ls.Locale("lang1")
|
||||
|
||||
tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML})
|
||||
|
@ -286,7 +291,7 @@ func TestLocaleStoreQuirks(t *testing.T) {
|
|||
|
||||
for _, testData := range testDataList {
|
||||
ls := NewLocaleStore()
|
||||
err := ls.AddLocaleByIni("lang1", "Lang1", nil, []byte("a="+testData.in), nil)
|
||||
err := ls.AddLocaleByIni("lang1", "Lang1", nil, nil, []byte("a="+testData.in), nil)
|
||||
lang1, _ := ls.Locale("lang1")
|
||||
require.NoError(t, err, testData.hint)
|
||||
assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint)
|
||||
|
|
|
@ -24,6 +24,7 @@ type locale struct {
|
|||
|
||||
newStyleMessages map[string]string
|
||||
pluralRule PluralFormRule
|
||||
usedPluralForms []PluralFormIndex
|
||||
}
|
||||
|
||||
var _ Locale = (*locale)(nil)
|
||||
|
@ -56,7 +57,7 @@ const (
|
|||
// the correct plural form for a given count and language.
|
||||
|
||||
// AddLocaleByIni adds locale by ini into the store
|
||||
func (store *localeStore) AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, source, moreSource []byte) error {
|
||||
func (store *localeStore) AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, usedPluralForms []PluralFormIndex, source, moreSource []byte) error {
|
||||
if _, ok := store.localeMap[langName]; ok {
|
||||
return ErrLocaleAlreadyExist
|
||||
}
|
||||
|
@ -64,7 +65,7 @@ func (store *localeStore) AddLocaleByIni(langName, langDesc string, pluralRule P
|
|||
store.langNames = append(store.langNames, langName)
|
||||
store.langDescs = append(store.langDescs, langDesc)
|
||||
|
||||
l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string), pluralRule: pluralRule, newStyleMessages: make(map[string]string)}
|
||||
l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string), pluralRule: pluralRule, usedPluralForms: usedPluralForms, newStyleMessages: make(map[string]string)}
|
||||
store.localeMap[l.langName] = l
|
||||
|
||||
iniFile, err := setting.NewConfigProviderForLocale(source, moreSource)
|
||||
|
@ -118,7 +119,7 @@ func (l *locale) LookupNewStyleMessage(trKey string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (l *locale) LookupPlural(trKey string, count any) string {
|
||||
func (l *locale) LookupPluralByCount(trKey string, count any) string {
|
||||
n, err := util.ToInt64(count)
|
||||
if err != nil {
|
||||
log.Error("Invalid plural count '%s'", count)
|
||||
|
@ -126,6 +127,10 @@ func (l *locale) LookupPlural(trKey string, count any) string {
|
|||
}
|
||||
|
||||
pluralForm := l.pluralRule(n)
|
||||
return l.LookupPluralByForm(trKey, pluralForm)
|
||||
}
|
||||
|
||||
func (l *locale) LookupPluralByForm(trKey string, pluralForm PluralFormIndex) string {
|
||||
suffix := ""
|
||||
switch pluralForm {
|
||||
case PluralFormZero:
|
||||
|
@ -141,7 +146,7 @@ func (l *locale) LookupPlural(trKey string, count any) string {
|
|||
case PluralFormOther:
|
||||
// No suffix for the "other" string.
|
||||
default:
|
||||
log.Error("Invalid plural form index %d for count %d", pluralForm, count)
|
||||
log.Error("Invalid plural form index %d", pluralForm)
|
||||
return ""
|
||||
}
|
||||
|
||||
|
@ -149,7 +154,7 @@ func (l *locale) LookupPlural(trKey string, count any) string {
|
|||
return result
|
||||
}
|
||||
|
||||
log.Error("Missing translation for plural form index %d for count %d", pluralForm, count)
|
||||
log.Error("Missing translation for plural form %s", suffix)
|
||||
return ""
|
||||
}
|
||||
|
||||
|
@ -167,6 +172,10 @@ func (store *localeStore) SetDefaultLang(lang string) {
|
|||
store.defaultLang = lang
|
||||
}
|
||||
|
||||
func (store *localeStore) GetDefaultLang() string {
|
||||
return store.defaultLang
|
||||
}
|
||||
|
||||
// Locale returns the locale for the lang or the default language
|
||||
func (store *localeStore) Locale(lang string) (Locale, bool) {
|
||||
l, found := store.localeMap[lang]
|
||||
|
@ -185,6 +194,10 @@ func (store *localeStore) Close() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (l *locale) Language() string {
|
||||
return l.langName
|
||||
}
|
||||
|
||||
func (l *locale) TrString(trKey string, trArgs ...any) string {
|
||||
format := trKey
|
||||
|
||||
|
@ -251,11 +264,11 @@ func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
|
|||
}
|
||||
|
||||
func (l *locale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML {
|
||||
message := l.LookupPlural(trKey, count)
|
||||
message := l.LookupPluralByCount(trKey, count)
|
||||
|
||||
if message == "" {
|
||||
if defaultLang, ok := l.store.localeMap[l.store.defaultLang]; ok {
|
||||
message = defaultLang.LookupPlural(trKey, count)
|
||||
message = defaultLang.LookupPluralByCount(trKey, count)
|
||||
}
|
||||
if message == "" {
|
||||
message = trKey
|
||||
|
@ -269,6 +282,36 @@ func (l *locale) TrPluralString(count any, trKey string, trArgs ...any) template
|
|||
return template.HTML(message)
|
||||
}
|
||||
|
||||
func (l *locale) TrPluralStringAllForms(trKey string) ([]string, []string) {
|
||||
defaultLang, hasDefaultLang := l.store.localeMap[l.store.defaultLang]
|
||||
|
||||
var fallback []string
|
||||
fallback = nil
|
||||
|
||||
result := make([]string, len(l.usedPluralForms))
|
||||
allPresent := true
|
||||
|
||||
for i, form := range l.usedPluralForms {
|
||||
result[i] = l.LookupPluralByForm(trKey, form)
|
||||
if result[i] == "" {
|
||||
allPresent = false
|
||||
}
|
||||
}
|
||||
|
||||
if !allPresent {
|
||||
if hasDefaultLang {
|
||||
fallback = make([]string, len(defaultLang.usedPluralForms))
|
||||
for i, form := range defaultLang.usedPluralForms {
|
||||
fallback[i] = defaultLang.LookupPluralByForm(trKey, form)
|
||||
}
|
||||
} else {
|
||||
log.Error("Plural set for '%s' is incomplete and no fallback language is set.", trKey)
|
||||
}
|
||||
}
|
||||
|
||||
return result, fallback
|
||||
}
|
||||
|
||||
// HasKey returns whether a key is present in this locale or not
|
||||
func (l *locale) HasKey(trKey string) bool {
|
||||
_, ok := l.newStyleMessages[trKey]
|
||||
|
|
|
@ -6,11 +6,15 @@ package translation
|
|||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
||||
"forgejo.org/modules/translation/i18n"
|
||||
)
|
||||
|
||||
// MockLocale provides a mocked locale without any translations
|
||||
// MockLocale provides a mocked locale without any translations, other than those inserted into MockTranslations by a testcase
|
||||
type MockLocale struct {
|
||||
Lang, LangName string // these fields are used directly in templates: ctx.Locale.Lang
|
||||
|
||||
MockTranslations map[string]string
|
||||
}
|
||||
|
||||
var _ Locale = (*MockLocale)(nil)
|
||||
|
@ -20,11 +24,14 @@ func (l MockLocale) Language() string {
|
|||
}
|
||||
|
||||
func (l MockLocale) TrString(s string, _ ...any) string {
|
||||
if val, ok := l.MockTranslations[s]; ok {
|
||||
return val
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (l MockLocale) Tr(s string, a ...any) template.HTML {
|
||||
return template.HTML(s)
|
||||
return template.HTML(l.TrString(s))
|
||||
}
|
||||
|
||||
func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
|
||||
|
@ -35,6 +42,11 @@ func (l MockLocale) TrPluralString(count any, trKey string, trArgs ...any) templ
|
|||
return template.HTML(trKey)
|
||||
}
|
||||
|
||||
// TrPluralStringAllForms implements Locale.
|
||||
func (l MockLocale) TrPluralStringAllForms(trKey string) ([]string, []string) {
|
||||
return []string{l.TrString(trKey + i18n.PluralFormSeparator + "one"), l.TrString(trKey + i18n.PluralFormSeparator + "other")}, nil
|
||||
}
|
||||
|
||||
func (l MockLocale) TrSize(s int64) ReadableSize {
|
||||
return ReadableSize{fmt.Sprint(s), ""}
|
||||
}
|
||||
|
|
|
@ -251,3 +251,34 @@ var PluralRules = []i18n.PluralFormRule{
|
|||
return i18n.PluralFormOther
|
||||
},
|
||||
}
|
||||
|
||||
var UsedPluralForms = [][]i18n.PluralFormIndex{
|
||||
// [ 0] Common 2-form, e.g. English, German
|
||||
{i18n.PluralFormOne, i18n.PluralFormOther},
|
||||
// [ 1] Bengali
|
||||
{i18n.PluralFormOne, i18n.PluralFormOther},
|
||||
// [ 2] Icelandic
|
||||
{i18n.PluralFormOne, i18n.PluralFormOther},
|
||||
// [ 3] Filipino
|
||||
{i18n.PluralFormOne, i18n.PluralFormOther},
|
||||
// [ 4] OneForm
|
||||
{i18n.PluralFormOther},
|
||||
// [ 5] Czech
|
||||
{i18n.PluralFormOne, i18n.PluralFormFew, i18n.PluralFormOther},
|
||||
// [ 6] Russian
|
||||
{i18n.PluralFormOne, i18n.PluralFormFew, i18n.PluralFormMany},
|
||||
// [ 7] Polish
|
||||
{i18n.PluralFormOne, i18n.PluralFormFew, i18n.PluralFormOther},
|
||||
// [ 8] Latvian
|
||||
{i18n.PluralFormZero, i18n.PluralFormOne, i18n.PluralFormOther},
|
||||
// [ 9] Lithuanian
|
||||
{i18n.PluralFormOne, i18n.PluralFormFew, i18n.PluralFormMany},
|
||||
// [10] French
|
||||
{i18n.PluralFormOne, i18n.PluralFormMany, i18n.PluralFormOther},
|
||||
// [11] Catalan
|
||||
{i18n.PluralFormOne, i18n.PluralFormMany, i18n.PluralFormOther},
|
||||
// [12] Slovenian
|
||||
{i18n.PluralFormOne, i18n.PluralFormTwo, i18n.PluralFormFew, i18n.PluralFormOther},
|
||||
// [13] Arabic
|
||||
{i18n.PluralFormZero, i18n.PluralFormOne, i18n.PluralFormTwo, i18n.PluralFormFew, i18n.PluralFormMany, i18n.PluralFormOther},
|
||||
}
|
||||
|
|
|
@ -46,6 +46,8 @@ type Locale interface {
|
|||
HasKey(trKey string) bool
|
||||
|
||||
PrettyNumber(v any) string
|
||||
|
||||
TrPluralStringAllForms(trKey string) ([]string, []string)
|
||||
}
|
||||
|
||||
// LangType represents a lang type
|
||||
|
@ -108,8 +110,9 @@ func InitLocales(ctx context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
pluralRuleIndex := GetPluralRuleImpl(setting.Langs[i])
|
||||
key := "locale_" + setting.Langs[i] + ".ini"
|
||||
if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], PluralRules[GetPluralRuleImpl(setting.Langs[i])], localeDataBase, localeData[key]); err != nil {
|
||||
if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], PluralRules[pluralRuleIndex], UsedPluralForms[pluralRuleIndex], localeDataBase, localeData[key]); err != nil {
|
||||
log.Error("Failed to set old-style messages to %s: %v", setting.Langs[i], err)
|
||||
}
|
||||
|
||||
|
@ -324,6 +327,14 @@ func (l *locale) PrettyNumber(v any) string {
|
|||
return l.msgPrinter.Sprintf("%v", number.Decimal(v))
|
||||
}
|
||||
|
||||
func GetPluralRule(l Locale) int {
|
||||
return GetPluralRuleImpl(l.Language())
|
||||
}
|
||||
|
||||
func GetDefaultPluralRule() int {
|
||||
return GetPluralRuleImpl(i18n.DefaultLocales.GetDefaultLang())
|
||||
}
|
||||
|
||||
func init() {
|
||||
// prepare a default matcher, especially for tests
|
||||
supportedTags = []language.Tag{language.English}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue