2021-01-05 16:05:40 +03:00
// Copyright 2020 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2021-01-05 16:05:40 +03:00
package translation
import (
2022-08-28 12:43:25 +03:00
"context"
2024-02-15 00:48:45 +03:00
"html/template"
2022-01-08 15:18:39 +03:00
"sort"
"strings"
2022-08-28 12:43:25 +03:00
"sync"
2022-01-08 15:18:39 +03:00
2021-01-05 16:05:40 +03:00
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/options"
"code.gitea.io/gitea/modules/setting"
2022-04-03 12:46:48 +03:00
"code.gitea.io/gitea/modules/translation/i18n"
2023-04-29 00:23:19 +03:00
"code.gitea.io/gitea/modules/util"
2021-01-05 16:05:40 +03:00
"golang.org/x/text/language"
2023-04-17 06:37:23 +03:00
"golang.org/x/text/message"
"golang.org/x/text/number"
2021-01-05 16:05:40 +03:00
)
Append `(comment)` when a link points at a comment rather than the whole issue (#23734)
Close #23671
For the feature mentioned above, this PR append ' (comment)' to the
rendered html if it is a hashcomment.
After the PR, type in the following
```
pull request from other repo:
http://localhost:3000/testOrg/testOrgRepo/pulls/2
pull request from this repo:
http://localhost:3000/aaa/testA/pulls/2
issue comment from this repo:
http://localhost:3000/aaa/testA/issues/1#issuecomment-18
http://localhost:3000/aaa/testA/pulls/2#issue-9
issue comment from other repo:
http://localhost:3000/testOrg/testOrgRepo/pulls/2#issuecomment-24
http://localhost:3000/testOrg/testOrgRepo/pulls/2#issue
```
Gives:
<img width="687" alt="截屏2023-03-27 13 53 06"
src="https://user-images.githubusercontent.com/17645053/227852387-2b218e0d-3468-4d90-ad81-d702ddd17fd2.png">
Other than the above feature, this PR also includes two other changes:
1 Right now, the render of links from file changed tab in pull request
might not be very proper, for example, if type in the following. (not
sure if this is an issue or design, if not an issue, I will revert the
changes). example on
[try.gitea.io](https://try.gitea.io/HesterG/testrepo/pulls/1)
```
https://try.gitea.io/HesterG/testrepo/pulls/1/files#issuecomment-162725
https://try.gitea.io/HesterG/testrepo/pulls/1/files
```
it will render the following
<img width="899" alt="截屏2023-03-24 15 41 37"
src="https://user-images.githubusercontent.com/17645053/227456117-5eccedb7-9118-4540-929d-aee9a76de852.png">
In this PR, skip processing the link into a ref issue if it is a link
from files changed tab in pull request
After:
type in following
```
hash comment on files changed tab:
http://localhost:3000/testOrg/testOrgRepo/pulls/2/files#issuecomment-24
files changed link:
http://localhost:3000/testOrg/testOrgRepo/pulls/2/files
```
Gives
<img width="708" alt="截屏2023-03-27 22 09 02"
src="https://user-images.githubusercontent.com/17645053/227964273-5dc06c50-3713-489c-b05d-d95367d0ab0f.png">
2 Right now, after editing the comment area, there will not be tippys
attached to `ref-issue`; and no tippy attached on preview as well.
example:
https://user-images.githubusercontent.com/17645053/227850540-5ae34e2d-b1d7-4d0d-9726-7701bf825d1f.mov
In this PR, in frontend, make sure tippy is added after editing the
comment, and to the comment on preview tab
After:
https://user-images.githubusercontent.com/17645053/227853777-06f56b4c-1148-467c-b6f7-f79418e67504.mov
2023-04-03 11:02:57 +03:00
type contextKey struct { }
2023-04-17 06:37:23 +03:00
var ContextKey any = & contextKey { }
Append `(comment)` when a link points at a comment rather than the whole issue (#23734)
Close #23671
For the feature mentioned above, this PR append ' (comment)' to the
rendered html if it is a hashcomment.
After the PR, type in the following
```
pull request from other repo:
http://localhost:3000/testOrg/testOrgRepo/pulls/2
pull request from this repo:
http://localhost:3000/aaa/testA/pulls/2
issue comment from this repo:
http://localhost:3000/aaa/testA/issues/1#issuecomment-18
http://localhost:3000/aaa/testA/pulls/2#issue-9
issue comment from other repo:
http://localhost:3000/testOrg/testOrgRepo/pulls/2#issuecomment-24
http://localhost:3000/testOrg/testOrgRepo/pulls/2#issue
```
Gives:
<img width="687" alt="截屏2023-03-27 13 53 06"
src="https://user-images.githubusercontent.com/17645053/227852387-2b218e0d-3468-4d90-ad81-d702ddd17fd2.png">
Other than the above feature, this PR also includes two other changes:
1 Right now, the render of links from file changed tab in pull request
might not be very proper, for example, if type in the following. (not
sure if this is an issue or design, if not an issue, I will revert the
changes). example on
[try.gitea.io](https://try.gitea.io/HesterG/testrepo/pulls/1)
```
https://try.gitea.io/HesterG/testrepo/pulls/1/files#issuecomment-162725
https://try.gitea.io/HesterG/testrepo/pulls/1/files
```
it will render the following
<img width="899" alt="截屏2023-03-24 15 41 37"
src="https://user-images.githubusercontent.com/17645053/227456117-5eccedb7-9118-4540-929d-aee9a76de852.png">
In this PR, skip processing the link into a ref issue if it is a link
from files changed tab in pull request
After:
type in following
```
hash comment on files changed tab:
http://localhost:3000/testOrg/testOrgRepo/pulls/2/files#issuecomment-24
files changed link:
http://localhost:3000/testOrg/testOrgRepo/pulls/2/files
```
Gives
<img width="708" alt="截屏2023-03-27 22 09 02"
src="https://user-images.githubusercontent.com/17645053/227964273-5dc06c50-3713-489c-b05d-d95367d0ab0f.png">
2 Right now, after editing the comment area, there will not be tippys
attached to `ref-issue`; and no tippy attached on preview as well.
example:
https://user-images.githubusercontent.com/17645053/227850540-5ae34e2d-b1d7-4d0d-9726-7701bf825d1f.mov
In this PR, in frontend, make sure tippy is added after editing the
comment, and to the comment on preview tab
After:
https://user-images.githubusercontent.com/17645053/227853777-06f56b4c-1148-467c-b6f7-f79418e67504.mov
2023-04-03 11:02:57 +03:00
2021-01-05 16:05:40 +03:00
// Locale represents an interface to translation
type Locale interface {
Language ( ) string
2024-02-15 00:48:45 +03:00
TrString ( string , ... any ) string
Tr ( key string , args ... any ) template . HTML
TrN ( cnt any , key1 , keyN string , args ... any ) template . HTML
2023-04-17 06:37:23 +03:00
PrettyNumber ( v any ) string
2021-01-05 16:05:40 +03:00
}
2021-01-26 18:36:53 +03:00
// LangType represents a lang type
type LangType struct {
2022-02-08 06:02:30 +03:00
Lang , Name string // these fields are used directly in templates: {{range .AllLangs}}{{.Lang}}{{.Name}}{{end}}
2021-01-26 18:36:53 +03:00
}
2021-01-05 16:05:40 +03:00
var (
2023-05-21 04:50:53 +03:00
lock * sync . RWMutex
allLangs [ ] * LangType
allLangMap map [ string ] * LangType
2021-04-14 21:52:01 +03:00
matcher language . Matcher
supportedTags [ ] language . Tag
2021-01-05 16:05:40 +03:00
)
2022-01-08 15:18:39 +03:00
// AllLangs returns all supported languages sorted by name
2022-02-08 06:02:30 +03:00
func AllLangs ( ) [ ] * LangType {
2021-01-26 18:36:53 +03:00
return allLangs
}
2021-01-05 16:05:40 +03:00
// InitLocales loads the locales
2022-08-28 12:43:25 +03:00
func InitLocales ( ctx context . Context ) {
if lock != nil {
lock . Lock ( )
defer lock . Unlock ( )
} else if ! setting . IsProd && lock == nil {
lock = & sync . RWMutex { }
2021-01-05 16:05:40 +03:00
}
2022-08-28 12:43:25 +03:00
refreshLocales := func ( ) {
i18n . ResetDefaultLocales ( )
2023-04-12 13:16:45 +03:00
localeNames , err := options . AssetFS ( ) . ListFiles ( "locale" , true )
2022-08-28 12:43:25 +03:00
if err != nil {
log . Fatal ( "Failed to list locale files: %v" , err )
}
2022-06-26 17:19:22 +03:00
2022-09-25 02:00:16 +03:00
localeData := make ( map [ string ] [ ] byte , len ( localeNames ) )
2022-08-28 12:43:25 +03:00
for _ , name := range localeNames {
2022-09-25 02:00:16 +03:00
localeData [ name ] , err = options . Locale ( name )
2022-06-26 17:19:22 +03:00
if err != nil {
log . Fatal ( "Failed to load %s locale file. %v" , name , err )
}
2021-01-05 16:05:40 +03:00
}
2022-08-28 12:43:25 +03:00
supportedTags = make ( [ ] language . Tag , len ( setting . Langs ) )
for i , lang := range setting . Langs {
supportedTags [ i ] = language . Raw . Make ( lang )
}
2021-01-26 18:36:53 +03:00
2022-08-28 12:43:25 +03:00
matcher = language . NewMatcher ( supportedTags )
for i := range setting . Names {
2022-09-25 02:00:16 +03:00
var localeDataBase [ ] byte
if i == 0 && setting . Langs [ 0 ] != "en-US" {
// Only en-US has complete translations. When use other language as default, the en-US should still be used as fallback.
localeDataBase = localeData [ "locale_en-US.ini" ]
if localeDataBase == nil {
log . Fatal ( "Failed to load locale_en-US.ini file." )
}
}
2022-06-26 17:19:22 +03:00
2022-09-25 02:00:16 +03:00
key := "locale_" + setting . Langs [ i ] + ".ini"
if err = i18n . DefaultLocales . AddLocaleByIni ( setting . Langs [ i ] , setting . Names [ i ] , localeDataBase , localeData [ key ] ) ; err != nil {
2022-08-28 12:43:25 +03:00
log . Error ( "Failed to set messages to %s: %v" , setting . Langs [ i ] , err )
}
2021-01-26 18:36:53 +03:00
}
2022-08-28 12:43:25 +03:00
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 )
2022-04-03 12:46:48 +03:00
}
}
2021-01-26 18:36:53 +03:00
2022-08-28 12:43:25 +03:00
refreshLocales ( )
2022-04-03 12:46:48 +03:00
langs , descs := i18n . DefaultLocales . ListLangNameDesc ( )
allLangs = make ( [ ] * LangType , 0 , len ( langs ) )
2022-02-08 06:02:30 +03:00
allLangMap = map [ string ] * LangType { }
2021-01-26 18:36:53 +03:00
for i , v := range langs {
2022-02-08 06:02:30 +03:00
l := & LangType { v , descs [ i ] }
allLangs = append ( allLangs , l )
allLangMap [ v ] = l
2021-01-05 16:05:40 +03:00
}
2022-01-08 15:18:39 +03:00
2022-02-08 06:02:30 +03:00
// Sort languages case-insensitive according to their name - needed for the user settings
2022-01-08 15:18:39 +03:00
sort . Slice ( allLangs , func ( i , j int ) bool {
return strings . ToLower ( allLangs [ i ] . Name ) < strings . ToLower ( allLangs [ j ] . Name )
} )
2022-08-28 12:43:25 +03:00
if ! setting . IsProd {
2023-04-12 13:16:45 +03:00
go options . AssetFS ( ) . WatchLocalChanges ( ctx , func ( ) {
lock . Lock ( )
defer lock . Unlock ( )
refreshLocales ( )
2022-08-28 12:43:25 +03:00
} )
}
2021-01-05 16:05:40 +03:00
}
// Match matches accept languages
2021-04-14 21:52:01 +03:00
func Match ( tags ... language . Tag ) language . Tag {
_ , i , _ := matcher . Match ( tags ... )
return supportedTags [ i ]
2021-01-05 16:05:40 +03:00
}
// locale represents the information of localization.
type locale struct {
2022-08-28 12:43:25 +03:00
i18n . Locale
2022-02-08 06:02:30 +03:00
Lang , LangName string // these fields are used directly in templates: .i18n.Lang
2023-04-17 06:37:23 +03:00
msgPrinter * message . Printer
2021-01-05 16:05:40 +03:00
}
2024-02-15 00:48:45 +03:00
var _ Locale = ( * locale ) ( nil )
2021-01-05 16:05:40 +03:00
// NewLocale return a locale
func NewLocale ( lang string ) Locale {
2022-08-28 12:43:25 +03:00
if lock != nil {
lock . RLock ( )
defer lock . RUnlock ( )
}
2022-02-08 06:02:30 +03:00
langName := "unknown"
if l , ok := allLangMap [ lang ] ; ok {
langName = l . Name
2023-04-17 06:37:23 +03:00
} else if len ( setting . Langs ) > 0 {
lang = setting . Langs [ 0 ]
langName = setting . Names [ 0 ]
2022-02-08 06:02:30 +03:00
}
2023-04-17 06:37:23 +03:00
2022-08-28 12:43:25 +03:00
i18nLocale , _ := i18n . GetLocale ( lang )
2023-04-17 06:37:23 +03:00
l := & locale {
2022-08-28 12:43:25 +03:00
Locale : i18nLocale ,
2022-02-08 06:02:30 +03:00
Lang : lang ,
LangName : langName ,
2021-01-05 16:05:40 +03:00
}
2023-04-17 06:37:23 +03:00
if langTag , err := language . Parse ( lang ) ; err != nil {
log . Error ( "Failed to parse language tag from name %q: %v" , l . Lang , err )
l . msgPrinter = message . NewPrinter ( language . English )
} else {
l . msgPrinter = message . NewPrinter ( langTag )
}
return l
2021-01-05 16:05:40 +03:00
}
func ( l * locale ) Language ( ) string {
return l . Lang
}
2022-01-02 06:33:57 +03:00
// Language specific rules for translating plural texts
var trNLangRules = map [ string ] func ( int64 ) int {
// the default rule is "en-US" if a language isn't listed here
"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
} ,
}
2024-02-15 00:48:45 +03:00
func ( l * locale ) Tr ( s string , args ... any ) template . HTML {
return l . TrHTML ( s , args ... )
}
2022-01-02 06:33:57 +03:00
// TrN returns translated message for plural text translation
2024-02-15 00:48:45 +03:00
func ( l * locale ) TrN ( cnt any , key1 , keyN string , args ... any ) template . HTML {
2022-01-02 06:33:57 +03:00
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 ... )
}
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 ... )
}
2023-04-17 06:37:23 +03:00
func ( l * locale ) PrettyNumber ( v any ) string {
// TODO: this mechanism is not good enough, the complete solution is to switch the translation system to ICU message format
2023-04-29 00:23:19 +03:00
if s , ok := v . ( string ) ; ok {
if num , err := util . ToInt64 ( s ) ; err == nil {
v = num
} else if num , err := util . ToFloat64 ( s ) ; err == nil {
v = num
}
}
2023-04-17 06:37:23 +03:00
return l . msgPrinter . Sprintf ( "%v" , number . Decimal ( v ) )
}
2023-05-21 04:50:53 +03:00
func init ( ) {
// prepare a default matcher, especially for tests
supportedTags = [ ] language . Tag { language . English }
matcher = language . NewMatcher ( supportedTags )
}