diff --git a/.gitignore b/.gitignore
index e1200ce4e8..ffc493a51d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,6 +55,8 @@ cpu.out
*.log
*.log.*.gz
+/build/lint-locale/lint-locale
+/build/lint-locale-usage/lint-locale-usage
/gitea
/gitea-vet
/debug
diff --git a/CODEOWNERS b/CODEOWNERS
index 34cdceca09..25a3f698dc 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -39,4 +39,5 @@ options/locale/.* @0ko
options/locale_next/.* @0ko
# Personal interest
+build/lint-locale-usage/.* @fogti
.*/webhook.* @oliverpool
diff --git a/Makefile b/Makefile
index 9b87eb4af0..001d5ccf7c 100644
--- a/Makefile
+++ b/Makefile
@@ -463,7 +463,7 @@ lint-locale:
.PHONY: lint-locale-usage
lint-locale-usage:
- $(GO) run build/lint-locale-usage/lint-locale-usage.go
+ $(GO) run ./build/lint-locale-usage --allow-masked-usages-from=build/lint-locale-usage/allowed-masked-usage.txt
.PHONY: lint-md
lint-md: node_modules
diff --git a/build/lint-locale-usage/allowed-masked-usage.txt b/build/lint-locale-usage/allowed-masked-usage.txt
new file mode 100644
index 0000000000..f2c599a365
--- /dev/null
+++ b/build/lint-locale-usage/allowed-masked-usage.txt
@@ -0,0 +1,50 @@
+# translation tooling test keys
+meta.last_line
+translation_meta.test
+
+# models/admin/task.go: instances of $TranslatableMessage.Format
+# this also gets instantiated as a Messenger once
+repo.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.
+
+# services/context/context.go
+relativetime.
+
+# templates/mail/issue/default.tmpl: $.locale.Tr
+mail.issue.in_tree_path
+
+# templates/package/metadata/arch.tmpl: $.locale.Tr
+packages.details.license
+
+# 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.
+projects.type-1.display_name
+projects.type-2.display_name
+projects.type-3.display_name
+
+# templates/repo/settings/webhook/link_menu.tmpl, templates/webhook/new.tmpl: repo.settings.web_hook_name_
+# tests/integration/repo_archive_text_test.go
+repo.settings.
diff --git a/build/lint-locale-usage/handle-go.go b/build/lint-locale-usage/handle-go.go
new file mode 100644
index 0000000000..a91a210313
--- /dev/null
+++ b/build/lint-locale-usage/handle-go.go
@@ -0,0 +1,217 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package main
+
+import (
+ "fmt"
+ "go/ast"
+ goParser "go/parser"
+ "go/token"
+ "strconv"
+ "strings"
+)
+
+func (handler Handler) handleGoTrBasicLit(fset *token.FileSet, argLit *ast.BasicLit, prefix string) {
+ if argLit.Kind == token.STRING {
+ // extract string content
+ arg, err := strconv.Unquote(argLit.Value)
+ if err != nil {
+ return
+ }
+ // found interesting strings
+ arg = prefix + arg
+ if strings.HasSuffix(arg, ".") || strings.HasSuffix(arg, "_") {
+ prep, trunc := PrepareMsgidPrefix(arg)
+ if trunc {
+ handler.OnWarning(fset, argLit.ValuePos, fmt.Sprintf("needed to truncate message id prefix: %s", arg))
+ }
+ handler.OnMsgidPrefix(fset, argLit.ValuePos, prep, trunc)
+ } else {
+ handler.OnMsgid(fset, argLit.ValuePos, arg)
+ }
+ }
+}
+
+func (handler Handler) handleGoTrArgument(fset *token.FileSet, n ast.Expr, prefix string) {
+ if argLit, ok := n.(*ast.BasicLit); ok {
+ handler.handleGoTrBasicLit(fset, argLit, prefix)
+ } 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 {
+ return
+ }
+ // found interesting strings
+ arg = prefix + arg
+ prep, trunc := PrepareMsgidPrefix(arg)
+ if trunc {
+ handler.OnWarning(fset, argLit.ValuePos, fmt.Sprintf("needed to truncate message id prefix: %s", arg))
+ }
+ handler.OnMsgidPrefix(fset, argLit.ValuePos, prep, trunc)
+ }
+ }
+}
+
+func (handler Handler) handleGoCommentGroup(fset *token.FileSet, cg *ast.CommentGroup, commentPrefix string) *string {
+ if cg == nil {
+ return nil
+ }
+ var matches []token.Pos
+ matchInsPrefix := ""
+ commentPrefix = "//" + commentPrefix
+ for _, comment := range cg.List {
+ ctxt := strings.TrimSpace(comment.Text)
+ if ctxt == commentPrefix {
+ matches = append(matches, comment.Slash)
+ } else if after, found := strings.CutPrefix(ctxt, commentPrefix+"Suffix "); found {
+ matches = append(matches, comment.Slash)
+ matchInsPrefix = strings.TrimSpace(after)
+ }
+ }
+ switch len(matches) {
+ case 0:
+ return nil
+ case 1:
+ return &matchInsPrefix
+ default:
+ handler.OnWarning(
+ fset,
+ matches[0],
+ fmt.Sprintf("encountered multiple %s... directives, ignoring", strings.TrimSpace(commentPrefix)),
+ )
+ return &matchInsPrefix
+ }
+}
+
+// the `Handle*File` functions follow the following calling convention:
+// * `fname` is the name of the input file
+// * `src` is either `nil` (then the function invokes `ReadFile` to read the file)
+// or the contents of the file as {`[]byte`, or a `string`}
+
+func (handler Handler) HandleGoFile(fname string, src any) error {
+ fset := token.NewFileSet()
+ node, err := goParser.ParseFile(fset, fname, src, goParser.SkipObjectResolution|goParser.ParseComments)
+ if err != nil {
+ return LocatedError{
+ Location: fname,
+ Kind: "Go parser",
+ Err: err,
+ }
+ }
+
+ ast.Inspect(node, func(n ast.Node) bool {
+ // search for function calls of the form `anything.Tr(any-string-lit, ...)`
+
+ switch n2 := n.(type) {
+ case *ast.CallExpr:
+ if len(n2.Args) == 0 {
+ return true
+ }
+ funSel, ok := n2.Fun.(*ast.SelectorExpr)
+ if !ok {
+ return true
+ }
+
+ ltf, ok := handler.LocaleTrFunctions[funSel.Sel.Name]
+ if !ok {
+ return true
+ }
+
+ var gotUnexpectedInvoke *int
+
+ for _, argNum := range ltf {
+ if len(n2.Args) <= int(argNum) {
+ argc := len(n2.Args)
+ gotUnexpectedInvoke = &argc
+ } else {
+ handler.handleGoTrArgument(fset, n2.Args[int(argNum)], "")
+ }
+ }
+
+ if gotUnexpectedInvoke != nil {
+ handler.OnUnexpectedInvoke(fset, funSel.Sel.NamePos, funSel.Sel.Name, *gotUnexpectedInvoke)
+ }
+ case *ast.CompositeLit:
+ ident, ok := n2.Type.(*ast.Ident)
+ if !ok {
+ return true
+ }
+
+ // special case: models/unit/unit.go
+ if strings.HasSuffix(fname, "unit.go") && ident.Name == "Unit" {
+ if len(n2.Elts) != 6 {
+ handler.OnWarning(fset, n2.Pos(), "unexpected initialization of 'Unit' (unexpected number of arguments)")
+ }
+ // NameKey has index 2
+ // invoked like '{{ctx.Locale.Tr $unit.NameKey}}'
+ nameKey, ok := n2.Elts[2].(*ast.BasicLit)
+ if !ok || nameKey.Kind != token.STRING {
+ handler.OnWarning(fset, n2.Elts[2].Pos(), "unexpected initialization of 'Unit' (expected string literal as NameKey)")
+ return true
+ }
+
+ // extract string content
+ arg, err := strconv.Unquote(nameKey.Value)
+ if err == nil {
+ // found interesting strings
+ handler.OnMsgid(fset, nameKey.ValuePos, arg)
+ }
+ }
+ case *ast.FuncDecl:
+ matchInsPrefix := handler.handleGoCommentGroup(fset, n2.Doc, "llu:returnsTrKey")
+ if matchInsPrefix == nil {
+ return true
+ }
+ results := n2.Type.Results.List
+ if len(results) != 1 {
+ handler.OnWarning(fset, n2.Type.Func, fmt.Sprintf("function %s has unexpected return type; expected single return value", n2.Name.Name))
+ return true
+ }
+
+ ast.Inspect(n2.Body, func(n ast.Node) bool {
+ // 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, *matchInsPrefix)
+ }
+ return true
+ })
+ }
+ return false
+ }
+ return true
+ })
+ return true
+ case *ast.GenDecl:
+ if !(n2.Tok == token.CONST || n2.Tok == token.VAR) {
+ return true
+ }
+ matchInsPrefix := handler.handleGoCommentGroup(fset, n2.Doc, " llu:TrKeys")
+ if matchInsPrefix == nil {
+ return true
+ }
+ for _, spec := range n2.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, *matchInsPrefix)
+ return false
+ }
+ return true
+ })
+ }
+ }
+
+ return true
+ })
+
+ return nil
+}
diff --git a/build/lint-locale-usage/handle-tmpl.go b/build/lint-locale-usage/handle-tmpl.go
new file mode 100644
index 0000000000..a71d7d47e3
--- /dev/null
+++ b/build/lint-locale-usage/handle-tmpl.go
@@ -0,0 +1,233 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package main
+
+import (
+ "fmt"
+ "go/token"
+ "os"
+ "strings"
+ "text/template"
+ tmplParser "text/template/parse"
+
+ fjTemplates "forgejo.org/modules/templates"
+ "forgejo.org/modules/util"
+)
+
+// derived from source: modules/templates/scopedtmpl/scopedtmpl.go, L169-L213
+func (handler Handler) handleTemplateNode(fset *token.FileSet, node tmplParser.Node) {
+ switch node.Type() {
+ case tmplParser.NodeAction:
+ handler.handleTemplatePipeNode(fset, node.(*tmplParser.ActionNode).Pipe)
+ case tmplParser.NodeList:
+ nodeList := node.(*tmplParser.ListNode)
+ handler.handleTemplateFileNodes(fset, nodeList.Nodes)
+ case tmplParser.NodePipe:
+ handler.handleTemplatePipeNode(fset, node.(*tmplParser.PipeNode))
+ case tmplParser.NodeTemplate:
+ handler.handleTemplatePipeNode(fset, node.(*tmplParser.TemplateNode).Pipe)
+ case tmplParser.NodeIf:
+ nodeIf := node.(*tmplParser.IfNode)
+ handler.handleTemplateBranchNode(fset, nodeIf.BranchNode)
+ case tmplParser.NodeRange:
+ nodeRange := node.(*tmplParser.RangeNode)
+ handler.handleTemplateBranchNode(fset, nodeRange.BranchNode)
+ case tmplParser.NodeWith:
+ nodeWith := node.(*tmplParser.WithNode)
+ handler.handleTemplateBranchNode(fset, nodeWith.BranchNode)
+
+ case tmplParser.NodeCommand:
+ nodeCommand := node.(*tmplParser.CommandNode)
+
+ handler.handleTemplateFileNodes(fset, nodeCommand.Args)
+
+ if len(nodeCommand.Args) < 2 {
+ return
+ }
+
+ funcname := ""
+ if nodeChain, ok := nodeCommand.Args[0].(*tmplParser.ChainNode); ok {
+ if nodeIdent, ok := nodeChain.Node.(*tmplParser.IdentifierNode); ok {
+ if nodeIdent.Ident != "ctx" || len(nodeChain.Field) != 2 || nodeChain.Field[0] != "Locale" {
+ return
+ }
+ funcname = nodeChain.Field[1]
+ }
+ } else if nodeField, ok := nodeCommand.Args[0].(*tmplParser.FieldNode); ok {
+ if len(nodeField.Ident) != 2 || !(nodeField.Ident[0] == "locale" || nodeField.Ident[0] == "Locale") {
+ return
+ }
+ funcname = nodeField.Ident[1]
+ }
+
+ var gotUnexpectedInvoke *int
+ ltf, ok := handler.LocaleTrFunctions[funcname]
+ if !ok {
+ return
+ }
+
+ for _, argNum := range ltf {
+ if len(nodeCommand.Args) >= int(argNum+2) {
+ handler.handleTemplateMsgid(fset, nodeCommand.Args[int(argNum+1)])
+ } else {
+ argc := len(nodeCommand.Args) - 1
+ gotUnexpectedInvoke = &argc
+ }
+ }
+
+ if gotUnexpectedInvoke != nil {
+ handler.OnUnexpectedInvoke(fset, token.Pos(nodeCommand.Pos), funcname, *gotUnexpectedInvoke)
+ }
+
+ default:
+ }
+}
+
+func (handler Handler) handleTemplateMsgid(fset *token.FileSet, node tmplParser.Node) {
+ // the column numbers are a bit "off", but much better than nothing
+ pos := token.Pos(node.Position())
+
+ switch node.Type() {
+ case tmplParser.NodeString:
+ nodeString := node.(*tmplParser.StringNode)
+ // found interesting strings
+ handler.OnMsgid(fset, pos, nodeString.Text)
+
+ case tmplParser.NodePipe:
+ nodePipe := node.(*tmplParser.PipeNode)
+ handler.handleTemplatePipeNode(fset, nodePipe)
+
+ if len(nodePipe.Cmds) == 0 {
+ handler.OnWarning(fset, pos, fmt.Sprintf("unsupported invocation of locate function (no commands): %s", node.String()))
+ } else if len(nodePipe.Cmds) != 1 {
+ handler.OnWarning(fset, pos, fmt.Sprintf("unsupported invocation of locate function (too many commands): %s", node.String()))
+ return
+ }
+ nodeCommand := nodePipe.Cmds[0]
+ if len(nodeCommand.Args) < 2 {
+ handler.OnWarning(fset, pos, fmt.Sprintf("unsupported invocation of locate function (not enough arguments): %s", node.String()))
+ return
+ }
+
+ nodeIdent, ok := nodeCommand.Args[0].(*tmplParser.IdentifierNode)
+ if !ok || (nodeIdent.Ident != "print" && nodeIdent.Ident != "printf") {
+ // handler.OnWarning(fset, pos, fmt.Sprintf("unsupported invocation of locate function (bad command): %s", node.String()))
+ return
+ }
+
+ nodeString, ok := nodeCommand.Args[1].(*tmplParser.StringNode)
+ if !ok {
+ //handler.OnWarning(
+ // fset,
+ // pos,
+ // fmt.Sprintf("unsupported invocation of locate function (string should be first argument to %s): %s", nodeIdent.Ident, node.String()),
+ //)
+ return
+ }
+
+ msgidPrefix := nodeString.Text
+ stringPos := token.Pos(nodeString.Pos)
+
+ if len(nodeCommand.Args) == 2 {
+ // found interesting strings
+ handler.OnMsgid(fset, stringPos, msgidPrefix)
+ } else {
+ if nodeIdent.Ident == "printf" {
+ parts := strings.SplitN(msgidPrefix, "%", 2)
+ if len(parts) != 2 {
+ handler.OnWarning(
+ fset,
+ stringPos,
+ fmt.Sprintf("unsupported invocation of locate function (format string doesn't match \"prefix%%smth\" pattern): %s", nodeString.String()),
+ )
+ return
+ }
+ msgidPrefix = parts[0]
+ }
+
+ msgidPrefixFin, truncated := PrepareMsgidPrefix(msgidPrefix)
+ if truncated {
+ handler.OnWarning(fset, stringPos, fmt.Sprintf("needed to truncate message id prefix: %s", msgidPrefix))
+ }
+
+ // found interesting strings
+ handler.OnMsgidPrefix(fset, stringPos, msgidPrefixFin, truncated)
+ }
+
+ default:
+ // handler.OnWarning(fset, pos, fmt.Sprintf("unknown invocation of locate function: %s", node.String()))
+ }
+}
+
+func (handler Handler) handleTemplatePipeNode(fset *token.FileSet, pipeNode *tmplParser.PipeNode) {
+ if pipeNode == nil {
+ return
+ }
+
+ // NOTE: we can't pass `pipeNode.Cmds` to handleTemplateFileNodes due to incompatible argument types
+ for _, node := range pipeNode.Cmds {
+ handler.handleTemplateNode(fset, node)
+ }
+}
+
+func (handler Handler) handleTemplateBranchNode(fset *token.FileSet, branchNode tmplParser.BranchNode) {
+ handler.handleTemplatePipeNode(fset, branchNode.Pipe)
+ handler.handleTemplateFileNodes(fset, branchNode.List.Nodes)
+ if branchNode.ElseList != nil {
+ handler.handleTemplateFileNodes(fset, branchNode.ElseList.Nodes)
+ }
+}
+
+func (handler Handler) handleTemplateFileNodes(fset *token.FileSet, nodes []tmplParser.Node) {
+ for _, node := range nodes {
+ handler.handleTemplateNode(fset, node)
+ }
+}
+
+// the `Handle*File` functions follow the following calling convention:
+// * `fname` is the name of the input file
+// * `src` is either `nil` (then the function invokes `ReadFile` to read the file)
+// or the contents of the file as {`[]byte`, or a `string`}
+
+func (handler Handler) HandleTemplateFile(fname string, src any) error {
+ var tmplContent []byte
+ switch src2 := src.(type) {
+ case nil:
+ var err error
+ tmplContent, err = os.ReadFile(fname)
+ if err != nil {
+ return LocatedError{
+ Location: fname,
+ Kind: "ReadFile",
+ Err: err,
+ }
+ }
+ case []byte:
+ tmplContent = src2
+ case string:
+ // SAFETY: we do not modify tmplContent below
+ tmplContent = util.UnsafeStringToBytes(src2)
+ default:
+ panic("invalid type for 'src'")
+ }
+
+ fset := token.NewFileSet()
+ fset.AddFile(fname, 1, len(tmplContent)).SetLinesForContent(tmplContent)
+ // SAFETY: we do not modify tmplContent2 below
+ tmplContent2 := util.UnsafeBytesToString(tmplContent)
+
+ tmpl := template.New(fname)
+ tmpl.Funcs(fjTemplates.NewFuncMap())
+ tmplParsed, err := tmpl.Parse(tmplContent2)
+ if err != nil {
+ return LocatedError{
+ Location: fname,
+ Kind: "Template parser",
+ Err: err,
+ }
+ }
+ handler.handleTemplateFileNodes(fset, tmplParsed.Root.Nodes)
+ return nil
+}
diff --git a/build/lint-locale-usage/lint-locale-usage.go b/build/lint-locale-usage/lint-locale-usage.go
index 88375c1c36..8c67a781e9 100644
--- a/build/lint-locale-usage/lint-locale-usage.go
+++ b/build/lint-locale-usage/lint-locale-usage.go
@@ -5,22 +5,19 @@
package main
import (
+ "bufio"
+ "errors"
+ "flag"
"fmt"
- "go/ast"
- goParser "go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
- "strconv"
+ "sort"
"strings"
- "text/template"
- tmplParser "text/template/parse"
"forgejo.org/modules/container"
- fjTemplates "forgejo.org/modules/templates"
"forgejo.org/modules/translation/localeiter"
- "forgejo.org/modules/util"
)
// this works by first gathering all valid source string IDs from `en-US` reference files
@@ -63,241 +60,180 @@ func InitLocaleTrFunctions() map[string][]uint {
type Handler struct {
OnMsgid func(fset *token.FileSet, pos token.Pos, msgid string)
+ OnMsgidPrefix func(fset *token.FileSet, pos token.Pos, msgidPrefix string, truncated bool)
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
}
-// the `Handle*File` functions follow the following calling convention:
-// * `fname` is the name of the input file
-// * `src` is either `nil` (then the function invokes `ReadFile` to read the file)
-// or the contents of the file as {`[]byte`, or a `string`}
+type StringTrie interface {
+ Matches(key []string) bool
+}
-func (handler Handler) HandleGoFile(fname string, src any) error {
- fset := token.NewFileSet()
- node, err := goParser.ParseFile(fset, fname, src, goParser.SkipObjectResolution)
- if err != nil {
- return LocatedError{
- Location: fname,
- Kind: "Go parser",
- Err: err,
- }
- }
-
- ast.Inspect(node, func(n ast.Node) bool {
- // search for function calls of the form `anything.Tr(any-string-lit, ...)`
-
- call, ok := n.(*ast.CallExpr)
- if !ok || len(call.Args) < 1 {
- return true
- }
-
- funSel, ok := call.Fun.(*ast.SelectorExpr)
- if !ok {
- return true
- }
-
- ltf, ok := handler.LocaleTrFunctions[funSel.Sel.Name]
- if !ok {
- return true
- }
-
- var gotUnexpectedInvoke *int
-
- for _, argNum := range ltf {
- if len(call.Args) >= int(argNum+1) {
- argLit, ok := call.Args[int(argNum)].(*ast.BasicLit)
- if !ok || argLit.Kind != token.STRING {
- continue
- }
-
- // extract string content
- arg, err := strconv.Unquote(argLit.Value)
- if err == nil {
- // found interesting strings
- handler.OnMsgid(fset, argLit.ValuePos, arg)
- }
- } else {
- argc := len(call.Args)
- gotUnexpectedInvoke = &argc
- }
- }
-
- if gotUnexpectedInvoke != nil {
- handler.OnUnexpectedInvoke(fset, funSel.Sel.NamePos, funSel.Sel.Name, *gotUnexpectedInvoke)
- }
+type StringTrieMap map[string]StringTrie
+func (m StringTrieMap) Matches(key []string) bool {
+ if len(key) == 0 || m == nil {
return true
- })
-
- return nil
-}
-
-// derived from source: modules/templates/scopedtmpl/scopedtmpl.go, L169-L213
-func (handler Handler) handleTemplateNode(fset *token.FileSet, node tmplParser.Node) {
- switch node.Type() {
- case tmplParser.NodeAction:
- handler.handleTemplatePipeNode(fset, node.(*tmplParser.ActionNode).Pipe)
- case tmplParser.NodeList:
- nodeList := node.(*tmplParser.ListNode)
- handler.handleTemplateFileNodes(fset, nodeList.Nodes)
- case tmplParser.NodePipe:
- handler.handleTemplatePipeNode(fset, node.(*tmplParser.PipeNode))
- case tmplParser.NodeTemplate:
- handler.handleTemplatePipeNode(fset, node.(*tmplParser.TemplateNode).Pipe)
- case tmplParser.NodeIf:
- nodeIf := node.(*tmplParser.IfNode)
- handler.handleTemplateBranchNode(fset, nodeIf.BranchNode)
- case tmplParser.NodeRange:
- nodeRange := node.(*tmplParser.RangeNode)
- handler.handleTemplateBranchNode(fset, nodeRange.BranchNode)
- case tmplParser.NodeWith:
- nodeWith := node.(*tmplParser.WithNode)
- handler.handleTemplateBranchNode(fset, nodeWith.BranchNode)
-
- case tmplParser.NodeCommand:
- nodeCommand := node.(*tmplParser.CommandNode)
-
- handler.handleTemplateFileNodes(fset, nodeCommand.Args)
-
- if len(nodeCommand.Args) < 2 {
- return
- }
-
- nodeChain, ok := nodeCommand.Args[0].(*tmplParser.ChainNode)
- if !ok {
- return
- }
-
- nodeIdent, ok := nodeChain.Node.(*tmplParser.IdentifierNode)
- if !ok || nodeIdent.Ident != "ctx" || len(nodeChain.Field) != 2 || nodeChain.Field[0] != "Locale" {
- return
- }
-
- ltf, ok := handler.LocaleTrFunctions[nodeChain.Field[1]]
- if !ok {
- return
- }
-
- var gotUnexpectedInvoke *int
-
- for _, argNum := range ltf {
- if len(nodeCommand.Args) >= int(argNum+2) {
- nodeString, ok := nodeCommand.Args[int(argNum+1)].(*tmplParser.StringNode)
- if ok {
- // found interesting strings
- // the column numbers are a bit "off", but much better than nothing
- handler.OnMsgid(fset, token.Pos(nodeString.Pos), nodeString.Text)
- }
- } else {
- argc := len(nodeCommand.Args) - 1
- gotUnexpectedInvoke = &argc
- }
- }
-
- if gotUnexpectedInvoke != nil {
- handler.OnUnexpectedInvoke(fset, token.Pos(nodeChain.Pos), nodeChain.Field[1], *gotUnexpectedInvoke)
- }
-
- default:
}
+ value, ok := m[key[0]]
+ if !ok {
+ return false
+ }
+ if value == nil {
+ return true
+ }
+ return value.Matches(key[1:])
}
-func (handler Handler) handleTemplatePipeNode(fset *token.FileSet, pipeNode *tmplParser.PipeNode) {
- if pipeNode == nil {
+func (m StringTrieMap) Insert(key []string) {
+ if m == nil {
return
}
- // NOTE: we can't pass `pipeNode.Cmds` to handleTemplateFileNodes due to incompatible argument types
- for _, node := range pipeNode.Cmds {
- handler.handleTemplateNode(fset, node)
- }
-}
+ switch len(key) {
+ case 0:
+ return
-func (handler Handler) handleTemplateBranchNode(fset *token.FileSet, branchNode tmplParser.BranchNode) {
- handler.handleTemplatePipeNode(fset, branchNode.Pipe)
- handler.handleTemplateFileNodes(fset, branchNode.List.Nodes)
- if branchNode.ElseList != nil {
- handler.handleTemplateFileNodes(fset, branchNode.ElseList.Nodes)
- }
-}
+ case 1:
+ m[key[0]] = nil
-func (handler Handler) handleTemplateFileNodes(fset *token.FileSet, nodes []tmplParser.Node) {
- for _, node := range nodes {
- handler.handleTemplateNode(fset, node)
- }
-}
-
-func (handler Handler) HandleTemplateFile(fname string, src any) error {
- var tmplContent []byte
- switch src2 := src.(type) {
- case nil:
- var err error
- tmplContent, err = os.ReadFile(fname)
- if err != nil {
- return LocatedError{
- Location: fname,
- Kind: "ReadFile",
- Err: err,
- }
- }
- case []byte:
- tmplContent = src2
- case string:
- // SAFETY: we do not modify tmplContent below
- tmplContent = util.UnsafeStringToBytes(src2)
default:
- panic("invalid type for 'src'")
+ if value, ok := m[key[0]]; ok {
+ if value == nil {
+ return
+ }
+ } else {
+ m[key[0]] = make(StringTrieMap)
+ }
+ m[key[0]].(StringTrieMap).Insert(key[1:])
}
+}
- fset := token.NewFileSet()
- fset.AddFile(fname, 1, len(tmplContent)).SetLinesForContent(tmplContent)
- // SAFETY: we do not modify tmplContent2 below
- tmplContent2 := util.UnsafeBytesToString(tmplContent)
-
- tmpl := template.New(fname)
- tmpl.Funcs(fjTemplates.NewFuncMap())
- tmplParsed, err := tmpl.Parse(tmplContent2)
+func ParseAllowedMaskedUsages(fname string, usedMsgids container.Set[string], allowedMaskedPrefixes StringTrieMap, chkMsgid func(msgid string) bool) error {
+ file, err := os.Open(fname)
if err != nil {
return LocatedError{
Location: fname,
- Kind: "Template parser",
+ Kind: "Open",
+ Err: err,
+ }
+ }
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ lno := 0
+ for scanner.Scan() {
+ lno++
+ line := strings.TrimSpace(scanner.Text())
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue
+ }
+ if linePrefix, found := strings.CutSuffix(line, "."); found {
+ allowedMaskedPrefixes.Insert(strings.Split(linePrefix, "."))
+ } else {
+ if !chkMsgid(line) {
+ return LocatedError{
+ Location: fmt.Sprintf("%s: line %d", fname, lno),
+ Kind: "undefined msgid",
+ Err: errors.New(line),
+ }
+ }
+ usedMsgids.Add(line)
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return LocatedError{
+ Location: fname,
+ Kind: "Scanner",
Err: err,
}
}
- handler.handleTemplateFileNodes(fset, tmplParsed.Root.Nodes)
return nil
}
-// This command assumes that we get started from the project root directory
-//
-// Possible command line flags:
-//
-// --allow-missing-msgids don't return an error code if missing message IDs are found
-//
-// EXIT CODES:
-//
-// 0 success, no issues found
-// 1 unable to walk directory tree
-// 2 unable to parse locale ini/json files
-// 3 unable to parse go or text/template files
-// 4 found missing message IDs
-//
+// Truncating a message id prefix to the last dot
+func PrepareMsgidPrefix(s string) (string, bool) {
+ index := strings.LastIndexByte(s, 0x2e)
+ if index == -1 {
+ return "", true
+ }
+ return s[:index], index != len(s)-1
+}
+
+func Usage() {
+ outp := flag.CommandLine.Output()
+ fmt.Fprintf(outp, "Usage of %s:\n", os.Args[0])
+ flag.PrintDefaults()
+
+ fmt.Fprintf(outp, "\nThis command assumes that it gets started from the project root directory.\n")
+
+ fmt.Fprintf(outp, "\nExit codes:\n")
+ for _, i := range []string{
+ "0\tsuccess, no issues found",
+ "1\tunable to walk directory tree",
+ "2\tunable to parse locale ini/json files",
+ "3\tunable to parse go or text/template files",
+ "4\tfound missing message IDs",
+ "5\tfound unused message IDs",
+ } {
+ fmt.Fprintf(outp, "\t%s\n", i)
+ }
+
+ fmt.Fprintf(outp, "\nSpecial Go doc comments:\n")
+ for _, i := range []string{
+ "//llu:returnsTrKey",
+ "\tcan be used in front of functions to indicate",
+ "\tthat the function returns message IDs",
+ "\tWARNING: this currently doesn't support nested functions properly",
+ "",
+ "//llu:returnsTrKeySuffix prefix.",
+ "\tsimilar to llu:returnsTrKey, but the given prefix is prepended",
+ "\tto the found strings before interpreting them as msgids",
+ "",
+ "// llu:TrKeys",
+ "\tcan be used in front of 'const' and 'var' blocks",
+ "\tin order to mark all contained strings as message IDs",
+ "",
+ "// llu:TrKeysSuffix prefix.",
+ "\tlike llu:returnsTrKeySuffix, but for 'const' and 'var' blocks",
+ } {
+ if i == "" {
+ fmt.Fprintf(outp, "\n")
+ } else {
+ fmt.Fprintf(outp, "\t%s\n", i)
+ }
+ }
+}
+
//nolint:forbidigo
func main() {
allowMissingMsgids := false
- for _, arg := range os.Args[1:] {
- if arg == "--allow-missing-msgids" {
- allowMissingMsgids = true
- }
- }
+ allowUnusedMsgids := false
+ usedMsgids := make(container.Set[string])
+ allowedMaskedPrefixes := make(StringTrieMap)
- onError := func(err error) {
- if err == nil {
- return
- }
- fmt.Println(err.Error())
- os.Exit(3)
+ // It's possible for execl to hand us an empty os.Args.
+ if len(os.Args) == 0 {
+ flag.CommandLine = flag.NewFlagSet("lint-locale-usage", flag.ExitOnError)
+ } else {
+ flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
}
+ flag.CommandLine.Usage = Usage
+ flag.Usage = Usage
+
+ flag.BoolVar(
+ &allowMissingMsgids,
+ "allow-missing-msgids",
+ false,
+ "don't return an error code if missing message IDs are found",
+ )
+ flag.BoolVar(
+ &allowUnusedMsgids,
+ "allow-unused-msgids",
+ false,
+ "don't return an error code if unused message IDs are found",
+ )
msgids := make(container.Set[string])
@@ -334,17 +270,50 @@ func main() {
gotAnyMsgidError := false
+ flag.Func(
+ "allow-masked-usages-from",
+ "supply a file containing a newline-separated list of allowed masked usages",
+ func(argval string) error {
+ return ParseAllowedMaskedUsages(argval, usedMsgids, allowedMaskedPrefixes, func(msgid string) bool {
+ return msgids.Contains(msgid)
+ })
+ },
+ )
+ flag.Parse()
+
+ onError := func(err error) {
+ if err == nil {
+ return
+ }
+ fmt.Println(err.Error())
+ os.Exit(3)
+ }
+
handler := Handler{
+ OnMsgidPrefix: func(fset *token.FileSet, pos token.Pos, msgidPrefix string, truncated bool) {
+ msgidPrefixSplit := strings.Split(msgidPrefix, ".")
+ if !truncated {
+ allowedMaskedPrefixes.Insert(msgidPrefixSplit)
+ } else if !allowedMaskedPrefixes.Matches(msgidPrefixSplit) {
+ gotAnyMsgidError = true
+ fmt.Printf("%s:\tmissing msgid prefix: %s\n", fset.Position(pos).String(), msgidPrefix)
+ }
+ },
OnMsgid: func(fset *token.FileSet, pos token.Pos, msgid string) {
if !msgids.Contains(msgid) {
gotAnyMsgidError = true
fmt.Printf("%s:\tmissing msgid: %s\n", fset.Position(pos).String(), msgid)
+ } else {
+ usedMsgids.Add(msgid)
}
},
OnUnexpectedInvoke: func(fset *token.FileSet, pos token.Pos, funcname string, argc int) {
gotAnyMsgidError = true
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(),
}
@@ -377,7 +346,27 @@ func main() {
os.Exit(1)
}
+ unusedMsgids := []string{}
+
+ for msgid := range msgids {
+ if !usedMsgids.Contains(msgid) && !allowedMaskedPrefixes.Matches(strings.Split(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 {
os.Exit(4)
}
+ if !allowUnusedMsgids && len(unusedMsgids) != 0 {
+ os.Exit(5)
+ }
}
diff --git a/models/asymkey/gpg_key_object_verification.go b/models/asymkey/gpg_key_object_verification.go
index 745ed04869..10567dcd4f 100644
--- a/models/asymkey/gpg_key_object_verification.go
+++ b/models/asymkey/gpg_key_object_verification.go
@@ -36,6 +36,7 @@ type ObjectVerification struct {
TrustStatus string
}
+// llu:TrKeys
const (
// 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.
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 5edebb4105..8c0ddb8ddf 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -469,6 +469,8 @@ func (issue *Issue) GetLastEventTimestamp() timeutil.TimeStamp {
}
// GetLastEventLabel returns the localization label for the current issue.
+//
+//llu:returnsTrKey
func (issue *Issue) GetLastEventLabel() string {
if issue.IsClosed {
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.
+//
+//llu:returnsTrKey
func (issue *Issue) GetLastEventLabelFake() string {
if issue.IsClosed {
if issue.IsPull && issue.PullRequest.HasMerged {
diff --git a/models/moderation/abuse_report.go b/models/moderation/abuse_report.go
index 0bf8aab174..152b81bb51 100644
--- a/models/moderation/abuse_report.go
+++ b/models/moderation/abuse_report.go
@@ -48,6 +48,7 @@ const (
AbuseCategoryTypeIllegalContent // 4
)
+// llu:TrKeys
var AbuseCategoriesTranslationKeys = map[AbuseCategoryType]string{
AbuseCategoryTypeSpam: "moderation.abuse_category.spam",
AbuseCategoryTypeMalware: "moderation.abuse_category.malware",
diff --git a/models/project/project.go b/models/project/project.go
index b9813fda91..18c647c8ac 100644
--- a/models/project/project.go
+++ b/models/project/project.go
@@ -182,6 +182,8 @@ func init() {
}
// GetCardConfig retrieves the types of configurations project column cards could have
+//
+//llu:returnsTrKey
func GetCardConfig() []CardConfig {
return []CardConfig{
{CardTypeTextOnly, "repo.projects.card_type.text_only"},
diff --git a/models/project/template.go b/models/project/template.go
index 06d5d2af14..278cf5b781 100644
--- a/models/project/template.go
+++ b/models/project/template.go
@@ -26,6 +26,8 @@ const (
)
// GetTemplateConfigs retrieves the template configs of configurations project columns could have
+//
+//llu:returnsTrKey
func GetTemplateConfigs() []TemplateConfig {
return []TemplateConfig{
{TemplateTypeNone, "repo.projects.type.none"},
diff --git a/models/unit/unit.go b/models/unit/unit.go
index a14f3ff364..6b4f2765ee 100644
--- a/models/unit/unit.go
+++ b/models/unit/unit.go
@@ -271,7 +271,6 @@ type Unit struct {
Name string
NameKey string
URI string
- DescKey string
Idx int
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",
"repo.code",
"/",
- "repo.code.desc",
0,
perm.AccessModeOwner,
}
@@ -309,7 +307,6 @@ var (
"issues",
"repo.issues",
"/issues",
- "repo.issues.desc",
1,
perm.AccessModeOwner,
}
@@ -319,7 +316,6 @@ var (
"ext_issues",
"repo.ext_issues",
"/issues",
- "repo.ext_issues.desc",
1,
perm.AccessModeRead,
}
@@ -329,7 +325,6 @@ var (
"pulls",
"repo.pulls",
"/pulls",
- "repo.pulls.desc",
2,
perm.AccessModeOwner,
}
@@ -339,7 +334,6 @@ var (
"releases",
"repo.releases",
"/releases",
- "repo.releases.desc",
3,
perm.AccessModeOwner,
}
@@ -349,7 +343,6 @@ var (
"wiki",
"repo.wiki",
"/wiki",
- "repo.wiki.desc",
4,
perm.AccessModeOwner,
}
@@ -359,7 +352,6 @@ var (
"ext_wiki",
"repo.ext_wiki",
"/wiki",
- "repo.ext_wiki.desc",
4,
perm.AccessModeRead,
}
@@ -369,7 +361,6 @@ var (
"projects",
"repo.projects",
"/projects",
- "repo.projects.desc",
5,
perm.AccessModeOwner,
}
@@ -379,7 +370,6 @@ var (
"packages",
"repo.packages",
"/packages",
- "packages.desc",
6,
perm.AccessModeRead,
}
@@ -389,7 +379,6 @@ var (
"actions",
"repo.actions",
"/actions",
- "actions.unit.desc",
7,
perm.AccessModeOwner,
}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 1dc6d38c8c..dbb1c73c11 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -15,7 +15,6 @@ version = Version
powered_by = Powered by %s
page = Page
template = Template
-language = Language
notifications = Notifications
active_stopwatch = Active time tracker
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.
repository = Repository
-organization = Organization
-mirror = Mirror
-new_mirror = New mirror
new_fork = New repository fork
-new_project = New project
new_project_column = New column
admin_panel = Site administration
settings = Settings
@@ -104,7 +99,6 @@ disabled = Disabled
locked = Locked
copy = Copy
-copy_generic = Copy to clipboard
copy_url = Copy URL
copy_hash = Copy hash
copy_path = Copy path
@@ -168,14 +162,6 @@ filter.private = Private
[search]
search = Search…
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…
user_kind = Search users…
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.
invalid_admin_setting = Administrator account setting 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_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
@@ -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.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.issue = @%[1]s assigned you to issue %[2]s in repository %[3]s.
@@ -581,7 +564,6 @@ yes = Yes
no = No
confirm = Confirm
cancel = Cancel
-modify = Update
[form]
UserName = Username
@@ -733,17 +715,14 @@ form.name_chars_not_allowed = Username "%s" contains invalid characters.
profile = Profile
account = Account
appearance = Appearance
-password = Password
security = Security
avatar = Avatar
ssh_gpg_keys = SSH / GPG keys
applications = Applications
orgs = Organizations
repos = Repositories
-delete = Delete account
twofa = Two-factor authentication (TOTP)
organization = Organizations
-uid = UID
webauthn = Two-factor authentication (Security keys)
blocked_users = Blocked users
storage_overview = Storage overview
@@ -765,12 +744,10 @@ update_language = Change language
update_language_not_found = Language "%s" is not available.
update_language_success = Language 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_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.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
language = Language
language.title = Default language
@@ -961,9 +938,7 @@ permissions_list = Permissions:
manage_oauth2_applications = Manage OAuth2 applications
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_desc = Removing an OAuth2 application will revoke access to all signed access tokens. Continue?
remove_oauth2_application_success = The application has been deleted.
create_oauth2_application = Create a new OAuth2 application
create_oauth2_application_button = Create application
@@ -977,7 +952,6 @@ oauth2_client_id = Client ID
oauth2_client_secret = Client secret
oauth2_regenerate_secret = Regenerate 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_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?
@@ -1118,11 +1092,8 @@ open_with_editor = Open with %s
download_zip = Download ZIP
download_tar = Download TAR.GZ
download_bundle = Download BUNDLE
-generate_repo = Generate repository
-generate_from = Generate from
repo_desc = Description
repo_desc_helper = Enter short description (optional)
-repo_lang = Language
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.
issue_labels = Labels
@@ -1160,7 +1131,6 @@ mirror_lfs = Large File Storage (LFS)
mirror_lfs_desc = Activate mirroring of LFS data.
mirror_lfs_endpoint = LFS endpoint
mirror_lfs_endpoint_desc = Sync will attempt to use the clone url to determine the LFS server. 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_blank_placeholder = (Unset)
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.
forks = Forks
stars = Stars
-reactions_more = and %d more
unit_disabled = The site administrator has disabled this repository section.
language_other = Other
adopt_search = Enter username to search for unadopted repositories… (leave blank to find all)
@@ -1187,9 +1156,9 @@ blame.ignore_revs.failed = Failed to ignore revisions in .git-blame
author_search_tooltip = Shows a maximum of 30 users
summary_card_alt = Summary card of repository %s
-tree_path_not_found_commit = Path %[1]s doesn't exist in commit %[2]s
-tree_path_not_found_branch = Path %[1]s doesn't exist in branch %[2]s
-tree_path_not_found_tag = Path %[1]s doesn't exist in tag %[2]s
+tree_path_not_found.commit = Path %[1]s doesn't exist in commit %[2]s
+tree_path_not_found.branch = Path %[1]s doesn't exist in branch %[2]s
+tree_path_not_found.tag = Path %[1]s doesn't exist in tag %[2]s
transfer.accept = Accept transfer
transfer.accept_desc = Transfer to "%s"
@@ -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.
desc.private = Private
-desc.public = Public
desc.template = Template
desc.internal = Internal
desc.archived = Archived
@@ -1301,7 +1269,6 @@ unwatch = Unwatch
star = Star
unstar = Unstar
fork = Fork
-download_archive = Download repository
more_operations = More operations
no_desc = No description
@@ -1315,29 +1282,22 @@ broken_message = The Git data underlying this repository cannot be read. Contact
code = Code
code.desc = Access source code, files, commits and branches.
-branch = Branch
-tree = Tree
clear_ref = `Clear current reference`
filter_branch_and_tag = Filter branch or tag
find_tag = Find tag
branches = Branches
-tag = Tag
tags = Tags
issues = Issues
pulls = Pull requests
project = Projects
packages = Packages
actions = Actions
-release = Release
releases = Releases
labels = Labels
milestones = Milestones
org_labels_desc = Organization level labels that can be used with all repositories under this organization
org_labels_desc_manage = manage
-commits = Commits
-commit = Commit
-
n_commit_one=%s commit
n_commit_few=%s commits
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_desc = New branch name…
editor.cancel = Cancel
-editor.filename_cannot_be_empty = The filename cannot be empty.
editor.filename_is_invalid = The filename is invalid: "%s".
editor.invalid_commit_mail = Invalid mail for creating a commit.
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.commit_email = Commit email
-commits.desc = Browse source code change history.
commits.commits = Commits
commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories.
commits.nothing_to_compare = These branches are equal.
@@ -1477,8 +1435,6 @@ commits.message = Message
commits.browse_further = Browse further
commits.renamed_from = Renamed from %s
commits.date = Date
-commits.older = Older
-commits.newer = Newer
commits.signed_by = Signed by
commits.signed_by_untrusted_user = Signed by untrusted user
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
projects = Projects
-projects.desc = Manage issues and pulls in project boards.
projects.description = Description (optional)
projects.description_placeholder = Description
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.text_only = Text only
-issues.desc = Organize bug reports, tasks and milestones.
issues.filter_assignees = Filter assignee
issues.filter_milestones = Filter milestone
issues.filter_projects = Filter project
@@ -1583,7 +1537,6 @@ issues.new_label = New label
issues.new_label_placeholder = Label name
issues.new_label_desc_placeholder = Description
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.helper = Select a 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_no_select = All authors
issues.filter_type = Type
-issues.filter_type.all_issues = All issues
issues.filter_type.all_pull_requests = All pull requests
issues.filter_type.assigned_to_you = Assigned to 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_few = %d participants
issues.attachment.open_tab = `Click to see "%s" in a new tab`
-issues.attachment.download = `Click to download "%s"`
issues.subscribe = Subscribe
issues.unsubscribe = Unsubscribe
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_minutes = Minutes
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.due_date = Due date
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.content.empty = You need to leave a comment indicating the requested change(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_requests = requested reviews from %[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.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.
-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
compare.compare_base = base
compare.compare_head = compare
-pulls.desc = Enable pull requests and code reviews.
pulls.new = New 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
@@ -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_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.review_only_possible_for_full_diff = Review is only possible when viewing the full diff
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_have_tag = The selected branches/tags are equal.
@@ -2107,7 +2053,6 @@ ext_wiki = External Wiki
wiki = Wiki
wiki.welcome = Welcome to the wiki.
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.page = Page
wiki.filter_page = Filter page
@@ -2737,11 +2682,9 @@ diff.protected = Protected
diff.image.side_by_side = Side by side
diff.image.swipe = Swipe
diff.image.overlay = Overlay
-diff.has_escaped = This line has hidden Unicode characters
diff.show_file_tree = Show file tree
diff.hide_file_tree = Hide file tree
-releases.desc = Track project versions and downloads.
release.releases = Releases
release.detail = Release details
release.tags = Tags
@@ -2753,7 +2696,6 @@ release.compare = Compare
release.edit = Edit
release.ahead.commits = %d commits
release.ahead.target = to %s since this release
-tag.ahead.target = to %s since this tag
release.source_code = Source code
release.new_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_invalid = The tag name is not valid.
release.tag_name_protected = The tag name is protected.
-release.tag_already_exist = This tag name already exists.
release.downloads = Downloads
release.download_count_one = %s download
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.already_exists = A branch named "%s" already exists.
-branch.delete_head = Delete
branch.delete = Delete branch "%s"
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?
@@ -2842,7 +2782,6 @@ tag.create_tag_from = Create new tag from "%s"
tag.create_success = Tag "%s" has been created.
topic.manage_topics = Manage topics
-topic.done = Done
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.
@@ -2913,7 +2852,6 @@ form.create_org_not_allowed = You are not allowed to create an organization.
settings = Settings
settings.options = Organization
-settings.full_name = Full name
settings.email = Contact email
settings.website = Website
settings.location = Location
@@ -2943,8 +2881,6 @@ settings.hooks_desc = Add webhooks which will be triggered for all repos
settings.labels_desc = Add labels which can be used on issues for all repositories under this organization.
-members.membership_visibility = Membership visibility:
-members.public = Visible
members.public_helper = Make hidden
members.private = Hidden
members.private_helper = Make visible
@@ -2955,8 +2891,6 @@ members.remove = Remove
members.remove.detail = Remove %[1]s from %[2]s?
members.leave = Leave
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.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.settings = Settings
teams.owners_permission_desc = Owners have full access to all repositories and have administrator access to the organization.
-teams.members = Team members
teams.update_settings = Update settings
teams.delete_team = Delete team
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_success = The team has been deleted.
teams.admin_permission_desc = This team grants Administrator access: members can read from, push to and add collaborators to team repositories.
-teams.create_repo_permission_desc = Additionally, this team grants Create repository 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.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_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.
@@ -3201,7 +3130,6 @@ repos.unadopted = Unadopted repositories
repos.unadopted.no_more = No unadopted repositories found.
repos.owner = Owner
repos.name = Name
-repos.private = Private
repos.issues = Issues
repos.size = Size
repos.lfs_size = LFS size
@@ -3240,7 +3168,6 @@ auths.updated = Updated
auths.auth_type = Authentication type
auths.auth_name = Authentication name
auths.security_protocol = Security protocol
-auths.domain = Domain
auths.host = Host
auths.port = Port
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_removal = Remove users from synchronized teams if user does not belong to corresponding LDAP group
auths.enable_ldap_groups = Enable LDAP groups
-auths.ms_ad_sa = MS AD search attributes
auths.smtp_auth = SMTP authentication type
auths.smtphost = SMTP host
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.deletion_success = The authentication source has been deleted.
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.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.app_data_path = App data path
config.repo_root_path = Repository root path
-config.lfs_root_path = LFS root path
config.log_file_root_path = Log path
config.script_type = Script type
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_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_adapter = Cache adapter
config.cache_interval = Cache interval
@@ -3454,10 +3375,8 @@ config.cookie_name = Cookie name
config.gc_interval_time = GC interval time
config.session_life_time = Session lifetime
config.https_only = HTTPS only
-config.cookie_life_time = Cookie lifetime
config.picture_config = Picture and avatar configuration
-config.picture_service = Picture service
config.disable_gravatar = Disable Gravatar
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.
@@ -3477,7 +3396,6 @@ config.git_gc_timeout = GC Operation timeout
config.log_config = Log configuration
config.logger_name_fmt = Logger: %s
config.disabled_logger = Disabled
-config.access_log_mode = Access log mode
config.access_log_template = Access log template
config.xorm_log_sql = Log SQL
@@ -3496,14 +3414,10 @@ monitor.stacktrace = Stacktrace
monitor.processes_count = %d Processes
monitor.download_diagnosis_report = Download diagnosis report
monitor.duration = Duration (s)
-monitor.desc = Description
-monitor.start = Start Time
-monitor.execute_time = Execution Time
monitor.last_execution_result = Result
monitor.process.cancel = Cancel process
monitor.process.cancel_desc = Canceling a process may cause data loss
monitor.process.cancel_notices = Cancel: %s?
-monitor.process.children = Children
monitor.queues = Queues
monitor.queue = Queue: %s
@@ -3642,11 +3556,9 @@ error.probable_bad_default_signature = WARNING! Although the default key has thi
[units]
unit = Unit
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]
title = Packages
-desc = Manage repository packages.
empty = There are no packages yet.
empty.documentation = For more information on the package registry, see the documentation.
empty.repo = Did you upload a package, but it's not shown here? Go to package settings and link it to this repo.
@@ -3707,7 +3619,6 @@ composer.registry = Setup this registry in your ~/.composer/config.json.condarc
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.overview = %d packages are scheduled to be removed.
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.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
@@ -3840,7 +3750,6 @@ management = Manage secrets
[actions]
actions = Actions
-unit.desc = Manage integrated CI/CD pipelines with Forgejo Actions.
status.unknown = Unknown
status.waiting = Waiting
@@ -3931,7 +3840,6 @@ variables.none = There are no variables yet.
variables.deletion = Remove variable
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.id_not_exist = Variable with ID %d does not exist.
variables.edit = Edit Variable
variables.not_found = Failed to find the variable.
variables.deletion.failed = Failed to remove variable.
diff --git a/routers/web/repo/helper.go b/routers/web/repo/helper.go
index 9d67f142fb..3401f2f666 100644
--- a/routers/web/repo/helper.go
+++ b/routers/web/repo/helper.go
@@ -35,7 +35,7 @@ func HandleGitError(ctx *context.Context, msg string, err error) {
case ctx.Repo.IsViewCommit:
refType = "commit"
}
- ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.tree_path_not_found_"+refType, ctx.Repo.TreePath, url.PathEscape(ctx.Repo.RefName))
+ ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.tree_path_not_found."+refType, ctx.Repo.TreePath, url.PathEscape(ctx.Repo.RefName))
ctx.Data["NotFoundGoBackURL"] = ctx.Repo.RepoLink + "/src/" + refType + "/" + url.PathEscape(ctx.Repo.RefName)
ctx.NotFound(msg, err)
} else {
diff --git a/routers/web/user/task.go b/routers/web/user/task.go
index 296c44f809..4059139552 100644
--- a/routers/web/user/task.go
+++ b/routers/web/user/task.go
@@ -35,7 +35,7 @@ func TaskStatus(ctx *context.Context) {
var translatableMessage admin_model.TranslatableMessage
if err := json.Unmarshal([]byte(message), &translatableMessage); err != nil {
translatableMessage = admin_model.TranslatableMessage{
- Format: "migrate.migrating_failed.error",
+ Format: "repo.migrate.migrating_failed.error",
Args: []any{task.Message},
}
}
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index 1f2a7f232f..2f3d289c80 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -427,6 +427,7 @@ func (diffFile *DiffFile) ShouldBeHidden() bool {
return diffFile.IsGenerated || diffFile.IsViewed
}
+//llu:returnsTrKey
func (diffFile *DiffFile) ModeTranslationKey(mode string) string {
switch mode {
case "040000":
diff --git a/services/mailer/mail_actions.go b/services/mailer/mail_actions.go
index a99af823b3..1119781613 100644
--- a/services/mailer/mail_actions.go
+++ b/services/mailer/mail_actions.go
@@ -53,7 +53,7 @@ func sendMailActionRun(to *user_model.User, run *actions_model.ActionRun, priorS
if run.Status.IsSuccess() {
subject = locale.TrString("mail.actions.successful_run_after_failure_subject", run.Title, run.Repo.FullName())
} else {
- subject = locale.TrString("mail.actions.not_successful_run", run.Title, run.Repo.FullName())
+ subject = locale.TrString("mail.actions.not_successful_run_subject", run.Title, run.Repo.FullName())
}
commitSHA := run.CommitSHA