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