feat(build): improve lint-locale-usage

* add check for unused msgids
* add ability to manually allowlist specific msgids and groups of msgids

* lint-locale-usage should handle .locale.Tr and Unit{...}
* lint-locale-usage should handle simple dynamic msgids
* remove unnecessary DescKey and associated msgids + values
This commit is contained in:
Ellen Emilia Anna Zscheile 2025-07-30 23:38:02 +02:00
commit 31190e385e
12 changed files with 365 additions and 146 deletions

2
.gitignore vendored
View file

@ -55,6 +55,8 @@ cpu.out
*.log *.log
*.log.*.gz *.log.*.gz
/build/lint-locale/lint-locale
/build/lint-locale-usage/lint-locale-usage
/gitea /gitea
/gitea-vet /gitea-vet
/debug /debug

View file

@ -460,7 +460,7 @@ lint-locale:
.PHONY: lint-locale-usage .PHONY: lint-locale-usage
lint-locale-usage: lint-locale-usage:
$(GO) run build/lint-locale-usage/lint-locale-usage.go $(GO) run build/lint-locale-usage/lint-locale-usage.go --allow-masked-usages-from=build/lint-locale-usage/allowed-masked-usage.txt
.PHONY: lint-md .PHONY: lint-md
lint-md: node_modules lint-md: node_modules

View file

@ -0,0 +1,62 @@
# translation tooling test keys
meta.last_line
translation_meta.test
# models/admin/task.go: instances of $TranslatableMessage.Format
# this also gets instantiate as a Messenger once
org.matrix.custom.html
migrate.migrating_failed.error
# models/asymkey/gpg_key_object_verification.go: $ObjectVerification.Reason
# unfortunately, it is non-trivial to parse all the occurences
gpg.error.extract_sign
gpg.error.failed_retrieval_gpg_keys
gpg.error.generate_hash
gpg.error.no_committer_account
# models/system/notice.go: func (n *Notice) TrStr() string
admin.notices.type_1
admin.notices.type_2
# modules/setting/ui.go
themes.names.
# routers/web/repo/helper.go: "repo.tree_path_not_found_"+refType
repo.tree_path_not_found_branch
repo.tree_path_not_found_commit
repo.tree_path_not_found_tag
# services/context/context.go
relativetime.
# templates/mail/issue/default.tmpl: $.locale.Tr
mail.issue.in_tree_path
# templates/org/team/new.tmpl: ctx.Locale.Tr (print "repo.permissions." $unit.Name ".read")}, and such
repo.permissions.
# templates/package/metadata/arch.tmpl: $.locale.Tr
packages.details.license
# templates/repo/editor/commit_form.tmpl: ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CanCommitToBranch.WontSignReason)
repo.signing.wont_sign.
# templates/repo/issue/view_content.tmpl: indirection via $closeTranslationKey
repo.issues.close
repo.pulls.close
# templates/repo/issue/view_content/comments.tmpl: indirection via $refTr
repo.issues.ref_closing_from
repo.issues.ref_issue_from
repo.issues.ref_pull_from
repo.issues.ref_reopening_from
# templates/repo/issue/view_content/comments.tmpl: ctx.Locale.Tr (printf "projects.type-%d.display_name" .OldProject.Type)
projects.type-1.display_name
projects.type-2.display_name
projects.type-3.display_name
# modules/migration/messenger.go: invocations of Messenger
# services/migrations/migrate.go: messenger(...)
# *: repo.migrate.*.description (unknown where they come from)
repo.migrate.

View file

@ -5,6 +5,7 @@
package main package main
import ( import (
"bufio"
"fmt" "fmt"
"go/ast" "go/ast"
goParser "go/parser" goParser "go/parser"
@ -12,6 +13,7 @@ import (
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strconv" "strconv"
"strings" "strings"
"text/template" "text/template"
@ -63,10 +65,120 @@ func InitLocaleTrFunctions() map[string][]uint {
type Handler struct { type Handler struct {
OnMsgid func(fset *token.FileSet, pos token.Pos, msgid string) OnMsgid func(fset *token.FileSet, pos token.Pos, msgid string)
OnMsgidPrefix func(fset *token.FileSet, pos token.Pos, msgidPrefix string)
OnUnexpectedInvoke func(fset *token.FileSet, pos token.Pos, funcname string, argc int) OnUnexpectedInvoke func(fset *token.FileSet, pos token.Pos, funcname string, argc int)
OnWarning func(fset *token.FileSet, pos token.Pos, msg string)
LocaleTrFunctions map[string][]uint LocaleTrFunctions map[string][]uint
} }
type StringTrie interface {
Matches(key []string) bool
}
type StringTrieMap map[string]StringTrie
func (m *StringTrieMap) Matches(key []string) bool {
if len(key) == 0 || m == nil {
return true
}
value, ok := (*m)[key[0]]
if !ok {
return false
}
if value == nil {
return true
}
return value.Matches(key[1:])
}
func (m *StringTrieMap) Insert(key []string) {
if m == nil {
return
}
switch len(key) {
case 0:
return
case 1:
(*m)[key[0]] = nil
default:
if value, ok := (*m)[key[0]]; ok {
if value == nil {
return
}
} else {
tmp := make(StringTrieMap)
(*m)[key[0]] = &tmp
}
(*m)[key[0]].(*StringTrieMap).Insert(key[1:])
}
}
func DecodeKeyForStm(key string) []string {
ret := strings.Split(key, ".")
i := len(ret)
for i > 0 && ret[i-1] == "" {
i--
}
return ret[:i]
}
func ParseAllowedMaskedUsages(fname string, usedMsgids *container.Set[string], allowedMaskedPrefixes *StringTrieMap) error {
file, err := os.Open(fname)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if strings.HasSuffix(line, ".") {
allowedMaskedPrefixes.Insert(DecodeKeyForStm(line))
} else {
(*usedMsgids)[line] = struct{}{}
}
}
return scanner.Err()
}
func (handler Handler) HandleGoTrBasicLit(fset *token.FileSet, argLit *ast.BasicLit) {
if argLit.Kind == token.STRING {
// extract string content
arg, err := strconv.Unquote(argLit.Value)
if err == nil {
// found interesting strings
if strings.HasSuffix(arg, ".") || strings.HasSuffix(arg, "_") {
handler.OnMsgidPrefix(fset, argLit.ValuePos, arg)
} else {
handler.OnMsgid(fset, argLit.ValuePos, arg)
}
}
}
}
func (handler Handler) HandleGoTrArgument(fset *token.FileSet, n ast.Expr) {
if argLit, ok := n.(*ast.BasicLit); ok {
handler.HandleGoTrBasicLit(fset, argLit)
} else if argBinExpr, ok := n.(*ast.BinaryExpr); ok {
if argBinExpr.Op != token.ADD {
// pass
} else if argLit, ok := argBinExpr.X.(*ast.BasicLit); ok && argLit.Kind == token.STRING {
// extract string content
arg, err := strconv.Unquote(argLit.Value)
if err == nil {
// found interesting strings
handler.OnMsgidPrefix(fset, argLit.ValuePos, arg)
}
}
}
}
// the `Handle*File` functions follow the following calling convention: // the `Handle*File` functions follow the following calling convention:
// * `fname` is the name of the input file // * `fname` is the name of the input file
// * `src` is either `nil` (then the function invokes `ReadFile` to read the file) // * `src` is either `nil` (then the function invokes `ReadFile` to read the file)
@ -74,7 +186,7 @@ type Handler struct {
func (handler Handler) HandleGoFile(fname string, src any) error { func (handler Handler) HandleGoFile(fname string, src any) error {
fset := token.NewFileSet() fset := token.NewFileSet()
node, err := goParser.ParseFile(fset, fname, src, goParser.SkipObjectResolution) node, err := goParser.ParseFile(fset, fname, src, goParser.SkipObjectResolution|goParser.ParseComments)
if err != nil { if err != nil {
return LocatedError{ return LocatedError{
Location: fname, Location: fname,
@ -86,44 +198,113 @@ func (handler Handler) HandleGoFile(fname string, src any) error {
ast.Inspect(node, func(n ast.Node) bool { ast.Inspect(node, func(n ast.Node) bool {
// search for function calls of the form `anything.Tr(any-string-lit, ...)` // search for function calls of the form `anything.Tr(any-string-lit, ...)`
call, ok := n.(*ast.CallExpr) if call, ok := n.(*ast.CallExpr); ok && len(call.Args) >= 1 {
if !ok || len(call.Args) < 1 { funSel, ok := call.Fun.(*ast.SelectorExpr)
return true if !ok {
} return true
}
funSel, ok := call.Fun.(*ast.SelectorExpr) ltf, ok := handler.LocaleTrFunctions[funSel.Sel.Name]
if !ok { if !ok {
return true return true
} }
ltf, ok := handler.LocaleTrFunctions[funSel.Sel.Name] var gotUnexpectedInvoke *int
if !ok {
return true
}
var gotUnexpectedInvoke *int for _, argNum := range ltf {
if len(call.Args) < int(argNum+1) {
argc := len(call.Args)
gotUnexpectedInvoke = &argc
} else {
handler.HandleGoTrArgument(fset, call.Args[int(argNum)])
}
}
for _, argNum := range ltf { if gotUnexpectedInvoke != nil {
if len(call.Args) >= int(argNum+1) { handler.OnUnexpectedInvoke(fset, funSel.Sel.NamePos, funSel.Sel.Name, *gotUnexpectedInvoke)
argLit, ok := call.Args[int(argNum)].(*ast.BasicLit) }
if !ok || argLit.Kind != token.STRING { } else if composite, ok := n.(*ast.CompositeLit); ok {
continue ident, ok := composite.Type.(*ast.Ident)
if !ok {
return true
}
// special case: models/unit/unit.go
if strings.HasSuffix(fname, "unit.go") && ident.Name == "Unit" && len(composite.Elts) == 6 {
// NameKey has index 2
// invoked like '{{ctx.Locale.Tr $unit.NameKey}}'
nameKey, ok := composite.Elts[2].(*ast.BasicLit)
if !ok || nameKey.Kind != token.STRING {
handler.OnWarning(fset, composite.Elts[2].Pos(), "unexpected initialization of 'Unit'")
return true
} }
// extract string content // extract string content
arg, err := strconv.Unquote(argLit.Value) arg, err := strconv.Unquote(nameKey.Value)
if err == nil { if err == nil {
// found interesting strings // found interesting strings
handler.OnMsgid(fset, argLit.ValuePos, arg) handler.OnMsgid(fset, nameKey.ValuePos, arg)
} }
} else {
argc := len(call.Args)
gotUnexpectedInvoke = &argc
} }
} } else if function, ok := n.(*ast.FuncDecl); ok {
matches := false
if function.Doc != nil {
for _, comment := range function.Doc.List {
if strings.TrimSpace(comment.Text) == "//llu:returnsTrKey" {
matches = true
break
}
}
}
if !matches {
return true
}
results := function.Type.Results.List
if len(results) != 1 {
handler.OnWarning(fset, function.Type.Func, fmt.Sprintf("function %s has unexpected return type; expected single return value", function.Name.Name))
return true
}
if gotUnexpectedInvoke != nil { ast.Inspect(function.Body, func(n ast.Node) bool {
handler.OnUnexpectedInvoke(fset, funSel.Sel.NamePos, funSel.Sel.Name, *gotUnexpectedInvoke) // search for return stmts
// TODO: what about nested functions?
if ret, ok := n.(*ast.ReturnStmt); ok {
for _, res := range ret.Results {
ast.Inspect(res, func(n ast.Node) bool {
if expr, ok := n.(ast.Expr); ok {
handler.HandleGoTrArgument(fset, expr)
}
return true
})
}
return false
}
return true
})
return true
} else if decl, ok := n.(*ast.GenDecl); ok && (decl.Tok == token.CONST || decl.Tok == token.VAR) {
matches := false
if decl.Doc != nil {
for _, comment := range decl.Doc.List {
if strings.TrimSpace(comment.Text) == "// llu:TrKeys" {
matches = true
break
}
}
}
if !matches {
return true
}
for _, spec := range decl.Specs {
// interpret all contained strings as message IDs
ast.Inspect(spec, func(n ast.Node) bool {
if argLit, ok := n.(*ast.BasicLit); ok {
handler.HandleGoTrBasicLit(fset, argLit)
return false
}
return true
})
}
} }
return true return true
@ -163,22 +344,26 @@ func (handler Handler) handleTemplateNode(fset *token.FileSet, node tmplParser.N
return return
} }
nodeChain, ok := nodeCommand.Args[0].(*tmplParser.ChainNode) funcname := ""
if !ok { if nodeChain, ok := nodeCommand.Args[0].(*tmplParser.ChainNode); ok {
return if nodeIdent, ok := nodeChain.Node.(*tmplParser.IdentifierNode); ok {
} if nodeIdent.Ident != "ctx" || len(nodeChain.Field) != 2 || nodeChain.Field[0] != "Locale" {
return
nodeIdent, ok := nodeChain.Node.(*tmplParser.IdentifierNode) }
if !ok || nodeIdent.Ident != "ctx" || len(nodeChain.Field) != 2 || nodeChain.Field[0] != "Locale" { funcname = nodeChain.Field[1]
return }
} } else if nodeField, ok := nodeCommand.Args[0].(*tmplParser.FieldNode); ok {
if len(nodeField.Ident) != 2 || !(nodeField.Ident[0] == "locale" || nodeField.Ident[0] == "Locale") {
ltf, ok := handler.LocaleTrFunctions[nodeChain.Field[1]] return
if !ok { }
return funcname = nodeField.Ident[1]
} }
var gotUnexpectedInvoke *int var gotUnexpectedInvoke *int
ltf, ok := handler.LocaleTrFunctions[funcname]
if !ok {
return
}
for _, argNum := range ltf { for _, argNum := range ltf {
if len(nodeCommand.Args) >= int(argNum+2) { if len(nodeCommand.Args) >= int(argNum+2) {
@ -195,7 +380,7 @@ func (handler Handler) handleTemplateNode(fset *token.FileSet, node tmplParser.N
} }
if gotUnexpectedInvoke != nil { if gotUnexpectedInvoke != nil {
handler.OnUnexpectedInvoke(fset, token.Pos(nodeChain.Pos), nodeChain.Field[1], *gotUnexpectedInvoke) handler.OnUnexpectedInvoke(fset, token.Pos(nodeCommand.Pos), funcname, *gotUnexpectedInvoke)
} }
default: default:
@ -273,6 +458,7 @@ func (handler Handler) HandleTemplateFile(fname string, src any) error {
// Possible command line flags: // Possible command line flags:
// //
// --allow-missing-msgids don't return an error code if missing message IDs are found // --allow-missing-msgids don't return an error code if missing message IDs are found
// --allow-unused-msgids don't return an error code if unused message IDs are found
// //
// EXIT CODES: // EXIT CODES:
// //
@ -281,13 +467,45 @@ func (handler Handler) HandleTemplateFile(fname string, src any) error {
// 2 unable to parse locale ini/json files // 2 unable to parse locale ini/json files
// 3 unable to parse go or text/template files // 3 unable to parse go or text/template files
// 4 found missing message IDs // 4 found missing message IDs
// 5 found unused message IDs
// 6 invalid command line argument
// 7 unable to parse allowed masked usages file
//
// SPECIAL GO DOC COMMENTS:
//
// //llu:returnsTrKey
// can be used in front of functions to indicate
// that the function returns message IDs
//
// // llu:TrKeys
// can be used in front of 'const' and 'var' blocks
// in order to mark all contained strings as message IDs
// //
//nolint:forbidigo //nolint:forbidigo
func main() { func main() {
allowMissingMsgids := false allowMissingMsgids := false
allowUnusedMsgids := false
usedMsgids := make(container.Set[string])
allowedMaskedPrefixes := make(StringTrieMap)
for _, arg := range os.Args[1:] { for _, arg := range os.Args[1:] {
if arg == "--allow-missing-msgids" { switch arg {
case "--allow-missing-msgids":
allowMissingMsgids = true allowMissingMsgids = true
case "--allow-unused-msgids":
allowUnusedMsgids = true
default:
if argval, found := strings.CutPrefix(arg, "--allow-masked-usages-from="); found {
if err := ParseAllowedMaskedUsages(argval, &usedMsgids, &allowedMaskedPrefixes); err != nil {
fmt.Printf("%s:\tERROR: unable to parse masked usages: %s\n", argval, err.Error())
os.Exit(7)
}
} else {
fmt.Printf("<command line>:\tERROR: unknown argument: %s\n", arg)
os.Exit(6)
}
} }
} }
@ -335,16 +553,25 @@ func main() {
gotAnyMsgidError := false gotAnyMsgidError := false
handler := Handler{ handler := Handler{
OnMsgidPrefix: func(fset *token.FileSet, pos token.Pos, msgidPrefix string) {
// TODO: perhaps we should check if we have any strings with such a prefix, but that's slow...
allowedMaskedPrefixes.Insert(DecodeKeyForStm(msgidPrefix))
},
OnMsgid: func(fset *token.FileSet, pos token.Pos, msgid string) { OnMsgid: func(fset *token.FileSet, pos token.Pos, msgid string) {
if !msgids.Contains(msgid) { if !msgids.Contains(msgid) {
gotAnyMsgidError = true gotAnyMsgidError = true
fmt.Printf("%s:\tmissing msgid: %s\n", fset.Position(pos).String(), msgid) fmt.Printf("%s:\tmissing msgid: %s\n", fset.Position(pos).String(), msgid)
} else {
usedMsgids[msgid] = struct{}{}
} }
}, },
OnUnexpectedInvoke: func(fset *token.FileSet, pos token.Pos, funcname string, argc int) { OnUnexpectedInvoke: func(fset *token.FileSet, pos token.Pos, funcname string, argc int) {
gotAnyMsgidError = true gotAnyMsgidError = true
fmt.Printf("%s:\tunexpected invocation of %s with %d arguments\n", fset.Position(pos).String(), funcname, argc) fmt.Printf("%s:\tunexpected invocation of %s with %d arguments\n", fset.Position(pos).String(), funcname, argc)
}, },
OnWarning: func(fset *token.FileSet, pos token.Pos, msg string) {
fmt.Printf("%s:\tWARNING: %s\n", fset.Position(pos).String(), msg)
},
LocaleTrFunctions: InitLocaleTrFunctions(), LocaleTrFunctions: InitLocaleTrFunctions(),
} }
@ -377,7 +604,27 @@ func main() {
os.Exit(1) os.Exit(1)
} }
unusedMsgids := []string{}
for msgid := range msgids {
if !usedMsgids.Contains(msgid) && !allowedMaskedPrefixes.Matches(DecodeKeyForStm(msgid)) {
unusedMsgids = append(unusedMsgids, msgid)
}
}
sort.Strings(unusedMsgids)
if len(unusedMsgids) != 0 {
fmt.Printf("=== unused msgids (%d): ===\n", len(unusedMsgids))
for _, msgid := range unusedMsgids {
fmt.Printf("- %s\n", msgid)
}
}
if !allowMissingMsgids && gotAnyMsgidError { if !allowMissingMsgids && gotAnyMsgidError {
os.Exit(4) os.Exit(4)
} }
if !allowUnusedMsgids && len(unusedMsgids) != 0 {
os.Exit(5)
}
} }

View file

@ -36,6 +36,7 @@ type ObjectVerification struct {
TrustStatus string TrustStatus string
} }
// llu:TrKeys
const ( const (
// BadSignature is used as the reason when the signature has a KeyID that is in the db // BadSignature is used as the reason when the signature has a KeyID that is in the db
// but no key that has that ID verifies the signature. This is a suspicious failure. // but no key that has that ID verifies the signature. This is a suspicious failure.

View file

@ -469,6 +469,8 @@ func (issue *Issue) GetLastEventTimestamp() timeutil.TimeStamp {
} }
// GetLastEventLabel returns the localization label for the current issue. // GetLastEventLabel returns the localization label for the current issue.
//
//llu:returnsTrKey
func (issue *Issue) GetLastEventLabel() string { func (issue *Issue) GetLastEventLabel() string {
if issue.IsClosed { if issue.IsClosed {
if issue.IsPull && issue.PullRequest.HasMerged { if issue.IsPull && issue.PullRequest.HasMerged {
@ -494,6 +496,8 @@ func (issue *Issue) GetLastComment(ctx context.Context) (*Comment, error) {
} }
// GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username. // GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username.
//
//llu:returnsTrKey
func (issue *Issue) GetLastEventLabelFake() string { func (issue *Issue) GetLastEventLabelFake() string {
if issue.IsClosed { if issue.IsClosed {
if issue.IsPull && issue.PullRequest.HasMerged { if issue.IsPull && issue.PullRequest.HasMerged {

View file

@ -48,6 +48,7 @@ const (
AbuseCategoryTypeIllegalContent // 4 AbuseCategoryTypeIllegalContent // 4
) )
// llu:TrKeys
var AbuseCategoriesTranslationKeys = map[AbuseCategoryType]string{ var AbuseCategoriesTranslationKeys = map[AbuseCategoryType]string{
AbuseCategoryTypeSpam: "moderation.abuse_category.spam", AbuseCategoryTypeSpam: "moderation.abuse_category.spam",
AbuseCategoryTypeMalware: "moderation.abuse_category.malware", AbuseCategoryTypeMalware: "moderation.abuse_category.malware",

View file

@ -182,6 +182,8 @@ func init() {
} }
// GetCardConfig retrieves the types of configurations project column cards could have // GetCardConfig retrieves the types of configurations project column cards could have
//
//llu:returnsTrKey
func GetCardConfig() []CardConfig { func GetCardConfig() []CardConfig {
return []CardConfig{ return []CardConfig{
{CardTypeTextOnly, "repo.projects.card_type.text_only"}, {CardTypeTextOnly, "repo.projects.card_type.text_only"},

View file

@ -26,6 +26,8 @@ const (
) )
// GetTemplateConfigs retrieves the template configs of configurations project columns could have // GetTemplateConfigs retrieves the template configs of configurations project columns could have
//
//llu:returnsTrKey
func GetTemplateConfigs() []TemplateConfig { func GetTemplateConfigs() []TemplateConfig {
return []TemplateConfig{ return []TemplateConfig{
{TemplateTypeNone, "repo.projects.type.none"}, {TemplateTypeNone, "repo.projects.type.none"},

View file

@ -271,7 +271,6 @@ type Unit struct {
Name string Name string
NameKey string NameKey string
URI string URI string
DescKey string
Idx int Idx int
MaxAccessMode perm.AccessMode // The max access mode of the unit. i.e. Read means this unit can only be read. MaxAccessMode perm.AccessMode // The max access mode of the unit. i.e. Read means this unit can only be read.
} }
@ -299,7 +298,6 @@ var (
"code", "code",
"repo.code", "repo.code",
"/", "/",
"repo.code.desc",
0, 0,
perm.AccessModeOwner, perm.AccessModeOwner,
} }
@ -309,7 +307,6 @@ var (
"issues", "issues",
"repo.issues", "repo.issues",
"/issues", "/issues",
"repo.issues.desc",
1, 1,
perm.AccessModeOwner, perm.AccessModeOwner,
} }
@ -319,7 +316,6 @@ var (
"ext_issues", "ext_issues",
"repo.ext_issues", "repo.ext_issues",
"/issues", "/issues",
"repo.ext_issues.desc",
1, 1,
perm.AccessModeRead, perm.AccessModeRead,
} }
@ -329,7 +325,6 @@ var (
"pulls", "pulls",
"repo.pulls", "repo.pulls",
"/pulls", "/pulls",
"repo.pulls.desc",
2, 2,
perm.AccessModeOwner, perm.AccessModeOwner,
} }
@ -339,7 +334,6 @@ var (
"releases", "releases",
"repo.releases", "repo.releases",
"/releases", "/releases",
"repo.releases.desc",
3, 3,
perm.AccessModeOwner, perm.AccessModeOwner,
} }
@ -349,7 +343,6 @@ var (
"wiki", "wiki",
"repo.wiki", "repo.wiki",
"/wiki", "/wiki",
"repo.wiki.desc",
4, 4,
perm.AccessModeOwner, perm.AccessModeOwner,
} }
@ -359,7 +352,6 @@ var (
"ext_wiki", "ext_wiki",
"repo.ext_wiki", "repo.ext_wiki",
"/wiki", "/wiki",
"repo.ext_wiki.desc",
4, 4,
perm.AccessModeRead, perm.AccessModeRead,
} }
@ -369,7 +361,6 @@ var (
"projects", "projects",
"repo.projects", "repo.projects",
"/projects", "/projects",
"repo.projects.desc",
5, 5,
perm.AccessModeOwner, perm.AccessModeOwner,
} }
@ -379,7 +370,6 @@ var (
"packages", "packages",
"repo.packages", "repo.packages",
"/packages", "/packages",
"packages.desc",
6, 6,
perm.AccessModeRead, perm.AccessModeRead,
} }
@ -389,7 +379,6 @@ var (
"actions", "actions",
"repo.actions", "repo.actions",
"/actions", "/actions",
"actions.unit.desc",
7, 7,
perm.AccessModeOwner, perm.AccessModeOwner,
} }

View file

@ -15,7 +15,6 @@ version = Version
powered_by = Powered by %s powered_by = Powered by %s
page = Page page = Page
template = Template template = Template
language = Language
notifications = Notifications notifications = Notifications
active_stopwatch = Active time tracker active_stopwatch = Active time tracker
tracked_time_summary = Summary of tracked time based on filters of issue list tracked_time_summary = Summary of tracked time based on filters of issue list
@ -53,11 +52,7 @@ webauthn_error_empty = You must set a name for this key.
webauthn_error_timeout = Timeout reached before your key could be read. Please reload this page and retry. webauthn_error_timeout = Timeout reached before your key could be read. Please reload this page and retry.
repository = Repository repository = Repository
organization = Organization
mirror = Mirror
new_mirror = New mirror
new_fork = New repository fork new_fork = New repository fork
new_project = New project
new_project_column = New column new_project_column = New column
admin_panel = Site administration admin_panel = Site administration
settings = Settings settings = Settings
@ -104,7 +99,6 @@ disabled = Disabled
locked = Locked locked = Locked
copy = Copy copy = Copy
copy_generic = Copy to clipboard
copy_url = Copy URL copy_url = Copy URL
copy_hash = Copy hash copy_hash = Copy hash
copy_path = Copy path copy_path = Copy path
@ -168,14 +162,6 @@ filter.private = Private
[search] [search]
search = Search… search = Search…
type_tooltip = Search type type_tooltip = Search type
fuzzy = Fuzzy
fuzzy_tooltip = Include results that also match the search term closely
union = Union
union_tooltip = Include results that match any of the whitespace separated keywords
exact = Exact
exact_tooltip = Include only results that match the exact search term
regexp = RegExp
regexp_tooltip = Interpret the search term as a regular expression
repo_kind = Search repos… repo_kind = Search repos…
user_kind = Search users… user_kind = Search users…
org_kind = Search orgs… org_kind = Search orgs…
@ -367,7 +353,6 @@ save_config_failed = Failed to save configuration: %v
enable_update_checker_helper_forgejo = It will periodically check for new Forgejo versions by checking a TXT DNS record at release.forgejo.org. enable_update_checker_helper_forgejo = It will periodically check for new Forgejo versions by checking a TXT DNS record at release.forgejo.org.
invalid_admin_setting = Administrator account setting is invalid: %v invalid_admin_setting = Administrator account setting is invalid: %v
invalid_log_root_path = The log path is invalid: %v invalid_log_root_path = The log path is invalid: %v
allow_dots_in_usernames = Allow users to use dots in their usernames. Doesn't affect existing accounts.
no_reply_address = Hidden email domain no_reply_address = Hidden email domain
no_reply_address_helper = Domain name for users with a hidden email address. For example, the username "joe" will be logged in Git as "joe@noreply.example.org" if the hidden email domain is set to "noreply.example.org". no_reply_address_helper = Domain name for users with a hidden email address. For example, the username "joe" will be logged in Git as "joe@noreply.example.org" if the hidden email domain is set to "noreply.example.org".
password_algorithm = Password hash algorithm password_algorithm = Password hash algorithm
@ -535,8 +520,6 @@ totp_enrolled.subject = You have activated TOTP as 2FA method
totp_enrolled.text_1.no_webauthn = You have just enabled TOTP for your account. This means that for all future logins to your account, you must use TOTP as a 2FA method. totp_enrolled.text_1.no_webauthn = You have just enabled TOTP for your account. This means that for all future logins to your account, you must use TOTP as a 2FA method.
totp_enrolled.text_1.has_webauthn = You have just enabled TOTP for your account. This means that for all future logins to your account, you could use TOTP as a 2FA method or use any of your security keys. totp_enrolled.text_1.has_webauthn = You have just enabled TOTP for your account. This means that for all future logins to your account, you could use TOTP as a 2FA method or use any of your security keys.
register_success = Registration successful
issue_assigned.pull = @%[1]s assigned you to pull request %[2]s in repository %[3]s. issue_assigned.pull = @%[1]s assigned you to pull request %[2]s in repository %[3]s.
issue_assigned.issue = @%[1]s assigned you to issue %[2]s in repository %[3]s. issue_assigned.issue = @%[1]s assigned you to issue %[2]s in repository %[3]s.
@ -581,7 +564,6 @@ yes = Yes
no = No no = No
confirm = Confirm confirm = Confirm
cancel = Cancel cancel = Cancel
modify = Update
[form] [form]
UserName = Username UserName = Username
@ -733,17 +715,14 @@ form.name_chars_not_allowed = Username "%s" contains invalid characters.
profile = Profile profile = Profile
account = Account account = Account
appearance = Appearance appearance = Appearance
password = Password
security = Security security = Security
avatar = Avatar avatar = Avatar
ssh_gpg_keys = SSH / GPG keys ssh_gpg_keys = SSH / GPG keys
applications = Applications applications = Applications
orgs = Organizations orgs = Organizations
repos = Repositories repos = Repositories
delete = Delete account
twofa = Two-factor authentication (TOTP) twofa = Two-factor authentication (TOTP)
organization = Organizations organization = Organizations
uid = UID
webauthn = Two-factor authentication (Security keys) webauthn = Two-factor authentication (Security keys)
blocked_users = Blocked users blocked_users = Blocked users
storage_overview = Storage overview storage_overview = Storage overview
@ -765,12 +744,10 @@ update_language = Change language
update_language_not_found = Language "%s" is not available. update_language_not_found = Language "%s" is not available.
update_language_success = Language has been updated. update_language_success = Language has been updated.
update_profile_success = Your profile has been updated. update_profile_success = Your profile has been updated.
change_username = Your username has been changed.
change_username_prompt = Note: Changing your username also changes your account URL. change_username_prompt = Note: Changing your username also changes your account URL.
change_username_redirect_prompt = The old username will redirect until someone claims it. change_username_redirect_prompt = The old username will redirect until someone claims it.
change_username_redirect_prompt.with_cooldown.one = The old username will be available to everyone after a cooldown period of %[1]d day. You can still reclaim the old username during the cooldown period. change_username_redirect_prompt.with_cooldown.one = The old username will be available to everyone after a cooldown period of %[1]d day. You can still reclaim the old username during the cooldown period.
change_username_redirect_prompt.with_cooldown.few = The old username will be available to everyone after a cooldown period of %[1]d days. You can still reclaim the old username during the cooldown period. change_username_redirect_prompt.with_cooldown.few = The old username will be available to everyone after a cooldown period of %[1]d days. You can still reclaim the old username during the cooldown period.
continue = Continue
cancel = Cancel cancel = Cancel
language = Language language = Language
language.title = Default language language.title = Default language
@ -961,9 +938,7 @@ permissions_list = Permissions:
manage_oauth2_applications = Manage OAuth2 applications manage_oauth2_applications = Manage OAuth2 applications
edit_oauth2_application = Edit OAuth2 Application edit_oauth2_application = Edit OAuth2 Application
oauth2_applications_desc = OAuth2 applications enables your third-party application to securely authenticate users at this Forgejo instance.
remove_oauth2_application = Remove OAuth2 Application remove_oauth2_application = Remove OAuth2 Application
remove_oauth2_application_desc = Removing an OAuth2 application will revoke access to all signed access tokens. Continue?
remove_oauth2_application_success = The application has been deleted. remove_oauth2_application_success = The application has been deleted.
create_oauth2_application = Create a new OAuth2 application create_oauth2_application = Create a new OAuth2 application
create_oauth2_application_button = Create application create_oauth2_application_button = Create application
@ -977,7 +952,6 @@ oauth2_client_id = Client ID
oauth2_client_secret = Client secret oauth2_client_secret = Client secret
oauth2_regenerate_secret = Regenerate secret oauth2_regenerate_secret = Regenerate secret
oauth2_regenerate_secret_hint = Lost your secret? oauth2_regenerate_secret_hint = Lost your secret?
oauth2_client_secret_hint = The secret will not be shown again after you leave or refresh this page. Please ensure that you have saved it.
oauth2_application_edit = Edit oauth2_application_edit = Edit
oauth2_application_create_description = OAuth2 applications gives your third-party application access to user accounts on this instance. oauth2_application_create_description = OAuth2 applications gives your third-party application access to user accounts on this instance.
oauth2_application_remove_description = Removing an OAuth2 application will prevent it from accessing authorized user accounts on this instance. Continue? oauth2_application_remove_description = Removing an OAuth2 application will prevent it from accessing authorized user accounts on this instance. Continue?
@ -1118,11 +1092,8 @@ open_with_editor = Open with %s
download_zip = Download ZIP download_zip = Download ZIP
download_tar = Download TAR.GZ download_tar = Download TAR.GZ
download_bundle = Download BUNDLE download_bundle = Download BUNDLE
generate_repo = Generate repository
generate_from = Generate from
repo_desc = Description repo_desc = Description
repo_desc_helper = Enter short description (optional) repo_desc_helper = Enter short description (optional)
repo_lang = Language
repo_gitignore_helper = Select .gitignore templates repo_gitignore_helper = Select .gitignore templates
repo_gitignore_helper_desc = Choose which files not to track from a list of templates for common languages. Typical artifacts generated by each language's build tools are included on .gitignore by default. repo_gitignore_helper_desc = Choose which files not to track from a list of templates for common languages. Typical artifacts generated by each language's build tools are included on .gitignore by default.
issue_labels = Labels issue_labels = Labels
@ -1160,7 +1131,6 @@ mirror_lfs = Large File Storage (LFS)
mirror_lfs_desc = Activate mirroring of LFS data. mirror_lfs_desc = Activate mirroring of LFS data.
mirror_lfs_endpoint = LFS endpoint mirror_lfs_endpoint = LFS endpoint
mirror_lfs_endpoint_desc = Sync will attempt to use the clone url to <a target="_blank" rel="noopener noreferrer" href="%s">determine the LFS server</a>. You can also specify a custom endpoint if the repository LFS data is stored somewhere else. mirror_lfs_endpoint_desc = Sync will attempt to use the clone url to <a target="_blank" rel="noopener noreferrer" href="%s">determine the LFS server</a>. You can also specify a custom endpoint if the repository LFS data is stored somewhere else.
mirror_last_synced = Last synchronized
mirror_password_placeholder = (Unchanged) mirror_password_placeholder = (Unchanged)
mirror_password_blank_placeholder = (Unset) mirror_password_blank_placeholder = (Unset)
mirror_password_help = Change the username to erase a stored password. mirror_password_help = Change the username to erase a stored password.
@ -1169,7 +1139,6 @@ stargazers = Stargazers
stars_remove_warning = This will remove all stars from this repository. stars_remove_warning = This will remove all stars from this repository.
forks = Forks forks = Forks
stars = Stars stars = Stars
reactions_more = and %d more
unit_disabled = The site administrator has disabled this repository section. unit_disabled = The site administrator has disabled this repository section.
language_other = Other language_other = Other
adopt_search = Enter username to search for unadopted repositories… (leave blank to find all) adopt_search = Enter username to search for unadopted repositories… (leave blank to find all)
@ -1199,7 +1168,6 @@ transfer.no_permission_to_accept = You do not have permission to accept this tra
transfer.no_permission_to_reject = You do not have permission to reject this transfer. transfer.no_permission_to_reject = You do not have permission to reject this transfer.
desc.private = Private desc.private = Private
desc.public = Public
desc.template = Template desc.template = Template
desc.internal = Internal desc.internal = Internal
desc.archived = Archived desc.archived = Archived
@ -1301,7 +1269,6 @@ unwatch = Unwatch
star = Star star = Star
unstar = Unstar unstar = Unstar
fork = Fork fork = Fork
download_archive = Download repository
more_operations = More operations more_operations = More operations
no_desc = No description no_desc = No description
@ -1315,29 +1282,22 @@ broken_message = The Git data underlying this repository cannot be read. Contact
code = Code code = Code
code.desc = Access source code, files, commits and branches. code.desc = Access source code, files, commits and branches.
branch = Branch
tree = Tree
clear_ref = `Clear current reference` clear_ref = `Clear current reference`
filter_branch_and_tag = Filter branch or tag filter_branch_and_tag = Filter branch or tag
find_tag = Find tag find_tag = Find tag
branches = Branches branches = Branches
tag = Tag
tags = Tags tags = Tags
issues = Issues issues = Issues
pulls = Pull requests pulls = Pull requests
project = Projects project = Projects
packages = Packages packages = Packages
actions = Actions actions = Actions
release = Release
releases = Releases releases = Releases
labels = Labels labels = Labels
milestones = Milestones milestones = Milestones
org_labels_desc = Organization level labels that can be used with <strong>all repositories</strong> under this organization org_labels_desc = Organization level labels that can be used with <strong>all repositories</strong> under this organization
org_labels_desc_manage = manage org_labels_desc_manage = manage
commits = Commits
commit = Commit
n_commit_one=%s commit n_commit_one=%s commit
n_commit_few=%s commits n_commit_few=%s commits
n_branch_one=%s branch n_branch_one=%s branch
@ -1431,7 +1391,6 @@ editor.propose_file_change = Propose file change
editor.new_branch_name = Name the new branch for this commit editor.new_branch_name = Name the new branch for this commit
editor.new_branch_name_desc = New branch name… editor.new_branch_name_desc = New branch name…
editor.cancel = Cancel editor.cancel = Cancel
editor.filename_cannot_be_empty = The filename cannot be empty.
editor.filename_is_invalid = The filename is invalid: "%s". editor.filename_is_invalid = The filename is invalid: "%s".
editor.invalid_commit_mail = Invalid mail for creating a commit. editor.invalid_commit_mail = Invalid mail for creating a commit.
editor.branch_does_not_exist = Branch "%s" does not exist in this repository. editor.branch_does_not_exist = Branch "%s" does not exist in this repository.
@ -1465,7 +1424,6 @@ editor.cherry_pick = Cherry-pick %s onto:
editor.revert = Revert %s onto: editor.revert = Revert %s onto:
editor.commit_email = Commit email editor.commit_email = Commit email
commits.desc = Browse source code change history.
commits.commits = Commits commits.commits = Commits
commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories. commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories.
commits.nothing_to_compare = These branches are equal. commits.nothing_to_compare = These branches are equal.
@ -1477,8 +1435,6 @@ commits.message = Message
commits.browse_further = Browse further commits.browse_further = Browse further
commits.renamed_from = Renamed from %s commits.renamed_from = Renamed from %s
commits.date = Date commits.date = Date
commits.older = Older
commits.newer = Newer
commits.signed_by = Signed by commits.signed_by = Signed by
commits.signed_by_untrusted_user = Signed by untrusted user commits.signed_by_untrusted_user = Signed by untrusted user
commits.signed_by_untrusted_user_unmatched = Signed by untrusted user who does not match committer commits.signed_by_untrusted_user_unmatched = Signed by untrusted user who does not match committer
@ -1503,7 +1459,6 @@ commitstatus.success = Success
ext_issues = External issues ext_issues = External issues
projects = Projects projects = Projects
projects.desc = Manage issues and pulls in project boards.
projects.description = Description (optional) projects.description = Description (optional)
projects.description_placeholder = Description projects.description_placeholder = Description
projects.create = Create project projects.create = Create project
@ -1540,7 +1495,6 @@ projects.card_type.desc = Card previews
projects.card_type.images_and_text = Images and text projects.card_type.images_and_text = Images and text
projects.card_type.text_only = Text only projects.card_type.text_only = Text only
issues.desc = Organize bug reports, tasks and milestones.
issues.filter_assignees = Filter assignee issues.filter_assignees = Filter assignee
issues.filter_milestones = Filter milestone issues.filter_milestones = Filter milestone
issues.filter_projects = Filter project issues.filter_projects = Filter project
@ -1583,7 +1537,6 @@ issues.new_label = New label
issues.new_label_placeholder = Label name issues.new_label_placeholder = Label name
issues.new_label_desc_placeholder = Description issues.new_label_desc_placeholder = Description
issues.create_label = Create label issues.create_label = Create label
issues.label_templates.title = Load a label preset
issues.label_templates.info = No labels exist yet. Create a label with "New label" or use a label preset: issues.label_templates.info = No labels exist yet. Create a label with "New label" or use a label preset:
issues.label_templates.helper = Select a label preset issues.label_templates.helper = Select a label preset
issues.label_templates.use = Use label preset issues.label_templates.use = Use label preset
@ -1628,7 +1581,6 @@ issues.filter_assginee_no_assignee = No assignee
issues.filter_poster = Author issues.filter_poster = Author
issues.filter_poster_no_select = All authors issues.filter_poster_no_select = All authors
issues.filter_type = Type issues.filter_type = Type
issues.filter_type.all_issues = All issues
issues.filter_type.all_pull_requests = All pull requests issues.filter_type.all_pull_requests = All pull requests
issues.filter_type.assigned_to_you = Assigned to you issues.filter_type.assigned_to_you = Assigned to you
issues.filter_type.created_by_you = Created by you issues.filter_type.created_by_you = Created by you
@ -1751,7 +1703,6 @@ issues.label.filter_sort.reverse_by_size = Largest size
issues.num_participants_one = %d participant issues.num_participants_one = %d participant
issues.num_participants_few = %d participants issues.num_participants_few = %d participants
issues.attachment.open_tab = `Click to see "%s" in a new tab` issues.attachment.open_tab = `Click to see "%s" in a new tab`
issues.attachment.download = `Click to download "%s"`
issues.subscribe = Subscribe issues.subscribe = Subscribe
issues.unsubscribe = Unsubscribe issues.unsubscribe = Unsubscribe
issues.unpin_issue = Unpin issue issues.unpin_issue = Unpin issue
@ -1799,7 +1750,6 @@ issues.del_time_history= `deleted spent time %s`
issues.add_time_hours = Hours issues.add_time_hours = Hours
issues.add_time_minutes = Minutes issues.add_time_minutes = Minutes
issues.add_time_sum_to_small = No time was entered. issues.add_time_sum_to_small = No time was entered.
issues.time_spent_total = Total time spent
issues.time_spent_from_all_authors = `Total time spent: %s` issues.time_spent_from_all_authors = `Total time spent: %s`
issues.due_date = Due date issues.due_date = Due date
issues.push_commit_1 = added %d commit %s issues.push_commit_1 = added %d commit %s
@ -1855,7 +1805,6 @@ issues.review.dismissed_label = Dismissed
issues.review.left_comment = left a comment issues.review.left_comment = left a comment
issues.review.content.empty = You need to leave a comment indicating the requested change(s). issues.review.content.empty = You need to leave a comment indicating the requested change(s).
issues.review.reject = requested changes %s issues.review.reject = requested changes %s
issues.review.wait = was requested for review %s
issues.review.add_review_request = requested review from %[1]s %[2]s issues.review.add_review_request = requested review from %[1]s %[2]s
issues.review.add_review_requests = requested reviews from %[1]s %[2]s issues.review.add_review_requests = requested reviews from %[1]s %[2]s
issues.review.remove_review_request = removed review request for %[1]s %[2]s issues.review.remove_review_request = removed review request for %[1]s %[2]s
@ -1885,13 +1834,11 @@ issues.content_history.delete_from_history_confirm = Delete from history?
issues.content_history.options = Options issues.content_history.options = Options
issues.blocked_by_user = You cannot create issues in this repository because you are blocked by the repository owner. issues.blocked_by_user = You cannot create issues in this repository because you are blocked by the repository owner.
comment.blocked_by_user = Commenting is not possible because you are blocked by the repository owner or the author. comment.blocked_by_user = Commenting is not possible because you are blocked by the repository owner or the author.
issues.reopen.blocked_by_user = You cannot reopen this issue because you are blocked by the repository owner or the poster of this issue.
issues.summary_card_alt = Summary card of an issue titled "%s" in repository %s issues.summary_card_alt = Summary card of an issue titled "%s" in repository %s
compare.compare_base = base compare.compare_base = base
compare.compare_head = compare compare.compare_head = compare
pulls.desc = Enable pull requests and code reviews.
pulls.new = New pull request pulls.new = New pull request
pulls.view = View pull request pulls.view = View pull request
pulls.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes pulls.edit.already_changed = Unable to save changes to the pull request. It appears the content has already been changed by another user. Please refresh the page and try editing again to avoid overwriting their changes
@ -1917,7 +1864,6 @@ pulls.show_changes_since_your_last_review = Show changes since your last review
pulls.showing_only_single_commit = Showing only changes of commit %[1]s pulls.showing_only_single_commit = Showing only changes of commit %[1]s
pulls.showing_specified_commit_range = Showing only changes between %[1]s..%[2]s pulls.showing_specified_commit_range = Showing only changes between %[1]s..%[2]s
pulls.select_commit_hold_shift_for_range = Select commit. Hold shift + click to select a range pulls.select_commit_hold_shift_for_range = Select commit. Hold shift + click to select a range
pulls.review_only_possible_for_full_diff = Review is only possible when viewing the full diff
pulls.filter_changes_by_commit = Filter by commit pulls.filter_changes_by_commit = Filter by commit
pulls.nothing_to_compare = These branches are equal. There is no need to create a pull request. pulls.nothing_to_compare = These branches are equal. There is no need to create a pull request.
pulls.nothing_to_compare_have_tag = The selected branches/tags are equal. pulls.nothing_to_compare_have_tag = The selected branches/tags are equal.
@ -2107,7 +2053,6 @@ ext_wiki = External Wiki
wiki = Wiki wiki = Wiki
wiki.welcome = Welcome to the wiki. wiki.welcome = Welcome to the wiki.
wiki.welcome_desc = The wiki lets you write and share documentation with collaborators. wiki.welcome_desc = The wiki lets you write and share documentation with collaborators.
wiki.desc = Write and share documentation with collaborators.
wiki.create_first_page = Create the first page wiki.create_first_page = Create the first page
wiki.page = Page wiki.page = Page
wiki.filter_page = Filter page wiki.filter_page = Filter page
@ -2737,11 +2682,9 @@ diff.protected = Protected
diff.image.side_by_side = Side by side diff.image.side_by_side = Side by side
diff.image.swipe = Swipe diff.image.swipe = Swipe
diff.image.overlay = Overlay diff.image.overlay = Overlay
diff.has_escaped = This line has hidden Unicode characters
diff.show_file_tree = Show file tree diff.show_file_tree = Show file tree
diff.hide_file_tree = Hide file tree diff.hide_file_tree = Hide file tree
releases.desc = Track project versions and downloads.
release.releases = Releases release.releases = Releases
release.detail = Release details release.detail = Release details
release.tags = Tags release.tags = Tags
@ -2753,7 +2696,6 @@ release.compare = Compare
release.edit = Edit release.edit = Edit
release.ahead.commits = <strong>%d</strong> commits release.ahead.commits = <strong>%d</strong> commits
release.ahead.target = to %s since this release release.ahead.target = to %s since this release
tag.ahead.target = to %s since this tag
release.source_code = Source code release.source_code = Source code
release.new_subheader = Releases organize project versions. release.new_subheader = Releases organize project versions.
release.edit_subheader = Releases organize project versions. release.edit_subheader = Releases organize project versions.
@ -2781,7 +2723,6 @@ release.deletion_tag_success = The tag has been deleted.
release.tag_name_already_exist = A release with this tag name already exists. release.tag_name_already_exist = A release with this tag name already exists.
release.tag_name_invalid = The tag name is not valid. release.tag_name_invalid = The tag name is not valid.
release.tag_name_protected = The tag name is protected. release.tag_name_protected = The tag name is protected.
release.tag_already_exist = This tag name already exists.
release.downloads = Downloads release.downloads = Downloads
release.download_count_one = %s download release.download_count_one = %s download
release.download_count_few = %s downloads release.download_count_few = %s downloads
@ -2802,7 +2743,6 @@ release.summary_card_alt = Summary card of an release titled "%s" in repository
branch.name = Branch name branch.name = Branch name
branch.already_exists = A branch named "%s" already exists. branch.already_exists = A branch named "%s" already exists.
branch.delete_head = Delete
branch.delete = Delete branch "%s" branch.delete = Delete branch "%s"
branch.delete_html = Delete branch branch.delete_html = Delete branch
branch.delete_desc = Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue? branch.delete_desc = Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
@ -2842,7 +2782,6 @@ tag.create_tag_from = Create new tag from "%s"
tag.create_success = Tag "%s" has been created. tag.create_success = Tag "%s" has been created.
topic.manage_topics = Manage topics topic.manage_topics = Manage topics
topic.done = Done
topic.count_prompt = You cannot select more than 25 topics topic.count_prompt = You cannot select more than 25 topics
topic.format_prompt = Topics must start with a letter or number, can include dashes ("-") and dots ("."), can be up to 35 characters long. Letters must be lowercase. topic.format_prompt = Topics must start with a letter or number, can include dashes ("-") and dots ("."), can be up to 35 characters long. Letters must be lowercase.
@ -2913,7 +2852,6 @@ form.create_org_not_allowed = You are not allowed to create an organization.
settings = Settings settings = Settings
settings.options = Organization settings.options = Organization
settings.full_name = Full name
settings.email = Contact email settings.email = Contact email
settings.website = Website settings.website = Website
settings.location = Location settings.location = Location
@ -2943,8 +2881,6 @@ settings.hooks_desc = Add webhooks which will be triggered for <strong>all repos
settings.labels_desc = Add labels which can be used on issues for <strong>all repositories</strong> under this organization. settings.labels_desc = Add labels which can be used on issues for <strong>all repositories</strong> under this organization.
members.membership_visibility = Membership visibility:
members.public = Visible
members.public_helper = Make hidden members.public_helper = Make hidden
members.private = Hidden members.private = Hidden
members.private_helper = Make visible members.private_helper = Make visible
@ -2955,8 +2891,6 @@ members.remove = Remove
members.remove.detail = Remove %[1]s from %[2]s? members.remove.detail = Remove %[1]s from %[2]s?
members.leave = Leave members.leave = Leave
members.leave.detail = Are you sure you want to leave organization "%s"? members.leave.detail = Are you sure you want to leave organization "%s"?
members.invite_desc = Add a new member to %s:
members.invite_now = Invite now
teams.join = Join teams.join = Join
teams.leave = Leave teams.leave = Leave
@ -2974,7 +2908,6 @@ teams.admin_access_helper = Members can pull and push to team repositories and a
teams.no_desc = This team has no description teams.no_desc = This team has no description
teams.settings = Settings teams.settings = Settings
teams.owners_permission_desc = Owners have full access to <strong>all repositories</strong> and have <strong>administrator access</strong> to the organization. teams.owners_permission_desc = Owners have full access to <strong>all repositories</strong> and have <strong>administrator access</strong> to the organization.
teams.members = Team members
teams.update_settings = Update settings teams.update_settings = Update settings
teams.delete_team = Delete team teams.delete_team = Delete team
teams.add_team_member = Add team member teams.add_team_member = Add team member
@ -2984,11 +2917,7 @@ teams.delete_team_title = Delete team
teams.delete_team_desc = Deleting a team revokes repository access from its members. Continue? teams.delete_team_desc = Deleting a team revokes repository access from its members. Continue?
teams.delete_team_success = The team has been deleted. teams.delete_team_success = The team has been deleted.
teams.admin_permission_desc = This team grants <strong>Administrator</strong> access: members can read from, push to and add collaborators to team repositories. teams.admin_permission_desc = This team grants <strong>Administrator</strong> access: members can read from, push to and add collaborators to team repositories.
teams.create_repo_permission_desc = Additionally, this team grants <strong>Create repository</strong> permission: members can create new repositories in organization.
teams.repositories = Team repositories
teams.remove_all_repos_title = Remove all team repositories
teams.remove_all_repos_desc = This will remove all repositories from the team. teams.remove_all_repos_desc = This will remove all repositories from the team.
teams.add_all_repos_title = Add all repositories
teams.add_all_repos_desc = This will add all the organization's repositories to the team. teams.add_all_repos_desc = This will add all the organization's repositories to the team.
teams.add_nonexistent_repo = The repository you're trying to add doesn't exist, please create it first. teams.add_nonexistent_repo = The repository you're trying to add doesn't exist, please create it first.
teams.add_duplicate_users = User is already a team member. teams.add_duplicate_users = User is already a team member.
@ -3201,7 +3130,6 @@ repos.unadopted = Unadopted repositories
repos.unadopted.no_more = No unadopted repositories found. repos.unadopted.no_more = No unadopted repositories found.
repos.owner = Owner repos.owner = Owner
repos.name = Name repos.name = Name
repos.private = Private
repos.issues = Issues repos.issues = Issues
repos.size = Size repos.size = Size
repos.lfs_size = LFS size repos.lfs_size = LFS size
@ -3240,7 +3168,6 @@ auths.updated = Updated
auths.auth_type = Authentication type auths.auth_type = Authentication type
auths.auth_name = Authentication name auths.auth_name = Authentication name
auths.security_protocol = Security protocol auths.security_protocol = Security protocol
auths.domain = Domain
auths.host = Host auths.host = Host
auths.port = Port auths.port = Port
auths.bind_dn = Bind DN auths.bind_dn = Bind DN
@ -3270,7 +3197,6 @@ auths.user_attribute_in_group = User attribute listed in group
auths.map_group_to_team = Map LDAP groups to Organization teams (leave the field empty to skip) auths.map_group_to_team = Map LDAP groups to Organization teams (leave the field empty to skip)
auths.map_group_to_team_removal = Remove users from synchronized teams if user does not belong to corresponding LDAP group auths.map_group_to_team_removal = Remove users from synchronized teams if user does not belong to corresponding LDAP group
auths.enable_ldap_groups = Enable LDAP groups auths.enable_ldap_groups = Enable LDAP groups
auths.ms_ad_sa = MS AD search attributes
auths.smtp_auth = SMTP authentication type auths.smtp_auth = SMTP authentication type
auths.smtphost = SMTP host auths.smtphost = SMTP host
auths.smtpport = SMTP port auths.smtpport = SMTP port
@ -3336,7 +3262,6 @@ auths.delete_auth_desc = Deleting an authentication source prevents users from u
auths.still_in_used = The authentication source is still in use. Convert or delete any users using this authentication source first. auths.still_in_used = The authentication source is still in use. Convert or delete any users using this authentication source first.
auths.deletion_success = The authentication source has been deleted. auths.deletion_success = The authentication source has been deleted.
auths.login_source_exist = The authentication source "%s" already exists. auths.login_source_exist = The authentication source "%s" already exists.
auths.login_source_of_type_exist = An authentication source of this type already exists.
auths.unable_to_initialize_openid = Unable to initialize OpenID Connect Provider: %s auths.unable_to_initialize_openid = Unable to initialize OpenID Connect Provider: %s
auths.invalid_openIdConnectAutoDiscoveryURL = Invalid Auto Discovery URL (this must be a valid URL starting with http:// or https://) auths.invalid_openIdConnectAutoDiscoveryURL = Invalid Auto Discovery URL (this must be a valid URL starting with http:// or https://)
@ -3355,7 +3280,6 @@ config.run_mode = Run mode
config.git_version = Git version config.git_version = Git version
config.app_data_path = App data path config.app_data_path = App data path
config.repo_root_path = Repository root path config.repo_root_path = Repository root path
config.lfs_root_path = LFS root path
config.log_file_root_path = Log path config.log_file_root_path = Log path
config.script_type = Script type config.script_type = Script type
config.reverse_auth_user = Reverse proxy authentication user config.reverse_auth_user = Reverse proxy authentication user
@ -3433,9 +3357,6 @@ config.send_test_mail_submit = Send
config.test_mail_failed = Failed to send a test email to "%s": %v config.test_mail_failed = Failed to send a test email to "%s": %v
config.test_mail_sent = A test email has been sent to "%s". config.test_mail_sent = A test email has been sent to "%s".
config.oauth_config = OAuth configuration
config.oauth_enabled = Enabled
config.cache_config = Cache configuration config.cache_config = Cache configuration
config.cache_adapter = Cache adapter config.cache_adapter = Cache adapter
config.cache_interval = Cache interval config.cache_interval = Cache interval
@ -3454,10 +3375,8 @@ config.cookie_name = Cookie name
config.gc_interval_time = GC interval time config.gc_interval_time = GC interval time
config.session_life_time = Session lifetime config.session_life_time = Session lifetime
config.https_only = HTTPS only config.https_only = HTTPS only
config.cookie_life_time = Cookie lifetime
config.picture_config = Picture and avatar configuration config.picture_config = Picture and avatar configuration
config.picture_service = Picture service
config.disable_gravatar = Disable Gravatar config.disable_gravatar = Disable Gravatar
config.enable_federated_avatar = Enable federated avatars config.enable_federated_avatar = Enable federated avatars
config.open_with_editor_app_help = The "Open with" editors for the clone menu. If left empty, the default will be used. Expand to see the default. config.open_with_editor_app_help = The "Open with" editors for the clone menu. If left empty, the default will be used. Expand to see the default.
@ -3477,7 +3396,6 @@ config.git_gc_timeout = GC Operation timeout
config.log_config = Log configuration config.log_config = Log configuration
config.logger_name_fmt = Logger: %s config.logger_name_fmt = Logger: %s
config.disabled_logger = Disabled config.disabled_logger = Disabled
config.access_log_mode = Access log mode
config.access_log_template = Access log template config.access_log_template = Access log template
config.xorm_log_sql = Log SQL config.xorm_log_sql = Log SQL
@ -3496,14 +3414,10 @@ monitor.stacktrace = Stacktrace
monitor.processes_count = %d Processes monitor.processes_count = %d Processes
monitor.download_diagnosis_report = Download diagnosis report monitor.download_diagnosis_report = Download diagnosis report
monitor.duration = Duration (s) monitor.duration = Duration (s)
monitor.desc = Description
monitor.start = Start Time
monitor.execute_time = Execution Time
monitor.last_execution_result = Result monitor.last_execution_result = Result
monitor.process.cancel = Cancel process monitor.process.cancel = Cancel process
monitor.process.cancel_desc = Canceling a process may cause data loss monitor.process.cancel_desc = Canceling a process may cause data loss
monitor.process.cancel_notices = Cancel: <strong>%s</strong>? monitor.process.cancel_notices = Cancel: <strong>%s</strong>?
monitor.process.children = Children
monitor.queues = Queues monitor.queues = Queues
monitor.queue = Queue: %s monitor.queue = Queue: %s
@ -3642,11 +3556,9 @@ error.probable_bad_default_signature = WARNING! Although the default key has thi
[units] [units]
unit = Unit unit = Unit
error.no_unit_allowed_repo = You are not allowed to access any section of this repository. error.no_unit_allowed_repo = You are not allowed to access any section of this repository.
error.unit_not_allowed = You are not allowed to access this repository section.
[packages] [packages]
title = Packages title = Packages
desc = Manage repository packages.
empty = There are no packages yet. empty = There are no packages yet.
empty.documentation = For more information on the package registry, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>. empty.documentation = For more information on the package registry, see <a target="_blank" rel="noopener noreferrer" href="%s">the documentation</a>.
empty.repo = Did you upload a package, but it's not shown here? Go to <a href="%[1]s">package settings</a> and link it to this repo. empty.repo = Did you upload a package, but it's not shown here? Go to <a href="%[1]s">package settings</a> and link it to this repo.
@ -3707,7 +3619,6 @@ composer.registry = Setup this registry in your <code>~/.composer/config.json</c
composer.install = To install the package using Composer, run the following command: composer.install = To install the package using Composer, run the following command:
composer.dependencies = Dependencies composer.dependencies = Dependencies
composer.dependencies.development = Development dependencies composer.dependencies.development = Development dependencies
conan.details.repository = Repository
conan.registry = Setup this registry from the command line: conan.registry = Setup this registry from the command line:
conan.install = To install the package using Conan, run the following command: conan.install = To install the package using Conan, run the following command:
conda.registry = Setup this registry as a Conda repository in your <code>.condarc</code> file: conda.registry = Setup this registry as a Conda repository in your <code>.condarc</code> file:
@ -3806,7 +3717,6 @@ owner.settings.cleanuprules.none = There are no cleanup rules yet.
owner.settings.cleanuprules.preview = Cleanup rule preview owner.settings.cleanuprules.preview = Cleanup rule preview
owner.settings.cleanuprules.preview.overview = %d packages are scheduled to be removed. owner.settings.cleanuprules.preview.overview = %d packages are scheduled to be removed.
owner.settings.cleanuprules.preview.none = Cleanup rule does not match any packages. owner.settings.cleanuprules.preview.none = Cleanup rule does not match any packages.
owner.settings.cleanuprules.enabled = Enabled
owner.settings.cleanuprules.pattern_full_match = Apply pattern to full package name owner.settings.cleanuprules.pattern_full_match = Apply pattern to full package name
owner.settings.cleanuprules.keep.title = Versions that match these rules are kept, even if they match a removal rule below. owner.settings.cleanuprules.keep.title = Versions that match these rules are kept, even if they match a removal rule below.
owner.settings.cleanuprules.keep.count = Keep the most recent owner.settings.cleanuprules.keep.count = Keep the most recent
@ -3840,7 +3750,6 @@ management = Manage secrets
[actions] [actions]
actions = Actions actions = Actions
unit.desc = Manage integrated CI/CD pipelines with Forgejo Actions.
status.unknown = Unknown status.unknown = Unknown
status.waiting = Waiting status.waiting = Waiting
@ -3931,7 +3840,6 @@ variables.none = There are no variables yet.
variables.deletion = Remove variable variables.deletion = Remove variable
variables.deletion.description = Removing a variable is permanent and cannot be undone. Continue? variables.deletion.description = Removing a variable is permanent and cannot be undone. Continue?
variables.description = Variables will be passed to certain actions and cannot be read otherwise. variables.description = Variables will be passed to certain actions and cannot be read otherwise.
variables.id_not_exist = Variable with ID %d does not exist.
variables.edit = Edit Variable variables.edit = Edit Variable
variables.not_found = Failed to find the variable. variables.not_found = Failed to find the variable.
variables.deletion.failed = Failed to remove variable. variables.deletion.failed = Failed to remove variable.

View file

@ -427,6 +427,7 @@ func (diffFile *DiffFile) ShouldBeHidden() bool {
return diffFile.IsGenerated || diffFile.IsViewed return diffFile.IsGenerated || diffFile.IsViewed
} }
//llu:returnsTrKey
func (diffFile *DiffFile) ModeTranslationKey(mode string) string { func (diffFile *DiffFile) ModeTranslationKey(mode string) string {
switch mode { switch mode {
case "040000": case "040000":