feat(i18n): complete cli localization foundation
This commit is contained in:
parent
29401c83e5
commit
9f0c63d2fb
|
|
@ -2,7 +2,9 @@ package auth
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
|
@ -38,54 +40,54 @@ func newLoginCmd(tr *i18n.Translator) *cobra.Command {
|
|||
Short: tr.T("cmd.auth.login.short"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if tokenMode {
|
||||
return loginWithToken()
|
||||
return loginWithToken(cmd.InOrStdin(), cmd.OutOrStdout(), tr)
|
||||
}
|
||||
return loginWithPassword()
|
||||
return loginWithPassword(cmd.OutOrStdout(), tr)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&tokenMode, "token", false, tr.T("flag.auth.token"))
|
||||
return cmd
|
||||
}
|
||||
|
||||
func loginWithPassword() error {
|
||||
func loginWithPassword(out io.Writer, tr *i18n.Translator) error {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Print("Username/Email/Phone: ")
|
||||
fmt.Fprint(out, tr.T("prompt.auth.username"))
|
||||
username, _ := reader.ReadString('\n')
|
||||
username = strings.TrimSpace(username)
|
||||
|
||||
fmt.Print("Password: ")
|
||||
fmt.Fprint(out, tr.T("prompt.auth.password"))
|
||||
passwordBytes, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read password: %w", err)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Fprintln(out)
|
||||
password := string(passwordBytes)
|
||||
|
||||
result, err := internalAuth.Login(username, password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("login failed: %w", err)
|
||||
return errors.New(tr.Tf("error.auth.login_failed", i18n.Args{"message": err.Error()}))
|
||||
}
|
||||
|
||||
fmt.Printf("✓ Logged in as %s\n", result.Login)
|
||||
fmt.Fprintln(out, tr.Tf("success.auth.logged_in_as", i18n.Args{"login": result.Login}))
|
||||
return nil
|
||||
}
|
||||
|
||||
func loginWithToken() error {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print("Paste your token: ")
|
||||
func loginWithToken(in io.Reader, out io.Writer, tr *i18n.Translator) error {
|
||||
reader := bufio.NewReader(in)
|
||||
fmt.Fprint(out, tr.T("prompt.auth.token"))
|
||||
token, _ := reader.ReadString('\n')
|
||||
token = strings.TrimSpace(token)
|
||||
|
||||
if token == "" {
|
||||
return fmt.Errorf("token cannot be empty")
|
||||
return errors.New(tr.T("error.auth.token_empty"))
|
||||
}
|
||||
|
||||
if err := internalAuth.StoreToken(token); err != nil {
|
||||
return fmt.Errorf("failed to store token: %w", err)
|
||||
return errors.New(tr.Tf("error.auth.store_token_failed", i18n.Args{"message": err.Error()}))
|
||||
}
|
||||
|
||||
fmt.Println("✓ Token saved")
|
||||
fmt.Fprintln(out, tr.T("success.auth.token_saved"))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +99,7 @@ func newLogoutCmd(tr *i18n.Translator) *cobra.Command {
|
|||
if err := internalAuth.DeleteToken(); err != nil {
|
||||
return fmt.Errorf("failed to delete token: %w", err)
|
||||
}
|
||||
fmt.Println("✓ Logged out")
|
||||
fmt.Fprintln(cmd.OutOrStdout(), tr.T("success.auth.logged_out"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
@ -110,35 +112,35 @@ func newStatusCmd(tr *i18n.Translator) *cobra.Command {
|
|||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Check env var token first
|
||||
if envToken := os.Getenv(envTokenVar); envToken != "" {
|
||||
fmt.Printf("✓ Logged in via %s environment variable\n", envTokenVar)
|
||||
fmt.Fprintln(cmd.OutOrStdout(), tr.Tf("success.auth.logged_in_via_env", i18n.Args{"env": envTokenVar}))
|
||||
}
|
||||
|
||||
token, err := internalAuth.LoadToken()
|
||||
if err != nil || token == "" {
|
||||
if os.Getenv(envTokenVar) == "" {
|
||||
fmt.Println("✗ Not logged in")
|
||||
fmt.Println(" Run: gitlink-cli auth login")
|
||||
fmt.Printf(" Or set %s environment variable\n", envTokenVar)
|
||||
fmt.Fprintln(cmd.OutOrStdout(), tr.T("warning.auth.not_logged_in"))
|
||||
fmt.Fprintln(cmd.OutOrStdout(), tr.T("output.auth.login_hint"))
|
||||
fmt.Fprintln(cmd.OutOrStdout(), tr.Tf("output.auth.env_hint", i18n.Args{"env": envTokenVar}))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
user, err := internalAuth.GetCurrentUser()
|
||||
if err != nil {
|
||||
fmt.Printf("✓ Token stored (but cannot verify: %v)\n", err)
|
||||
fmt.Fprintln(cmd.OutOrStdout(), tr.Tf("warning.auth.token_unverified", i18n.Args{"message": err.Error()}))
|
||||
return nil
|
||||
}
|
||||
|
||||
login, _ := user["login"].(string)
|
||||
name, _ := user["name"].(string)
|
||||
if login != "" {
|
||||
fmt.Printf("✓ Logged in as %s", login)
|
||||
text := tr.Tf("success.auth.logged_in_as", i18n.Args{"login": login})
|
||||
if name != "" {
|
||||
fmt.Printf(" (%s)", name)
|
||||
text = fmt.Sprintf("%s (%s)", text, name)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Fprintln(cmd.OutOrStdout(), text)
|
||||
} else {
|
||||
fmt.Println("✓ Token stored (user info unavailable)")
|
||||
fmt.Fprintln(cmd.OutOrStdout(), tr.T("warning.auth.user_unavailable"))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -31,9 +32,9 @@ func newInitCmd(tr *i18n.Translator) *cobra.Command {
|
|||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg := internalConfig.DefaultConfig()
|
||||
if err := internalConfig.Save(cfg); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
return errors.New(tr.Tf("error.config.save_failed", i18n.Args{"message": err.Error()}))
|
||||
}
|
||||
fmt.Printf("✓ Config initialized at %s\n", internalConfig.ConfigPath())
|
||||
fmt.Fprintln(cmd.OutOrStdout(), tr.Tf("success.config.initialized", i18n.Args{"path": internalConfig.ConfigPath()}))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
@ -65,9 +66,9 @@ func newGetCmd(tr *i18n.Translator) *cobra.Command {
|
|||
return err
|
||||
}
|
||||
if val == "" {
|
||||
fmt.Printf("%s: (not set)\n", args[0])
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%s: %s\n", args[0], tr.T("output.config.not_set"))
|
||||
} else {
|
||||
fmt.Printf("%s: %s\n", args[0], val)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%s: %s\n", args[0], val)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
|
@ -83,12 +84,13 @@ func newListCmd(tr *i18n.Translator) *cobra.Command {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("base_url: %s\n", cfg.BaseURL)
|
||||
fmt.Printf("default_format: %s\n", cfg.Format)
|
||||
fmt.Printf("editor: %s\n", cfg.Editor)
|
||||
fmt.Printf("pager: %s\n", cfg.Pager)
|
||||
fmt.Printf("lang: %s\n", cfg.Lang)
|
||||
fmt.Printf("\nConfig file: %s\n", internalConfig.ConfigPath())
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "base_url: %s\n", cfg.BaseURL)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "default_format: %s\n", cfg.Format)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "editor: %s\n", cfg.Editor)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "pager: %s\n", cfg.Pager)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "lang: %s\n", cfg.Lang)
|
||||
fmt.Fprintln(cmd.OutOrStdout())
|
||||
fmt.Fprintln(cmd.OutOrStdout(), tr.Tf("output.config.file", i18n.Args{"path": internalConfig.ConfigPath()}))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
|
|||
36
cmd/root.go
36
cmd/root.go
|
|
@ -1,6 +1,7 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
|
|
@ -18,14 +19,16 @@ import (
|
|||
var Version = "dev"
|
||||
|
||||
type RootOptions struct {
|
||||
Version string
|
||||
Args []string
|
||||
Version string
|
||||
Args []string
|
||||
Env map[string]string
|
||||
ConfigLang string
|
||||
}
|
||||
|
||||
func NewRootCmd(opts RootOptions, tr *i18n.Translator) (*cobra.Command, error) {
|
||||
if tr == nil {
|
||||
var err error
|
||||
tr, err = i18n.New(i18n.Options{Locale: "en-US"})
|
||||
tr, err = newTranslator(opts.Args, opts.Env, opts.ConfigLang)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -75,15 +78,12 @@ func newVersionCmd(version string, tr *i18n.Translator) *cobra.Command {
|
|||
|
||||
func Execute() error {
|
||||
args := os.Args[1:]
|
||||
tr, err := newTranslator(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rootCmd, err := NewRootCmd(RootOptions{
|
||||
Version: Version,
|
||||
Args: args,
|
||||
}, tr)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -94,17 +94,27 @@ func Execute() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func newTranslator(args []string) (*i18n.Translator, error) {
|
||||
func newTranslator(args []string, env map[string]string, configLang string) (*i18n.Translator, error) {
|
||||
available, err := i18n.AvailableLocales()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
locale := i18n.ResolveLocale(i18n.ResolveOptions{
|
||||
if env == nil {
|
||||
env = i18n.EnvMap()
|
||||
}
|
||||
if configLang == "" {
|
||||
configLang = loadConfigLangBestEffort()
|
||||
}
|
||||
resolved := i18n.ResolveLocaleDetailed(i18n.ResolveOptions{
|
||||
ExplicitLang: i18n.PreScanLang(args),
|
||||
Env: i18n.EnvMap(),
|
||||
ConfigLang: loadConfigLangBestEffort(),
|
||||
Env: env,
|
||||
ConfigLang: configLang,
|
||||
}, available)
|
||||
return i18n.New(i18n.Options{Locale: locale})
|
||||
if !resolved.Supported && (resolved.Source == "flag" || resolved.Source == "env") {
|
||||
tr := i18n.Default()
|
||||
return nil, errors.New(tr.Tf("error.unsupported_language", i18n.Args{"lang": resolved.Requested}))
|
||||
}
|
||||
return i18n.New(i18n.Options{Locale: resolved.Locale})
|
||||
}
|
||||
|
||||
func loadConfigLangBestEffort() string {
|
||||
|
|
|
|||
109
cmd/root_test.go
109
cmd/root_test.go
|
|
@ -40,6 +40,115 @@ func TestRootHelpUsesSelectedLocale(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRootHelpUsesExplicitLang(t *testing.T) {
|
||||
root, err := NewRootCmd(RootOptions{Version: "test", Args: []string{"--lang", "zh-CN", "--help"}, Env: map[string]string{}}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
help := out.String()
|
||||
for _, want := range []string{"用于管理 GitLink", "显示语言", "仓库"} {
|
||||
if !strings.Contains(help, want) {
|
||||
t.Fatalf("expected %q in help, got:\n%s", want, help)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootHelpUsesEnvLang(t *testing.T) {
|
||||
root, err := NewRootCmd(RootOptions{
|
||||
Version: "test",
|
||||
Args: []string{"repo", "--help"},
|
||||
Env: map[string]string{"GITLINK_LANG": "zh-CN"},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
help := out.String()
|
||||
for _, want := range []string{"仓库操作", "仓库所有者", "仓库名称"} {
|
||||
if !strings.Contains(help, want) {
|
||||
t.Fatalf("expected %q in help, got:\n%s", want, help)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExplicitLangOverridesConfigLang(t *testing.T) {
|
||||
root, err := NewRootCmd(RootOptions{
|
||||
Version: "test",
|
||||
Args: []string{"--lang", "en-US", "--help"},
|
||||
Env: map[string]string{},
|
||||
ConfigLang: "zh-CN",
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
help := out.String()
|
||||
if !strings.Contains(help, "Repository operations") {
|
||||
t.Fatalf("expected English help, got:\n%s", help)
|
||||
}
|
||||
if strings.Contains(help, "仓库操作") {
|
||||
t.Fatalf("expected explicit en-US to override config zh-CN, got:\n%s", help)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsupportedExplicitLangReturnsError(t *testing.T) {
|
||||
_, err := NewRootCmd(RootOptions{
|
||||
Version: "test",
|
||||
Args: []string{"--lang", "fr-FR", "--help"},
|
||||
Env: map[string]string{},
|
||||
}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected unsupported language error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unsupported language") {
|
||||
t.Fatalf("expected unsupported language error, got %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireArgUsesLocalizedError(t *testing.T) {
|
||||
root, err := NewRootCmd(RootOptions{
|
||||
Version: "test",
|
||||
Args: []string{"--lang", "zh-CN", "repo", "+create"},
|
||||
Env: map[string]string{},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
err = root.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected missing required flag error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "缺少必需参数") {
|
||||
t.Fatalf("expected localized missing flag error, got %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreCommandHelpUsesSelectedLocale(t *testing.T) {
|
||||
tr, err := i18n.New(i18n.Options{Locale: "zh-CN"})
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
# GitLink CLI i18n Guide
|
||||
|
||||
## Goals
|
||||
|
||||
GitLink CLI localizes human-facing command-line text while keeping machine-readable output stable. The i18n layer is infrastructure, not a place to store every string in the project.
|
||||
|
||||
## Translate
|
||||
|
||||
- Cobra command `Short`, `Long`, and human-facing examples.
|
||||
- Flag usage text.
|
||||
- User-facing errors.
|
||||
- Interactive prompts.
|
||||
- Success messages.
|
||||
- Warnings.
|
||||
- Confirmation messages.
|
||||
- Table column labels when the output is meant for humans.
|
||||
|
||||
## Do Not Translate
|
||||
|
||||
- JSON field names.
|
||||
- Raw API response bodies.
|
||||
- Debug logs and developer diagnostics.
|
||||
- Machine-readable status enum values.
|
||||
- HTTP methods, paths, query keys, and payload field names.
|
||||
- Long-form README documentation.
|
||||
- Test assertion descriptions.
|
||||
|
||||
## Key Names
|
||||
|
||||
Use stable, descriptive keys:
|
||||
|
||||
- `cmd.*` for command help.
|
||||
- `flag.*` for flag usage.
|
||||
- `error.*` for user-facing errors.
|
||||
- `prompt.*` for interactive input prompts.
|
||||
- `success.*` for successful user-facing operations.
|
||||
- `warning.*` for warnings.
|
||||
- `confirm.*` for confirmation prompts.
|
||||
- `table.*` for human table headers.
|
||||
|
||||
Do not invent numbered keys such as `msg001`. Prefer names that describe ownership and intent, for example `error.missing_required_flag`.
|
||||
|
||||
## Adding Text
|
||||
|
||||
1. Add the key to `internal/i18n/locales/en-US.json`.
|
||||
2. Add the same key to every other locale, including `zh-CN.json`.
|
||||
3. Keep placeholders identical across locales, for example `{name}`.
|
||||
4. Use `tr.T("key")` or `tr.Tf("key", i18n.Args{...})`.
|
||||
5. Run:
|
||||
|
||||
```powershell
|
||||
go run ./internal/i18n/cmd/check
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Use `go run ./internal/i18n/cmd/check --fix` to format locale JSON.
|
||||
|
||||
## Runtime Access
|
||||
|
||||
Command construction receives `*i18n.Translator` from `NewRootCmd`. Shortcut execution receives the same translator through `RuntimeContext.Tr`.
|
||||
|
||||
New command code should receive a translator explicitly. `i18n.Default()` exists only as a legacy migration fallback and should not be used for new command paths.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- Locale JSON is sorted and formatted with two spaces.
|
||||
- Every locale has the same keys as `en-US`.
|
||||
- Template placeholders match across locales.
|
||||
- New command/runtime text uses i18n only when it is human-facing.
|
||||
- JSON output, API raw responses, debug logs, and machine-readable values are unchanged.
|
||||
|
|
@ -1,13 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fix := flag.Bool("fix", false, "format locale JSON files")
|
||||
scanCode := flag.Bool("scan-code", false, "scan Go source for referenced i18n keys")
|
||||
flag.Parse()
|
||||
|
||||
problems, err := i18n.Validate(i18n.NewEmbedLoader(), "en-US")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
|
|
@ -19,5 +30,128 @@ func main() {
|
|||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := checkLocaleFormat(*fix); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if *scanCode {
|
||||
if err := checkCodeReferences(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
fmt.Println("i18n messages are valid")
|
||||
}
|
||||
|
||||
func checkLocaleFormat(fix bool) error {
|
||||
files, err := filepath.Glob(filepath.Join("internal", "i18n", "locales", "*.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, path := range files {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
formatted, err := formatJSON(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
if string(data) == string(formatted) {
|
||||
continue
|
||||
}
|
||||
if fix {
|
||||
if err := os.WriteFile(path, formatted, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("%s: locale JSON is not formatted; run go run ./internal/i18n/cmd/check --fix", path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatJSON(data []byte) ([]byte, error) {
|
||||
var messages map[string]string
|
||||
if err := json.Unmarshal(data, &messages); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(messages); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func checkCodeReferences() error {
|
||||
loader := i18n.NewEmbedLoader()
|
||||
base, err := loader.Load("en-US")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
used, defaultUses, err := scanCodeKeys([]string{"cmd", "shortcuts", "internal"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var missing []string
|
||||
for key := range used {
|
||||
if _, ok := base[key]; !ok {
|
||||
missing = append(missing, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(missing)
|
||||
if len(missing) > 0 {
|
||||
return fmt.Errorf("missing i18n key references: %s", strings.Join(missing, ", "))
|
||||
}
|
||||
for _, item := range defaultUses {
|
||||
fmt.Fprintf(os.Stderr, "warning: avoid new i18n.Default() usage at %s\n", item)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanCodeKeys(roots []string) (map[string]struct{}, []string, error) {
|
||||
keyPattern := regexp.MustCompile(`(?:tr|ctx\.Tr|i18n\.Default\(\))\.T(?:f)?\("([^"]+)"`)
|
||||
defaultPattern := regexp.MustCompile(`i18n\.Default\(\)\.T(?:f)?\("([^"]+)"`)
|
||||
used := map[string]struct{}{}
|
||||
var defaultUses []string
|
||||
for _, root := range roots {
|
||||
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if entry.IsDir() {
|
||||
if strings.Contains(filepath.ToSlash(path), "internal/i18n/locales") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if filepath.Ext(path) != ".go" {
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
text := string(data)
|
||||
for _, match := range keyPattern.FindAllStringSubmatch(text, -1) {
|
||||
used[match[1]] = struct{}{}
|
||||
}
|
||||
for _, match := range defaultPattern.FindAllStringSubmatchIndex(text, -1) {
|
||||
line := 1 + strings.Count(text[:match[0]], "\n")
|
||||
defaultUses = append(defaultUses, fmt.Sprintf("%s:%d", filepath.ToSlash(path), line))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
sort.Strings(defaultUses)
|
||||
return used, defaultUses, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,48 +35,50 @@ func NormalizeLocale(locale string) string {
|
|||
return strings.Join(normalized, "-")
|
||||
}
|
||||
|
||||
// MatchLocale resolves requested to one of available using exact, normalized,
|
||||
// language-primary, then fallback matching.
|
||||
// MatchLocale resolves requested to one of available using exact, safe alias,
|
||||
// then fallback matching.
|
||||
func MatchLocale(requested string, available []string, fallback string) string {
|
||||
return matchLocale(requested, available, fallback).Locale
|
||||
}
|
||||
|
||||
type localeMatch struct {
|
||||
Locale string
|
||||
Requested string
|
||||
Fallbacked bool
|
||||
Supported bool
|
||||
}
|
||||
|
||||
func matchLocale(requested string, available []string, fallback string) localeMatch {
|
||||
fallback = NormalizeLocale(fallback)
|
||||
if fallback == "" {
|
||||
fallback = defaultFallbackLocale
|
||||
}
|
||||
if len(available) == 0 {
|
||||
return fallback
|
||||
return localeMatch{Locale: fallback, Requested: NormalizeLocale(requested), Fallbacked: true}
|
||||
}
|
||||
|
||||
byLocale := make(map[string]string, len(available))
|
||||
byPrimary := make(map[string]string, len(available))
|
||||
for _, locale := range available {
|
||||
normalized := NormalizeLocale(locale)
|
||||
byLocale[normalized] = normalized
|
||||
if primary := primaryLanguage(normalized); primary != "" {
|
||||
if _, exists := byPrimary[primary]; !exists {
|
||||
byPrimary[primary] = normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
candidate := NormalizeLocale(requested)
|
||||
if candidate != "" {
|
||||
if matched, ok := byLocale[candidate]; ok {
|
||||
return matched
|
||||
return localeMatch{Locale: matched, Requested: candidate, Supported: true}
|
||||
}
|
||||
if alias := localeAlias(candidate); alias != "" {
|
||||
if matched, ok := byLocale[alias]; ok {
|
||||
return matched
|
||||
return localeMatch{Locale: matched, Requested: candidate, Supported: true}
|
||||
}
|
||||
}
|
||||
if matched, ok := byPrimary[primaryLanguage(candidate)]; ok {
|
||||
return matched
|
||||
}
|
||||
}
|
||||
|
||||
if matched, ok := byLocale[fallback]; ok {
|
||||
return matched
|
||||
return localeMatch{Locale: matched, Requested: candidate, Fallbacked: candidate != "", Supported: false}
|
||||
}
|
||||
return NormalizeLocale(available[0])
|
||||
return localeMatch{Locale: NormalizeLocale(available[0]), Requested: candidate, Fallbacked: candidate != "", Supported: false}
|
||||
}
|
||||
|
||||
func primaryLanguage(locale string) string {
|
||||
|
|
@ -87,10 +89,10 @@ func primaryLanguage(locale string) string {
|
|||
}
|
||||
|
||||
func localeAlias(locale string) string {
|
||||
switch primaryLanguage(locale) {
|
||||
case "zh":
|
||||
switch {
|
||||
case locale == "zh" || locale == "zh-CN" || locale == "zh-Hans" || locale == "zh-Hans-CN":
|
||||
return "zh-CN"
|
||||
case "en":
|
||||
case primaryLanguage(locale) == "en":
|
||||
return "en-US"
|
||||
default:
|
||||
return ""
|
||||
|
|
|
|||
|
|
@ -1,164 +1,184 @@
|
|||
{
|
||||
"cmd.root.short": "GitLink CLI - command-line tool for GitLink",
|
||||
"cmd.root.long": "Manage repositories, issues, pull requests, releases, CI and workflows on GitLink.",
|
||||
"cmd.api.short": "Make raw API requests to GitLink",
|
||||
"cmd.api.long": "Send arbitrary HTTP requests to the GitLink API. Authentication is injected automatically.",
|
||||
"cmd.auth.short": "Authentication commands",
|
||||
"cmd.api.short": "Make raw API requests to GitLink",
|
||||
"cmd.auth.login.short": "Login to GitLink",
|
||||
"cmd.auth.logout.short": "Logout from GitLink",
|
||||
"cmd.auth.short": "Authentication commands",
|
||||
"cmd.auth.status.short": "Show authentication status",
|
||||
"cmd.config.short": "Manage gitlink-cli configuration",
|
||||
"cmd.config.init.short": "Initialize configuration file",
|
||||
"cmd.config.set.short": "Set a configuration value",
|
||||
"cmd.config.get.short": "Get a configuration value",
|
||||
"cmd.config.list.short": "List all configuration values",
|
||||
"cmd.version.short": "Print version information",
|
||||
"cmd.repo.short": "Repository operations",
|
||||
"cmd.repo.list.short": "List repositories for a user or organization",
|
||||
"cmd.repo.info.short": "Show repository details",
|
||||
"cmd.repo.create.short": "Create a new repository",
|
||||
"cmd.repo.fork.short": "Fork a repository",
|
||||
"cmd.repo.delete.short": "Delete a repository",
|
||||
"cmd.issue.short": "Issue operations",
|
||||
"cmd.issue.list.short": "List issues",
|
||||
"cmd.issue.create.short": "Create a new issue",
|
||||
"cmd.issue.view.short": "View issue details",
|
||||
"cmd.issue.close.short": "Close an issue",
|
||||
"cmd.issue.update.short": "Update an issue",
|
||||
"cmd.issue.comment.short": "Add a comment to an issue",
|
||||
"cmd.issue.batch_list.short": "List issue batch maintenance candidates without changing remote data",
|
||||
"cmd.issue.batch_list.long": "List issue batch maintenance candidates without changing remote data.\n\nExamples:\n gitlink-cli issue +batch-list --owner Gitlink --repo gitlink-cli --state open --older-than-days 30 --limit 50 --format table\n gitlink-cli issue +batch-list --owner Gitlink --repo gitlink-cli --label bug --format json",
|
||||
"cmd.issue.batch_close.short": "Close filtered issues in bulk. Defaults to dry-run; pass --yes to execute.",
|
||||
"cmd.issue.batch_close.long": "Close filtered issues in bulk.\n\nThis command defaults to dry-run mode and only prints matching issues.\nPass --yes to execute remote close operations. Use restrictive filters and a small limit.\n\nExamples:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes",
|
||||
"cmd.issue.batch_label.short": "Add a label to filtered issues in bulk. Defaults to dry-run; pass --yes to execute.",
|
||||
"cmd.issue.batch_label.long": "Add a label to filtered issues in bulk.\n\nThis command defaults to dry-run mode and only prints matching issues.\nPass --yes to execute remote label operations. The current implementation does not fake label writes when the API endpoint is unavailable.\n\nExamples:\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50 --yes",
|
||||
"cmd.pr.short": "Pull request operations",
|
||||
"cmd.pr.list.short": "List pull requests",
|
||||
"cmd.pr.create.short": "Create a pull request",
|
||||
"cmd.pr.view.short": "View pull request details",
|
||||
"cmd.pr.merge.short": "Merge a pull request",
|
||||
"cmd.pr.close.short": "Close a pull request",
|
||||
"cmd.pr.files.short": "List changed files in a pull request",
|
||||
"cmd.pr.diff.short": "Show diff for a pull request",
|
||||
"cmd.pr.versions.short": "List pull request patchset versions",
|
||||
"cmd.pr.version_diff.short": "Show diff for a pull request patchset version",
|
||||
"cmd.pr.reviews.short": "List pull request reviews",
|
||||
"cmd.pr.review.short": "Create a pull request review",
|
||||
"cmd.pr.comment.short": "Add a comment to a pull request",
|
||||
"cmd.release.short": "Release operations",
|
||||
"cmd.release.list.short": "List releases",
|
||||
"cmd.release.create.short": "Create a release",
|
||||
"cmd.release.view.short": "View release details",
|
||||
"cmd.release.delete.short": "Delete a release",
|
||||
"cmd.branch.short": "Branch operations",
|
||||
"cmd.branch.list.short": "List branches",
|
||||
"cmd.branch.create.short": "Create a branch",
|
||||
"cmd.branch.delete.short": "Delete a branch",
|
||||
"cmd.branch.list.short": "List branches",
|
||||
"cmd.branch.protect.short": "Set branch protection",
|
||||
"cmd.branch.short": "Branch operations",
|
||||
"cmd.branch.unprotect.short": "Remove branch protection",
|
||||
"cmd.org.short": "Organization operations",
|
||||
"cmd.org.list.short": "List organizations",
|
||||
"cmd.org.info.short": "Show organization details",
|
||||
"cmd.org.members.short": "List organization members",
|
||||
"cmd.org.create.short": "Create an organization",
|
||||
"cmd.user.short": "User operations",
|
||||
"cmd.user.me.short": "Show current authenticated user",
|
||||
"cmd.user.info.short": "Show user profile",
|
||||
"cmd.search.short": "Search operations",
|
||||
"cmd.search.repos.short": "Search repositories",
|
||||
"cmd.search.users.short": "Search users",
|
||||
"cmd.ci.short": "CI/CD operations",
|
||||
"cmd.ci.builds.short": "List CI builds",
|
||||
"cmd.ci.logs.short": "View build logs",
|
||||
"cmd.ci.restart.short": "Restart a build",
|
||||
"cmd.ci.short": "CI/CD operations",
|
||||
"cmd.ci.stop.short": "Stop a build",
|
||||
"cmd.webhook.short": "Webhook operations",
|
||||
"cmd.webhook.list.short": "List repository webhooks",
|
||||
"cmd.webhook.view.short": "View webhook details",
|
||||
"cmd.config.get.short": "Get a configuration value",
|
||||
"cmd.config.init.short": "Initialize configuration file",
|
||||
"cmd.config.list.short": "List all configuration values",
|
||||
"cmd.config.set.short": "Set a configuration value",
|
||||
"cmd.config.short": "Manage gitlink-cli configuration",
|
||||
"cmd.issue.batch_close.long": "Close filtered issues in bulk.\n\nThis command defaults to dry-run mode and only prints matching issues.\nPass --yes to execute remote close operations. Use restrictive filters and a small limit.\n\nExamples:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes",
|
||||
"cmd.issue.batch_close.short": "Close filtered issues in bulk. Defaults to dry-run; pass --yes to execute.",
|
||||
"cmd.issue.batch_label.long": "Add a label to filtered issues in bulk.\n\nThis command defaults to dry-run mode and only prints matching issues.\nPass --yes to execute remote label operations. The current implementation does not fake label writes when the API endpoint is unavailable.\n\nExamples:\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50 --yes",
|
||||
"cmd.issue.batch_label.short": "Add a label to filtered issues in bulk. Defaults to dry-run; pass --yes to execute.",
|
||||
"cmd.issue.batch_list.long": "List issue batch maintenance candidates without changing remote data.\n\nExamples:\n gitlink-cli issue +batch-list --owner Gitlink --repo gitlink-cli --state open --older-than-days 30 --limit 50 --format table\n gitlink-cli issue +batch-list --owner Gitlink --repo gitlink-cli --label bug --format json",
|
||||
"cmd.issue.batch_list.short": "List issue batch maintenance candidates without changing remote data",
|
||||
"cmd.issue.close.short": "Close an issue",
|
||||
"cmd.issue.comment.short": "Add a comment to an issue",
|
||||
"cmd.issue.create.short": "Create a new issue",
|
||||
"cmd.issue.list.short": "List issues",
|
||||
"cmd.issue.short": "Issue operations",
|
||||
"cmd.issue.update.short": "Update an issue",
|
||||
"cmd.issue.view.short": "View issue details",
|
||||
"cmd.org.create.short": "Create an organization",
|
||||
"cmd.org.info.short": "Show organization details",
|
||||
"cmd.org.list.short": "List organizations",
|
||||
"cmd.org.members.short": "List organization members",
|
||||
"cmd.org.short": "Organization operations",
|
||||
"cmd.pr.close.short": "Close a pull request",
|
||||
"cmd.pr.comment.short": "Add a comment to a pull request",
|
||||
"cmd.pr.create.short": "Create a pull request",
|
||||
"cmd.pr.diff.short": "Show diff for a pull request",
|
||||
"cmd.pr.files.short": "List changed files in a pull request",
|
||||
"cmd.pr.list.short": "List pull requests",
|
||||
"cmd.pr.merge.short": "Merge a pull request",
|
||||
"cmd.pr.review.short": "Create a pull request review",
|
||||
"cmd.pr.reviews.short": "List pull request reviews",
|
||||
"cmd.pr.short": "Pull request operations",
|
||||
"cmd.pr.version_diff.short": "Show diff for a pull request patchset version",
|
||||
"cmd.pr.versions.short": "List pull request patchset versions",
|
||||
"cmd.pr.view.short": "View pull request details",
|
||||
"cmd.release.create.short": "Create a release",
|
||||
"cmd.release.delete.short": "Delete a release",
|
||||
"cmd.release.list.short": "List releases",
|
||||
"cmd.release.short": "Release operations",
|
||||
"cmd.release.view.short": "View release details",
|
||||
"cmd.repo.create.short": "Create a new repository",
|
||||
"cmd.repo.delete.short": "Delete a repository",
|
||||
"cmd.repo.fork.short": "Fork a repository",
|
||||
"cmd.repo.info.short": "Show repository details",
|
||||
"cmd.repo.list.short": "List repositories for a user or organization",
|
||||
"cmd.repo.short": "Repository operations",
|
||||
"cmd.root.long": "Manage repositories, issues, pull requests, releases, CI and workflows on GitLink.",
|
||||
"cmd.root.short": "GitLink CLI - command-line tool for GitLink",
|
||||
"cmd.search.repos.short": "Search repositories",
|
||||
"cmd.search.short": "Search operations",
|
||||
"cmd.search.users.short": "Search users",
|
||||
"cmd.user.info.short": "Show user profile",
|
||||
"cmd.user.me.short": "Show current authenticated user",
|
||||
"cmd.user.short": "User operations",
|
||||
"cmd.version.short": "Print version information",
|
||||
"cmd.webhook.create.short": "Create a repository webhook",
|
||||
"cmd.webhook.update.short": "Update a repository webhook while preserving unspecified fields when available",
|
||||
"cmd.webhook.delete.short": "Delete a repository webhook",
|
||||
"cmd.webhook.test.short": "Trigger a test delivery for a webhook",
|
||||
"cmd.webhook.list.short": "List repository webhooks",
|
||||
"cmd.webhook.short": "Webhook operations",
|
||||
"cmd.webhook.tasks.short": "List webhook delivery tasks",
|
||||
"flag.owner": "Repository owner (auto-detected from git remote)",
|
||||
"flag.repo": "Repository name (auto-detected from git remote)",
|
||||
"flag.format": "Output format: json, table, yaml (default: table)",
|
||||
"flag.debug": "Enable debug output",
|
||||
"flag.lang": "Display language",
|
||||
"cmd.webhook.test.short": "Trigger a test delivery for a webhook",
|
||||
"cmd.webhook.update.short": "Update a repository webhook while preserving unspecified fields when available",
|
||||
"cmd.webhook.view.short": "View webhook details",
|
||||
"error.auth.login_failed": "login failed: {message}",
|
||||
"error.auth.store_token_failed": "failed to store token: {message}",
|
||||
"error.auth.token_empty": "token cannot be empty",
|
||||
"error.config.save_failed": "failed to save config: {message}",
|
||||
"error.missing_required_flag": "required flag --{name} is missing",
|
||||
"error.unsupported_language": "unsupported language: {lang}",
|
||||
"flag.api.body": "Request body (JSON string)",
|
||||
"flag.api.query": "Query parameters (key=val&key2=val2)",
|
||||
"flag.api.header": "Additional headers (key:value)",
|
||||
"flag.api.query": "Query parameters (key=val&key2=val2)",
|
||||
"flag.auth.token": "Login by pasting an existing token",
|
||||
"flag.user": "User login (default: current user)",
|
||||
"flag.user.login": "User login name",
|
||||
"flag.page": "Page number",
|
||||
"flag.limit": "Items per page",
|
||||
"flag.description": "Description",
|
||||
"flag.dry_run": "Preview the request without creating it",
|
||||
"flag.comment.body": "Comment body",
|
||||
"flag.repo.category": "Filter: manage/mirror/sync/fork/all (default: manage)",
|
||||
"flag.repo.name": "Repository name",
|
||||
"flag.repo.description": "Repository description",
|
||||
"flag.repo.private": "Make repository private (true/false)",
|
||||
"flag.issue.state": "Filter by state: open, closed, all",
|
||||
"flag.issue.title": "Issue title",
|
||||
"flag.issue.body": "Issue description",
|
||||
"flag.issue.assignee": "Assignee login",
|
||||
"flag.issue.milestone": "Milestone ID",
|
||||
"flag.issue.label": "Label ID",
|
||||
"flag.issue.number": "Issue number (as shown in the web URL)",
|
||||
"flag.issue.new_title": "New title",
|
||||
"flag.issue.new_body": "New description",
|
||||
"flag.issue.new_state": "New state: open, closed, or numeric status_id",
|
||||
"flag.issue.label_filter": "Filter by existing label",
|
||||
"flag.issue.older_than_days": "Only include issues inactive for at least this many days",
|
||||
"flag.issue.batch_list.limit": "Maximum issues to return, capped at 100",
|
||||
"flag.issue.batch_close.state": "Filter by issue state before closing",
|
||||
"flag.issue.batch_close.older_than_days": "Required safety filter; must be at least 7",
|
||||
"flag.issue.batch_process.limit": "Maximum issues to process, capped at 100",
|
||||
"flag.issue.batch.yes": "Execute remote operations. Without this flag the command is dry-run only.",
|
||||
"flag.issue.batch.reason": "Optional reason shown in the batch result",
|
||||
"flag.issue.batch_label.state": "Filter by issue state",
|
||||
"flag.issue.add_label": "Label to add to each matching issue",
|
||||
"flag.pr.state": "Filter: open, merged, closed",
|
||||
"flag.pr.title": "PR title",
|
||||
"flag.pr.body": "PR description",
|
||||
"flag.pr.head": "Source branch",
|
||||
"flag.pr.base": "Target branch",
|
||||
"flag.pr.id": "PR number",
|
||||
"flag.pr.merge_method": "Merge method: merge, rebase, squash",
|
||||
"flag.pr.version_id": "Patchset version ID",
|
||||
"flag.pr.file": "Filter diff by file path",
|
||||
"flag.pr.review_status_filter": "Filter review status: common, approved, rejected",
|
||||
"flag.pr.review_status": "Review status: common, approved, rejected",
|
||||
"flag.pr.review_content": "Review content",
|
||||
"flag.pr.review_commit": "Commit SHA to attach the review to",
|
||||
"flag.branch.name": "Branch name",
|
||||
"flag.branch.from": "Source branch or commit",
|
||||
"flag.release.tag": "Tag name",
|
||||
"flag.release.name": "Release name",
|
||||
"flag.release.body": "Release notes",
|
||||
"flag.release.target": "Target branch",
|
||||
"flag.release.prerelease": "Mark as prerelease (true/false)",
|
||||
"flag.release.id_or_tag": "Release ID or tag",
|
||||
"flag.release.id": "Release ID",
|
||||
"flag.org.id_or_login": "Organization ID or login",
|
||||
"flag.org.id": "Organization ID",
|
||||
"flag.org.name": "Organization name",
|
||||
"flag.search.keyword": "Search keyword",
|
||||
"flag.branch.name": "Branch name",
|
||||
"flag.ci.build": "Build number",
|
||||
"flag.ci.stage": "Stage number",
|
||||
"flag.ci.step": "Step number",
|
||||
"flag.webhook.id": "Webhook ID",
|
||||
"flag.webhook.url": "Webhook target URL",
|
||||
"flag.webhook.events": "Comma-separated events, for example: push,issues_only",
|
||||
"flag.webhook.type": "Webhook type: gitea/slack/discord/dingtalk/telegram/msteams/feishu/matrix/jianmu/softbot",
|
||||
"flag.comment.body": "Comment body",
|
||||
"flag.debug": "Enable debug output",
|
||||
"flag.description": "Description",
|
||||
"flag.dry_run": "Preview the request without creating it",
|
||||
"flag.format": "Output format: json, table, yaml (default: table)",
|
||||
"flag.issue.add_label": "Label to add to each matching issue",
|
||||
"flag.issue.assignee": "Assignee login",
|
||||
"flag.issue.batch.reason": "Optional reason shown in the batch result",
|
||||
"flag.issue.batch.yes": "Execute remote operations. Without this flag the command is dry-run only.",
|
||||
"flag.issue.batch_close.older_than_days": "Required safety filter; must be at least 7",
|
||||
"flag.issue.batch_close.state": "Filter by issue state before closing",
|
||||
"flag.issue.batch_label.state": "Filter by issue state",
|
||||
"flag.issue.batch_list.limit": "Maximum issues to return, capped at 100",
|
||||
"flag.issue.batch_process.limit": "Maximum issues to process, capped at 100",
|
||||
"flag.issue.body": "Issue description",
|
||||
"flag.issue.label": "Label ID",
|
||||
"flag.issue.label_filter": "Filter by existing label",
|
||||
"flag.issue.milestone": "Milestone ID",
|
||||
"flag.issue.new_body": "New description",
|
||||
"flag.issue.new_state": "New state: open, closed, or numeric status_id",
|
||||
"flag.issue.new_title": "New title",
|
||||
"flag.issue.number": "Issue number (as shown in the web URL)",
|
||||
"flag.issue.older_than_days": "Only include issues inactive for at least this many days",
|
||||
"flag.issue.state": "Filter by state: open, closed, all",
|
||||
"flag.issue.title": "Issue title",
|
||||
"flag.lang": "Display language",
|
||||
"flag.limit": "Items per page",
|
||||
"flag.org.id": "Organization ID",
|
||||
"flag.org.id_or_login": "Organization ID or login",
|
||||
"flag.org.name": "Organization name",
|
||||
"flag.owner": "Repository owner (auto-detected from git remote)",
|
||||
"flag.page": "Page number",
|
||||
"flag.pr.base": "Target branch",
|
||||
"flag.pr.body": "PR description",
|
||||
"flag.pr.file": "Filter diff by file path",
|
||||
"flag.pr.head": "Source branch",
|
||||
"flag.pr.id": "PR number",
|
||||
"flag.pr.merge_method": "Merge method: merge, rebase, squash",
|
||||
"flag.pr.review_commit": "Commit SHA to attach the review to",
|
||||
"flag.pr.review_content": "Review content",
|
||||
"flag.pr.review_status": "Review status: common, approved, rejected",
|
||||
"flag.pr.review_status_filter": "Filter review status: common, approved, rejected",
|
||||
"flag.pr.state": "Filter: open, merged, closed",
|
||||
"flag.pr.title": "PR title",
|
||||
"flag.pr.version_id": "Patchset version ID",
|
||||
"flag.release.body": "Release notes",
|
||||
"flag.release.id": "Release ID",
|
||||
"flag.release.id_or_tag": "Release ID or tag",
|
||||
"flag.release.name": "Release name",
|
||||
"flag.release.prerelease": "Mark as prerelease (true/false)",
|
||||
"flag.release.tag": "Tag name",
|
||||
"flag.release.target": "Target branch",
|
||||
"flag.repo": "Repository name (auto-detected from git remote)",
|
||||
"flag.repo.category": "Filter: manage/mirror/sync/fork/all (default: manage)",
|
||||
"flag.repo.description": "Repository description",
|
||||
"flag.repo.name": "Repository name",
|
||||
"flag.repo.private": "Make repository private (true/false)",
|
||||
"flag.search.keyword": "Search keyword",
|
||||
"flag.user": "User login (default: current user)",
|
||||
"flag.user.login": "User login name",
|
||||
"flag.webhook.active": "Whether the webhook is active: true or false",
|
||||
"flag.webhook.branch_filter": "Branch glob filter for push/create/delete events",
|
||||
"flag.webhook.content_type": "Payload content type: json or form",
|
||||
"flag.webhook.events": "Comma-separated events, for example: push,issues_only",
|
||||
"flag.webhook.http_method": "HTTP method: POST or GET",
|
||||
"flag.webhook.id": "Webhook ID",
|
||||
"flag.webhook.secret": "Webhook secret",
|
||||
"flag.webhook.secret_update": "Webhook secret. Pass it again if the server does not return existing secrets.",
|
||||
"flag.webhook.branch_filter": "Branch glob filter for push/create/delete events",
|
||||
"flag.webhook.active": "Whether the webhook is active: true or false",
|
||||
"error.unsupported_language": "unsupported language: {lang}",
|
||||
"output.version": "gitlink-cli {version}"
|
||||
"flag.webhook.type": "Webhook type: gitea/slack/discord/dingtalk/telegram/msteams/feishu/matrix/jianmu/softbot",
|
||||
"flag.webhook.url": "Webhook target URL",
|
||||
"output.auth.env_hint": " Or set {env} environment variable",
|
||||
"output.auth.login_hint": " Run: gitlink-cli auth login",
|
||||
"output.config.file": "Config file: {path}",
|
||||
"output.config.not_set": "(not set)",
|
||||
"output.version": "gitlink-cli {version}",
|
||||
"prompt.auth.password": "Password: ",
|
||||
"prompt.auth.token": "Paste your access token: ",
|
||||
"prompt.auth.username": "Username/Email/Phone: ",
|
||||
"success.auth.logged_in_as": "✓ Logged in as {login}",
|
||||
"success.auth.logged_in_via_env": "✓ Logged in via {env} environment variable",
|
||||
"success.auth.logged_out": "✓ Logged out",
|
||||
"success.auth.token_saved": "✓ Token saved",
|
||||
"success.config.initialized": "✓ Config initialized at {path}",
|
||||
"warning.auth.not_logged_in": "✗ Not logged in",
|
||||
"warning.auth.token_unverified": "✓ Token stored (but cannot verify: {message})",
|
||||
"warning.auth.user_unavailable": "✓ Token stored (user info unavailable)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,164 +1,184 @@
|
|||
{
|
||||
"cmd.root.short": "GitLink CLI - GitLink 命令行工具",
|
||||
"cmd.root.long": "用于管理 GitLink 上的仓库、议题、拉取请求、发布、CI 和工作流。",
|
||||
"cmd.api.short": "向 GitLink 发起原始 API 请求",
|
||||
"cmd.api.long": "向 GitLink API 发送任意 HTTP 请求。认证信息会自动注入。",
|
||||
"cmd.auth.short": "认证命令",
|
||||
"cmd.api.short": "向 GitLink 发起原始 API 请求",
|
||||
"cmd.auth.login.short": "登录 GitLink",
|
||||
"cmd.auth.logout.short": "退出 GitLink 登录",
|
||||
"cmd.auth.short": "认证命令",
|
||||
"cmd.auth.status.short": "显示认证状态",
|
||||
"cmd.config.short": "管理 gitlink-cli 配置",
|
||||
"cmd.config.init.short": "初始化配置文件",
|
||||
"cmd.config.set.short": "设置配置项",
|
||||
"cmd.config.get.short": "获取配置项",
|
||||
"cmd.config.list.short": "列出所有配置项",
|
||||
"cmd.version.short": "打印版本信息",
|
||||
"cmd.repo.short": "仓库操作",
|
||||
"cmd.repo.list.short": "列出用户或组织的仓库",
|
||||
"cmd.repo.info.short": "显示仓库详情",
|
||||
"cmd.repo.create.short": "创建新仓库",
|
||||
"cmd.repo.fork.short": "Fork 仓库",
|
||||
"cmd.repo.delete.short": "删除仓库",
|
||||
"cmd.issue.short": "议题操作",
|
||||
"cmd.issue.list.short": "列出议题",
|
||||
"cmd.issue.create.short": "创建新议题",
|
||||
"cmd.issue.view.short": "查看议题详情",
|
||||
"cmd.issue.close.short": "关闭议题",
|
||||
"cmd.issue.update.short": "更新议题",
|
||||
"cmd.issue.comment.short": "给议题添加评论",
|
||||
"cmd.issue.batch_list.short": "列出议题批量维护候选项,不修改远端数据",
|
||||
"cmd.issue.batch_list.long": "列出议题批量维护候选项,不修改远端数据。\n\n示例:\n gitlink-cli issue +batch-list --owner Gitlink --repo gitlink-cli --state open --older-than-days 30 --limit 50 --format table\n gitlink-cli issue +batch-list --owner Gitlink --repo gitlink-cli --label bug --format json",
|
||||
"cmd.issue.batch_close.short": "批量关闭筛选后的议题。默认 dry-run;传入 --yes 后执行。",
|
||||
"cmd.issue.batch_close.long": "批量关闭筛选后的议题。\n\n该命令默认处于 dry-run 模式,只打印匹配的议题。\n传入 --yes 后执行远端关闭操作。请使用严格筛选条件和较小 limit。\n\n示例:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes",
|
||||
"cmd.issue.batch_label.short": "给筛选后的议题批量添加标签。默认 dry-run;传入 --yes 后执行。",
|
||||
"cmd.issue.batch_label.long": "给筛选后的议题批量添加标签。\n\n该命令默认处于 dry-run 模式,只打印匹配的议题。\n传入 --yes 后执行远端标签操作。当前实现不会在 API 端点不可用时伪造写入结果。\n\n示例:\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50 --yes",
|
||||
"cmd.pr.short": "拉取请求操作",
|
||||
"cmd.pr.list.short": "列出拉取请求",
|
||||
"cmd.pr.create.short": "创建拉取请求",
|
||||
"cmd.pr.view.short": "查看拉取请求详情",
|
||||
"cmd.pr.merge.short": "合并拉取请求",
|
||||
"cmd.pr.close.short": "关闭拉取请求",
|
||||
"cmd.pr.files.short": "列出拉取请求中的变更文件",
|
||||
"cmd.pr.diff.short": "显示拉取请求 diff",
|
||||
"cmd.pr.versions.short": "列出拉取请求补丁集版本",
|
||||
"cmd.pr.version_diff.short": "显示拉取请求补丁集版本 diff",
|
||||
"cmd.pr.reviews.short": "列出拉取请求评审",
|
||||
"cmd.pr.review.short": "创建拉取请求评审",
|
||||
"cmd.pr.comment.short": "给拉取请求添加评论",
|
||||
"cmd.release.short": "发布操作",
|
||||
"cmd.release.list.short": "列出发布",
|
||||
"cmd.release.create.short": "创建发布",
|
||||
"cmd.release.view.short": "查看发布详情",
|
||||
"cmd.release.delete.short": "删除发布",
|
||||
"cmd.branch.short": "分支操作",
|
||||
"cmd.branch.list.short": "列出分支",
|
||||
"cmd.branch.create.short": "创建分支",
|
||||
"cmd.branch.delete.short": "删除分支",
|
||||
"cmd.branch.list.short": "列出分支",
|
||||
"cmd.branch.protect.short": "设置分支保护",
|
||||
"cmd.branch.short": "分支操作",
|
||||
"cmd.branch.unprotect.short": "移除分支保护",
|
||||
"cmd.org.short": "组织操作",
|
||||
"cmd.org.list.short": "列出组织",
|
||||
"cmd.org.info.short": "显示组织详情",
|
||||
"cmd.org.members.short": "列出组织成员",
|
||||
"cmd.org.create.short": "创建组织",
|
||||
"cmd.user.short": "用户操作",
|
||||
"cmd.user.me.short": "显示当前认证用户",
|
||||
"cmd.user.info.short": "显示用户资料",
|
||||
"cmd.search.short": "搜索操作",
|
||||
"cmd.search.repos.short": "搜索仓库",
|
||||
"cmd.search.users.short": "搜索用户",
|
||||
"cmd.ci.short": "CI/CD 操作",
|
||||
"cmd.ci.builds.short": "列出 CI 构建",
|
||||
"cmd.ci.logs.short": "查看构建日志",
|
||||
"cmd.ci.restart.short": "重启构建",
|
||||
"cmd.ci.short": "CI/CD 操作",
|
||||
"cmd.ci.stop.short": "停止构建",
|
||||
"cmd.webhook.short": "Webhook 操作",
|
||||
"cmd.webhook.list.short": "列出仓库 Webhook",
|
||||
"cmd.webhook.view.short": "查看 Webhook 详情",
|
||||
"cmd.config.get.short": "获取配置项",
|
||||
"cmd.config.init.short": "初始化配置文件",
|
||||
"cmd.config.list.short": "列出所有配置项",
|
||||
"cmd.config.set.short": "设置配置项",
|
||||
"cmd.config.short": "管理 gitlink-cli 配置",
|
||||
"cmd.issue.batch_close.long": "批量关闭筛选后的议题。\n\n该命令默认处于 dry-run 模式,只打印匹配的议题。\n传入 --yes 后执行远端关闭操作。请使用严格筛选条件和较小 limit。\n\n示例:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes",
|
||||
"cmd.issue.batch_close.short": "批量关闭筛选后的议题。默认 dry-run;传入 --yes 后执行。",
|
||||
"cmd.issue.batch_label.long": "给筛选后的议题批量添加标签。\n\n该命令默认处于 dry-run 模式,只打印匹配的议题。\n传入 --yes 后执行远端标签操作。当前实现不会在 API 端点不可用时伪造写入结果。\n\n示例:\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50\n gitlink-cli issue +batch-label --owner Gitlink --repo gitlink-cli --add-label stale --older-than-days 30 --limit 50 --yes",
|
||||
"cmd.issue.batch_label.short": "给筛选后的议题批量添加标签。默认 dry-run;传入 --yes 后执行。",
|
||||
"cmd.issue.batch_list.long": "列出议题批量维护候选项,不修改远端数据。\n\n示例:\n gitlink-cli issue +batch-list --owner Gitlink --repo gitlink-cli --state open --older-than-days 30 --limit 50 --format table\n gitlink-cli issue +batch-list --owner Gitlink --repo gitlink-cli --label bug --format json",
|
||||
"cmd.issue.batch_list.short": "列出议题批量维护候选项,不修改远端数据",
|
||||
"cmd.issue.close.short": "关闭议题",
|
||||
"cmd.issue.comment.short": "给议题添加评论",
|
||||
"cmd.issue.create.short": "创建新议题",
|
||||
"cmd.issue.list.short": "列出议题",
|
||||
"cmd.issue.short": "议题操作",
|
||||
"cmd.issue.update.short": "更新议题",
|
||||
"cmd.issue.view.short": "查看议题详情",
|
||||
"cmd.org.create.short": "创建组织",
|
||||
"cmd.org.info.short": "显示组织详情",
|
||||
"cmd.org.list.short": "列出组织",
|
||||
"cmd.org.members.short": "列出组织成员",
|
||||
"cmd.org.short": "组织操作",
|
||||
"cmd.pr.close.short": "关闭拉取请求",
|
||||
"cmd.pr.comment.short": "给拉取请求添加评论",
|
||||
"cmd.pr.create.short": "创建拉取请求",
|
||||
"cmd.pr.diff.short": "显示拉取请求 diff",
|
||||
"cmd.pr.files.short": "列出拉取请求中的变更文件",
|
||||
"cmd.pr.list.short": "列出拉取请求",
|
||||
"cmd.pr.merge.short": "合并拉取请求",
|
||||
"cmd.pr.review.short": "创建拉取请求评审",
|
||||
"cmd.pr.reviews.short": "列出拉取请求评审",
|
||||
"cmd.pr.short": "拉取请求操作",
|
||||
"cmd.pr.version_diff.short": "显示拉取请求补丁集版本 diff",
|
||||
"cmd.pr.versions.short": "列出拉取请求补丁集版本",
|
||||
"cmd.pr.view.short": "查看拉取请求详情",
|
||||
"cmd.release.create.short": "创建发布",
|
||||
"cmd.release.delete.short": "删除发布",
|
||||
"cmd.release.list.short": "列出发布",
|
||||
"cmd.release.short": "发布操作",
|
||||
"cmd.release.view.short": "查看发布详情",
|
||||
"cmd.repo.create.short": "创建新仓库",
|
||||
"cmd.repo.delete.short": "删除仓库",
|
||||
"cmd.repo.fork.short": "Fork 仓库",
|
||||
"cmd.repo.info.short": "显示仓库详情",
|
||||
"cmd.repo.list.short": "列出用户或组织的仓库",
|
||||
"cmd.repo.short": "仓库操作",
|
||||
"cmd.root.long": "用于管理 GitLink 上的仓库、议题、拉取请求、发布、CI 和工作流。",
|
||||
"cmd.root.short": "GitLink CLI - GitLink 命令行工具",
|
||||
"cmd.search.repos.short": "搜索仓库",
|
||||
"cmd.search.short": "搜索操作",
|
||||
"cmd.search.users.short": "搜索用户",
|
||||
"cmd.user.info.short": "显示用户资料",
|
||||
"cmd.user.me.short": "显示当前认证用户",
|
||||
"cmd.user.short": "用户操作",
|
||||
"cmd.version.short": "打印版本信息",
|
||||
"cmd.webhook.create.short": "创建仓库 Webhook",
|
||||
"cmd.webhook.update.short": "更新仓库 Webhook,并在可用时保留未指定字段",
|
||||
"cmd.webhook.delete.short": "删除仓库 Webhook",
|
||||
"cmd.webhook.test.short": "触发 Webhook 测试投递",
|
||||
"cmd.webhook.list.short": "列出仓库 Webhook",
|
||||
"cmd.webhook.short": "Webhook 操作",
|
||||
"cmd.webhook.tasks.short": "列出 Webhook 投递任务",
|
||||
"flag.owner": "仓库所有者(自动从 git remote 检测)",
|
||||
"flag.repo": "仓库名称(自动从 git remote 检测)",
|
||||
"flag.format": "输出格式:json、table、yaml(默认:table)",
|
||||
"flag.debug": "启用调试输出",
|
||||
"flag.lang": "显示语言",
|
||||
"cmd.webhook.test.short": "触发 Webhook 测试投递",
|
||||
"cmd.webhook.update.short": "更新仓库 Webhook,并在可用时保留未指定字段",
|
||||
"cmd.webhook.view.short": "查看 Webhook 详情",
|
||||
"error.auth.login_failed": "登录失败:{message}",
|
||||
"error.auth.store_token_failed": "保存 Token 失败:{message}",
|
||||
"error.auth.token_empty": "Token 不能为空",
|
||||
"error.config.save_failed": "保存配置失败:{message}",
|
||||
"error.missing_required_flag": "缺少必需参数 --{name}",
|
||||
"error.unsupported_language": "不支持的语言:{lang}",
|
||||
"flag.api.body": "请求体(JSON 字符串)",
|
||||
"flag.api.query": "查询参数(key=val&key2=val2)",
|
||||
"flag.api.header": "附加请求头(key:value)",
|
||||
"flag.api.query": "查询参数(key=val&key2=val2)",
|
||||
"flag.auth.token": "通过粘贴已有 Token 登录",
|
||||
"flag.user": "用户登录名(默认:当前用户)",
|
||||
"flag.user.login": "用户登录名",
|
||||
"flag.page": "页码",
|
||||
"flag.limit": "每页条目数",
|
||||
"flag.description": "描述",
|
||||
"flag.dry_run": "预览请求,不实际创建",
|
||||
"flag.comment.body": "评论内容",
|
||||
"flag.repo.category": "筛选:manage/mirror/sync/fork/all(默认:manage)",
|
||||
"flag.repo.name": "仓库名称",
|
||||
"flag.repo.description": "仓库描述",
|
||||
"flag.repo.private": "设为私有仓库(true/false)",
|
||||
"flag.issue.state": "按状态筛选:open、closed、all",
|
||||
"flag.issue.title": "议题标题",
|
||||
"flag.issue.body": "议题描述",
|
||||
"flag.issue.assignee": "负责人登录名",
|
||||
"flag.issue.milestone": "里程碑 ID",
|
||||
"flag.issue.label": "标签 ID",
|
||||
"flag.issue.number": "议题编号(网页 URL 中显示的编号)",
|
||||
"flag.issue.new_title": "新标题",
|
||||
"flag.issue.new_body": "新描述",
|
||||
"flag.issue.new_state": "新状态:open、closed 或数字 status_id",
|
||||
"flag.issue.label_filter": "按已有标签筛选",
|
||||
"flag.issue.older_than_days": "只包含至少这么多天未活动的议题",
|
||||
"flag.issue.batch_list.limit": "最多返回的议题数,上限 100",
|
||||
"flag.issue.batch_close.state": "关闭前按议题状态筛选",
|
||||
"flag.issue.batch_close.older_than_days": "必需的安全筛选条件;至少为 7",
|
||||
"flag.issue.batch_process.limit": "最多处理的议题数,上限 100",
|
||||
"flag.issue.batch.yes": "执行远端操作。未传入该参数时仅 dry-run。",
|
||||
"flag.issue.batch.reason": "批量结果中显示的可选原因",
|
||||
"flag.issue.batch_label.state": "按议题状态筛选",
|
||||
"flag.issue.add_label": "要添加到每个匹配议题的标签",
|
||||
"flag.pr.state": "筛选:open、merged、closed",
|
||||
"flag.pr.title": "PR 标题",
|
||||
"flag.pr.body": "PR 描述",
|
||||
"flag.pr.head": "源分支",
|
||||
"flag.pr.base": "目标分支",
|
||||
"flag.pr.id": "PR 编号",
|
||||
"flag.pr.merge_method": "合并方式:merge、rebase、squash",
|
||||
"flag.pr.version_id": "补丁集版本 ID",
|
||||
"flag.pr.file": "按文件路径筛选 diff",
|
||||
"flag.pr.review_status_filter": "按评审状态筛选:common、approved、rejected",
|
||||
"flag.pr.review_status": "评审状态:common、approved、rejected",
|
||||
"flag.pr.review_content": "评审内容",
|
||||
"flag.pr.review_commit": "关联评审的 Commit SHA",
|
||||
"flag.branch.name": "分支名称",
|
||||
"flag.branch.from": "源分支或 Commit",
|
||||
"flag.release.tag": "标签名称",
|
||||
"flag.release.name": "发布名称",
|
||||
"flag.release.body": "发布说明",
|
||||
"flag.release.target": "目标分支",
|
||||
"flag.release.prerelease": "标记为预发布(true/false)",
|
||||
"flag.release.id_or_tag": "发布 ID 或标签",
|
||||
"flag.release.id": "发布 ID",
|
||||
"flag.org.id_or_login": "组织 ID 或登录名",
|
||||
"flag.org.id": "组织 ID",
|
||||
"flag.org.name": "组织名称",
|
||||
"flag.search.keyword": "搜索关键词",
|
||||
"flag.branch.name": "分支名称",
|
||||
"flag.ci.build": "构建编号",
|
||||
"flag.ci.stage": "阶段编号",
|
||||
"flag.ci.step": "步骤编号",
|
||||
"flag.webhook.id": "Webhook ID",
|
||||
"flag.webhook.url": "Webhook 目标 URL",
|
||||
"flag.webhook.events": "逗号分隔的事件,例如:push,issues_only",
|
||||
"flag.webhook.type": "Webhook 类型:gitea/slack/discord/dingtalk/telegram/msteams/feishu/matrix/jianmu/softbot",
|
||||
"flag.comment.body": "评论内容",
|
||||
"flag.debug": "启用调试输出",
|
||||
"flag.description": "描述",
|
||||
"flag.dry_run": "预览请求,不实际创建",
|
||||
"flag.format": "输出格式:json、table、yaml(默认:table)",
|
||||
"flag.issue.add_label": "要添加到每个匹配议题的标签",
|
||||
"flag.issue.assignee": "负责人登录名",
|
||||
"flag.issue.batch.reason": "批量结果中显示的可选原因",
|
||||
"flag.issue.batch.yes": "执行远端操作。未传入该参数时仅 dry-run。",
|
||||
"flag.issue.batch_close.older_than_days": "必需的安全筛选条件;至少为 7",
|
||||
"flag.issue.batch_close.state": "关闭前按议题状态筛选",
|
||||
"flag.issue.batch_label.state": "按议题状态筛选",
|
||||
"flag.issue.batch_list.limit": "最多返回的议题数,上限 100",
|
||||
"flag.issue.batch_process.limit": "最多处理的议题数,上限 100",
|
||||
"flag.issue.body": "议题描述",
|
||||
"flag.issue.label": "标签 ID",
|
||||
"flag.issue.label_filter": "按已有标签筛选",
|
||||
"flag.issue.milestone": "里程碑 ID",
|
||||
"flag.issue.new_body": "新描述",
|
||||
"flag.issue.new_state": "新状态:open、closed 或数字 status_id",
|
||||
"flag.issue.new_title": "新标题",
|
||||
"flag.issue.number": "议题编号(网页 URL 中显示的编号)",
|
||||
"flag.issue.older_than_days": "只包含至少这么多天未活动的议题",
|
||||
"flag.issue.state": "按状态筛选:open、closed、all",
|
||||
"flag.issue.title": "议题标题",
|
||||
"flag.lang": "显示语言",
|
||||
"flag.limit": "每页条目数",
|
||||
"flag.org.id": "组织 ID",
|
||||
"flag.org.id_or_login": "组织 ID 或登录名",
|
||||
"flag.org.name": "组织名称",
|
||||
"flag.owner": "仓库所有者(自动从 git remote 检测)",
|
||||
"flag.page": "页码",
|
||||
"flag.pr.base": "目标分支",
|
||||
"flag.pr.body": "PR 描述",
|
||||
"flag.pr.file": "按文件路径筛选 diff",
|
||||
"flag.pr.head": "源分支",
|
||||
"flag.pr.id": "PR 编号",
|
||||
"flag.pr.merge_method": "合并方式:merge、rebase、squash",
|
||||
"flag.pr.review_commit": "关联评审的 Commit SHA",
|
||||
"flag.pr.review_content": "评审内容",
|
||||
"flag.pr.review_status": "评审状态:common、approved、rejected",
|
||||
"flag.pr.review_status_filter": "按评审状态筛选:common、approved、rejected",
|
||||
"flag.pr.state": "筛选:open、merged、closed",
|
||||
"flag.pr.title": "PR 标题",
|
||||
"flag.pr.version_id": "补丁集版本 ID",
|
||||
"flag.release.body": "发布说明",
|
||||
"flag.release.id": "发布 ID",
|
||||
"flag.release.id_or_tag": "发布 ID 或标签",
|
||||
"flag.release.name": "发布名称",
|
||||
"flag.release.prerelease": "标记为预发布(true/false)",
|
||||
"flag.release.tag": "标签名称",
|
||||
"flag.release.target": "目标分支",
|
||||
"flag.repo": "仓库名称(自动从 git remote 检测)",
|
||||
"flag.repo.category": "筛选:manage/mirror/sync/fork/all(默认:manage)",
|
||||
"flag.repo.description": "仓库描述",
|
||||
"flag.repo.name": "仓库名称",
|
||||
"flag.repo.private": "设为私有仓库(true/false)",
|
||||
"flag.search.keyword": "搜索关键词",
|
||||
"flag.user": "用户登录名(默认:当前用户)",
|
||||
"flag.user.login": "用户登录名",
|
||||
"flag.webhook.active": "Webhook 是否启用:true 或 false",
|
||||
"flag.webhook.branch_filter": "用于 push/create/delete 事件的分支 glob 筛选",
|
||||
"flag.webhook.content_type": "Payload 内容类型:json 或 form",
|
||||
"flag.webhook.events": "逗号分隔的事件,例如:push,issues_only",
|
||||
"flag.webhook.http_method": "HTTP 方法:POST 或 GET",
|
||||
"flag.webhook.id": "Webhook ID",
|
||||
"flag.webhook.secret": "Webhook 密钥",
|
||||
"flag.webhook.secret_update": "Webhook 密钥。如果服务端不返回已有密钥,请再次传入。",
|
||||
"flag.webhook.branch_filter": "用于 push/create/delete 事件的分支 glob 筛选",
|
||||
"flag.webhook.active": "Webhook 是否启用:true 或 false",
|
||||
"error.unsupported_language": "不支持的语言:{lang}",
|
||||
"output.version": "gitlink-cli {version}"
|
||||
"flag.webhook.type": "Webhook 类型:gitea/slack/discord/dingtalk/telegram/msteams/feishu/matrix/jianmu/softbot",
|
||||
"flag.webhook.url": "Webhook 目标 URL",
|
||||
"output.auth.env_hint": " 或设置 {env} 环境变量",
|
||||
"output.auth.login_hint": " 运行:gitlink-cli auth login",
|
||||
"output.config.file": "配置文件:{path}",
|
||||
"output.config.not_set": "(未设置)",
|
||||
"output.version": "gitlink-cli {version}",
|
||||
"prompt.auth.password": "密码:",
|
||||
"prompt.auth.token": "粘贴你的访问 Token:",
|
||||
"prompt.auth.username": "用户名/邮箱/手机号:",
|
||||
"success.auth.logged_in_as": "✓ 已登录为 {login}",
|
||||
"success.auth.logged_in_via_env": "✓ 已通过 {env} 环境变量登录",
|
||||
"success.auth.logged_out": "✓ 已退出登录",
|
||||
"success.auth.token_saved": "✓ Token 已保存",
|
||||
"success.config.initialized": "✓ 配置已初始化:{path}",
|
||||
"warning.auth.not_logged_in": "✗ 未登录",
|
||||
"warning.auth.token_unverified": "✓ Token 已保存(但无法验证:{message})",
|
||||
"warning.auth.user_unavailable": "✓ Token 已保存(用户信息不可用)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,22 +12,51 @@ type ResolveOptions struct {
|
|||
ConfigLang string
|
||||
}
|
||||
|
||||
type ResolvedLocale struct {
|
||||
Locale string
|
||||
Source string
|
||||
Requested string
|
||||
Fallbacked bool
|
||||
Supported bool
|
||||
}
|
||||
|
||||
// ResolveLocale resolves a locale using CLI flag, env, config, system env, fallback.
|
||||
func ResolveLocale(opts ResolveOptions, available []string) string {
|
||||
candidates := []string{
|
||||
opts.ExplicitLang,
|
||||
envValue(opts.Env, "GITLINK_LANG"),
|
||||
opts.ConfigLang,
|
||||
envValue(opts.Env, "LC_ALL"),
|
||||
envValue(opts.Env, "LANG"),
|
||||
return ResolveLocaleDetailed(opts, available).Locale
|
||||
}
|
||||
|
||||
func ResolveLocaleDetailed(opts ResolveOptions, available []string) ResolvedLocale {
|
||||
candidates := []struct {
|
||||
source string
|
||||
value string
|
||||
}{
|
||||
{source: "flag", value: opts.ExplicitLang},
|
||||
{source: "env", value: envValue(opts.Env, "GITLINK_LANG")},
|
||||
{source: "config", value: opts.ConfigLang},
|
||||
{source: "lc_all", value: envValue(opts.Env, "LC_ALL")},
|
||||
{source: "lang", value: envValue(opts.Env, "LANG")},
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if strings.TrimSpace(candidate) == "" {
|
||||
if strings.TrimSpace(candidate.value) == "" {
|
||||
continue
|
||||
}
|
||||
return MatchLocale(candidate, available, defaultFallbackLocale)
|
||||
match := matchLocale(candidate.value, available, defaultFallbackLocale)
|
||||
return ResolvedLocale{
|
||||
Locale: match.Locale,
|
||||
Source: candidate.source,
|
||||
Requested: match.Requested,
|
||||
Fallbacked: match.Fallbacked,
|
||||
Supported: match.Supported,
|
||||
}
|
||||
}
|
||||
match := matchLocale(defaultFallbackLocale, available, defaultFallbackLocale)
|
||||
return ResolvedLocale{
|
||||
Locale: match.Locale,
|
||||
Source: "default",
|
||||
Requested: match.Requested,
|
||||
Fallbacked: match.Fallbacked,
|
||||
Supported: match.Supported,
|
||||
}
|
||||
return MatchLocale(defaultFallbackLocale, available, defaultFallbackLocale)
|
||||
}
|
||||
|
||||
// PreScanLang reads --lang before Cobra constructs localized help text.
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ type Translator struct {
|
|||
fallback map[string]string
|
||||
}
|
||||
|
||||
// Default returns an English translator for call sites that have not been
|
||||
// wired for dependency injection yet.
|
||||
// Default returns an English translator for legacy migration only.
|
||||
// New command code should receive *Translator explicitly.
|
||||
func Default() *Translator {
|
||||
tr, err := New(Options{Locale: defaultFallbackLocale})
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
var keyPattern = regexp.MustCompile(`^(cmd|flag|error|output|prompt|table)\.[a-z0-9_.-]+$`)
|
||||
var keyPattern = regexp.MustCompile(`^(cmd|flag|error|prompt|success|warning|confirm|table|output)\.[a-z0-9_.-]+$`)
|
||||
|
||||
// Problem describes a locale validation issue.
|
||||
type Problem struct {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,16 @@ package common
|
|||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// MountShortcut converts a Shortcut into a cobra.Command and adds it as a subcommand.
|
||||
func MountShortcut(parent *cobra.Command, s *Shortcut) {
|
||||
func MountShortcut(parent *cobra.Command, s *Shortcut, translators ...*i18n.Translator) {
|
||||
tr := i18n.Default()
|
||||
if len(translators) > 0 && translators[0] != nil {
|
||||
tr = translators[0]
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "+" + s.Name,
|
||||
Short: s.Description,
|
||||
|
|
@ -27,10 +32,16 @@ func MountShortcut(parent *cobra.Command, s *Shortcut) {
|
|||
}
|
||||
}
|
||||
|
||||
ctx, err := NewRuntimeContext(flagValues)
|
||||
ctx, err := NewRuntimeContext(flagValues, tr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range s.Flags {
|
||||
if f.Required && flagValues[f.Name] == "" {
|
||||
_, err := ctx.RequireArg(f.Name)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return s.Run(ctx)
|
||||
},
|
||||
|
|
@ -49,17 +60,18 @@ func MountShortcut(parent *cobra.Command, s *Shortcut) {
|
|||
} else {
|
||||
cmd.Flags().String(f.Name, f.Default, f.Usage)
|
||||
}
|
||||
if f.Required {
|
||||
cmd.MarkFlagRequired(f.Name)
|
||||
}
|
||||
}
|
||||
|
||||
parent.AddCommand(cmd)
|
||||
}
|
||||
|
||||
// MountShortcuts mounts multiple shortcuts under a parent command.
|
||||
func MountShortcuts(parent *cobra.Command, shortcuts []*Shortcut) {
|
||||
func MountShortcuts(parent *cobra.Command, shortcuts []*Shortcut, translators ...*i18n.Translator) {
|
||||
var tr *i18n.Translator
|
||||
if len(translators) > 0 {
|
||||
tr = translators[0]
|
||||
}
|
||||
for _, s := range shortcuts {
|
||||
MountShortcut(parent, s)
|
||||
MountShortcut(parent, s, tr)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ package common
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/context"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||||
)
|
||||
|
||||
|
|
@ -37,10 +39,14 @@ type RuntimeContext struct {
|
|||
Repo string
|
||||
Format string
|
||||
Args map[string]string
|
||||
Tr *i18n.Translator
|
||||
}
|
||||
|
||||
// NewRuntimeContext creates a RuntimeContext with auto-resolved owner/repo.
|
||||
func NewRuntimeContext(args map[string]string) (*RuntimeContext, error) {
|
||||
func NewRuntimeContext(args map[string]string, tr *i18n.Translator) (*RuntimeContext, error) {
|
||||
if tr == nil {
|
||||
tr = i18n.Default()
|
||||
}
|
||||
cli, err := client.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -58,6 +64,7 @@ func NewRuntimeContext(args map[string]string) (*RuntimeContext, error) {
|
|||
Repo: cmdutil.Repo,
|
||||
Format: format,
|
||||
Args: args,
|
||||
Tr: tr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +121,7 @@ func (ctx *RuntimeContext) Arg(name string) string {
|
|||
func (ctx *RuntimeContext) RequireArg(name string) (string, error) {
|
||||
v := ctx.Arg(name)
|
||||
if v == "" {
|
||||
return "", fmt.Errorf("required flag --%s is missing", name)
|
||||
return "", errors.New(ctx.Tr.Tf("error.missing_required_flag", i18n.Args{"name": name}))
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func RegisterAll(root *cobra.Command, tr *i18n.Translator) {
|
|||
Use: name,
|
||||
Short: descriptions[name],
|
||||
}
|
||||
common.MountShortcuts(groupCmd, shortcuts)
|
||||
common.MountShortcuts(groupCmd, shortcuts, tr)
|
||||
root.AddCommand(groupCmd)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue