0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-07-26 11:04:27 -04:00

Using comment with i18n-check: to match dynamic translation keys

This commit is contained in:
Lunny Xiao 2025-07-01 12:57:35 -07:00
parent 042156847f
commit c668142c84
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
17 changed files with 77 additions and 42 deletions

View File

@ -114,6 +114,7 @@ func (r *ActionRunner) StatusName() string {
}
func (r *ActionRunner) StatusLocaleName(lang translation.Locale) string {
// i18n-check: actions.runners.status.*
return lang.TrString("actions.runners.status." + r.StatusName())
}

View File

@ -43,6 +43,7 @@ func (s Status) String() string {
// LocaleString returns the locale string name of the Status
func (s Status) LocaleString(lang translation.Locale) string {
// i18n-check: actions.status.*
return lang.TrString("actions.status." + s.String())
}

View File

@ -206,6 +206,7 @@ func (status *CommitStatus) APIURL(ctx context.Context) string {
// LocaleString returns the locale string name of the Status
func (status *CommitStatus) LocaleString(lang translation.Locale) string {
// i18n-check: repo.commitstatus.*
return lang.TrString("repo.commitstatus." + status.State.String())
}

View File

@ -225,11 +225,13 @@ const (
// LocaleString returns the locale string name of the role
func (r RoleInRepo) LocaleString(lang translation.Locale) string {
// i18n-check: repo.issues.role.*
return lang.TrString("repo.issues.role." + string(r))
}
// LocaleHelper returns the locale tooltip of the role
func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
// i18n-check: repo.issues.role.*_helper
return lang.TrString("repo.issues.role." + string(r) + "_helper")
}

View File

@ -128,6 +128,7 @@ func BuildComplexityError(locale translation.Locale) template.HTML {
buffer.WriteString("<ul>")
for _, c := range requiredList {
buffer.WriteString("<li>")
// i18n-check: form.password_*
buffer.WriteString(locale.TrString(c.TrNameOne))
buffer.WriteString("</li>")
}

View File

@ -144,6 +144,7 @@ func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML {
args[i] = template.HTMLEscapeString(fmt.Sprint(v))
}
}
// i18n-check: ignore
return template.HTML(l.TrString(trKey, args...))
}

View File

@ -238,6 +238,7 @@ func (l *locale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
} else if t, ok := cnt.(int64); ok {
c = t
} else {
// i18n-check: ignore
return l.Tr(keyN, args...)
}

View File

@ -104,8 +104,10 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
trName := field.Tag.Get("locale")
if len(trName) == 0 {
// i18n-check: form.*
trName = l.TrString("form." + field.Name)
} else {
// i18n-check: ignore
trName = l.TrString(trName)
}

View File

@ -180,6 +180,7 @@ func DashboardPost(ctx *context.Context) {
task := cron.GetTask(form.Op)
if task != nil {
go task.RunWithUser(ctx.Doer, nil)
// i18n-check: admin.dashboard.*
ctx.Flash.Success(ctx.Tr("admin.dashboard.task.started", ctx.Tr("admin.dashboard."+form.Op)))
} else {
ctx.Flash.Error(ctx.Tr("admin.dashboard.task.unknown", form.Op))

View File

@ -847,6 +847,7 @@ func Run(ctx *context_module.Context) {
})
if err != nil {
if errLocale := util.ErrorAsLocale(err); errLocale != nil {
// i18n-check: ignore
ctx.Flash.Error(ctx.Tr(errLocale.TrKey, errLocale.TrArgs...))
ctx.Redirect(redirectURL)
} else {

View File

@ -51,6 +51,7 @@ func Activity(ctx *context.Context) {
}
ctx.Data["DateFrom"] = timeFrom
ctx.Data["DateUntil"] = timeUntil
// i18n-check: repo.activity.period.*
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
canReadCode := ctx.Repo.CanRead(unit.TypeCode)

View File

@ -39,6 +39,7 @@ func editorHandleFileOperationErrorRender(ctx *context_service.Context, message,
func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) {
if errAs := util.ErrorAsLocale(err); errAs != nil {
// i18n-check: ignore
ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...))
} else if errAs, ok := errorAs[git.ErrNotExist](err); ok {
ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath))

View File

@ -318,6 +318,7 @@ func MigrateStatus(ctx *context.Context) {
Args: []any{task.Message},
}
}
// i18n-check: repo.migrate.migrating_failed.*
message = ctx.Locale.TrString(translatableMessage.Format, translatableMessage.Args...)
}

View File

@ -167,6 +167,7 @@ func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
}
func (b *Base) Tr(msg string, args ...any) template.HTML {
// i18n-check: ignore
return b.Locale.Tr(msg, args...)
}

View File

@ -70,6 +70,7 @@ func (b *BaseConfig) DoNoticeOnSuccess() bool {
// Please note the `status` string will be concatenated with `admin.dashboard.cron.` and `admin.dashboard.task.` to provide locale messages. Similarly `name` will be composed with `admin.dashboard.` to provide the locale name for the task.
func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer string, args ...any) string {
realArgs := make([]any, 0, len(args)+2)
// i18n-check: admin.dashboard.*
realArgs = append(realArgs, locale.TrString("admin.dashboard."+name))
if doer == "" {
realArgs = append(realArgs, "(Cron)")
@ -80,7 +81,9 @@ func (b *BaseConfig) FormatMessage(locale translation.Locale, name, status, doer
realArgs = append(realArgs, args...)
}
if doer == "" {
// i18n-check: admin.dashboard.cron.*
return locale.TrString("admin.dashboard.cron."+status, realArgs...)
}
// i18n-check: admin.dashboard.task.*
return locale.TrString("admin.dashboard.task."+status, realArgs...)
}

View File

@ -159,6 +159,7 @@ func RegisterTask(name string, config Config, fun func(context.Context, *user_mo
log.Debug("Registering task: %s", name)
i18nKey := "admin.dashboard." + name
// i18n-check: admin.dashboard.*
if value := translation.NewLocale("en-US").TrString(i18nKey); value == i18nKey {
return fmt.Errorf("translation is missing for task %q, please add translation for %q", name, i18nKey)
}

View File

@ -6,6 +6,7 @@
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
@ -87,6 +88,55 @@ func searchUnusedKeyInFile(dir, path string, keys []string, res *[]bool) error {
return nil
}
func searchUntranslatedKeyInCall(path string, fset *token.FileSet, astf *ast.File, call *ast.CallExpr, arg ast.Expr, keys []string) string {
if lit, ok := arg.(*ast.BasicLit); ok && lit.Kind == token.STRING {
key := strings.Trim(lit.Value, `"`)
if !slices.Contains(keys, key) {
return key
}
return ""
}
var lastCg *ast.CommentGroup
for _, cg := range astf.Comments {
if cg.End() < call.Pos() {
if lastCg == nil || cg.End() > lastCg.End() {
lastCg = cg
}
}
}
if lastCg == nil {
fmt.Printf("no comment found for a dynamic translation key: %s:%d\n", path, fset.Position(call.Pos()).Line)
os.Exit(1)
return ""
}
transKeyMatch, ok := strings.CutPrefix(lastCg.Text(), "i18n-check:")
if !ok {
fmt.Printf("no comment found for a dynamic translation key: %s:%d\n", path, fset.Position(call.Pos()).Line)
os.Exit(1)
return ""
}
transKeyMatch = strings.TrimSpace(transKeyMatch)
switch transKeyMatch {
case "ignore": // i18n-check: ignore
return ""
default: // i18n-check: <transKeyMatch>
g := glob.MustCompile(transKeyMatch)
found := false
for _, key := range keys {
if g.Match(key) {
found = true
break
}
}
if !found {
return transKeyMatch
}
}
return ""
}
func searchUntranslatedKeyInGoFile(dir, path string, keys []string) ([]string, error) {
if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
return nil, nil
@ -94,7 +144,7 @@ func searchUntranslatedKeyInGoFile(dir, path string, keys []string) ([]string, e
var untranslated []string
fs := token.NewFileSet()
node, err := parser.ParseFile(fs, path, nil, 0)
node, err := parser.ParseFile(fs, path, nil, parser.ParseComments)
if err != nil {
return nil, err
}
@ -108,26 +158,17 @@ func searchUntranslatedKeyInGoFile(dir, path string, keys []string) ([]string, e
switch funIdent.Sel.Name {
case "Tr", "TrString":
if len(call.Args) >= 1 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok && lit.Kind == token.STRING {
key := strings.Trim(lit.Value, `"`)
if !slices.Contains(keys, key) {
untranslated = append(untranslated, key)
}
if key := searchUntranslatedKeyInCall(path, fs, node, call, call.Args[0], keys); key != "" {
untranslated = append(untranslated, key)
}
}
case "TrN":
if len(call.Args) >= 3 {
if lit, ok := call.Args[1].(*ast.BasicLit); ok && lit.Kind == token.STRING {
key := strings.Trim(lit.Value, `"`)
if !slices.Contains(keys, key) {
untranslated = append(untranslated, key)
}
if key := searchUntranslatedKeyInCall(path, fs, node, call, call.Args[1], keys); key != "" {
untranslated = append(untranslated, key)
}
if lit, ok := call.Args[2].(*ast.BasicLit); ok && lit.Kind == token.STRING {
key := strings.Trim(lit.Value, `"`)
if !slices.Contains(keys, key) {
untranslated = append(untranslated, key)
}
if key := searchUntranslatedKeyInCall(path, fs, node, call, call.Args[2], keys); key != "" {
untranslated = append(untranslated, key)
}
}
}
@ -205,29 +246,6 @@ func searchUnTranslatedKeyInFile(dir, path string, keys []string) ([]string, err
return append(untranslatedKeys, untranslatedKeysInTmpl...), nil
}
var whitelist = []string{
"repo.signing.wont_sign.*",
"repo.issues.role.*",
"repo.commitstatus.*",
"admin.dashboard.*",
"admin.dashboard.cron.*",
"admin.dashboard.task.*",
"repo.migrate.*.description",
"actions.runners.status.*",
"projects.*.display_name",
"admin.notices.*",
"form.NewBranchName", // FIXME: used in integration tests only
}
func isWhitelisted(key string) bool {
for _, w := range whitelist {
if glob.MustCompile(w).Match(key) {
return true
}
}
return false
}
func main() {
if len(os.Args) != 1 {
println("usage: clean-locales")
@ -248,9 +266,6 @@ func main() {
} else {
trKey = section.Name() + "." + key.Name()
}
if isWhitelisted(trKey) {
continue
}
keys = append(keys, trKey)
}
}