Add short subcommand aliases and EN/RU CLI localization

Subcommands gain one/two-letter aliases (dl, i, e, v, h) alongside
their full names. CLI-owned chrome - top-level usage, per-subcommand
usage lines, progress messages, and the license prompt - now goes
through internal/i18n, an env-driven message catalog (MSVC_GO_WINE_LANG,
falling back to the standard LC_ALL/LC_MESSAGES/LANG locale variables)
with English and Russian translations. Deeper error text from internal
packages stays in English.

Also refreshed the top-level usage text, which hadn't kept up with
--with-wdk/--list-workloads/--list-components/--print-deps-tree.
This commit is contained in:
Cheviiot
2026-07-25 03:31:59 +10:00
parent 8551d7fe99
commit 23ea620ce2
5 changed files with 206 additions and 38 deletions
+15 -13
View File
@@ -9,6 +9,7 @@ import (
"runtime" "runtime"
"github.com/Cheviiot/msvc-go-wine/internal/download" "github.com/Cheviiot/msvc-go-wine/internal/download"
"github.com/Cheviiot/msvc-go-wine/internal/i18n"
) )
func runDownload(args []string) int { func runDownload(args []string) int {
@@ -79,16 +80,16 @@ func runDownload(args []string) int {
if opts.HostArch == "" { if opts.HostArch == "" {
opts.HostArch = detectHostArch() opts.HostArch = detectHostArch()
} }
fmt.Println("Install packages for", opts.HostArch, "host architecture") fmt.Println(i18n.T("download.host_arch", opts.HostArch))
idx := download.BuildIndex(manifest, opts.HostArch, opts.Language) idx := download.BuildIndex(manifest, opts.HostArch, opts.Language)
if *listWorkloads || *listComponents { if *listWorkloads || *listComponents {
if *listWorkloads { if *listWorkloads {
printPackageList("Workload", download.PackagesByType(idx, "Workload"), opts.Language) printPackageList("download.workloads_header", download.PackagesByType(idx, "Workload"), opts.Language)
} }
if *listComponents { if *listComponents {
printPackageList("Component", download.PackagesByType(idx, "Component"), opts.Language) printPackageList("download.components_header", download.PackagesByType(idx, "Component"), opts.Language)
} }
return 0 return 0
} }
@@ -123,8 +124,8 @@ func runDownload(args []string) int {
downloadSize += p.DownloadSize() downloadSize += p.DownloadSize()
installSize += p.InstalledSize() installSize += p.InstalledSize()
} }
fmt.Printf("Selected %d packages, for a total download size of %s, install size of %s\n", fmt.Print(i18n.T("download.selected",
len(selected), download.HumanizeBytes(downloadSize), download.HumanizeBytes(installSize)) len(selected), download.HumanizeBytes(downloadSize), download.HumanizeBytes(installSize)))
cache := *cacheDir cache := *cacheDir
removeCache := false removeCache := false
@@ -148,7 +149,7 @@ func runDownload(args []string) int {
return 1 return 1
} }
*dest = def *dest = def
fmt.Println("--dest not set, using default:", *dest) fmt.Println(i18n.T("download.default_dest", *dest))
} }
if err := download.FetchPayloads(selected, cache, *onlyDownload); err != nil { if err := download.FetchPayloads(selected, cache, *onlyDownload); err != nil {
@@ -207,7 +208,7 @@ func runDownload(args []string) int {
} }
} }
fmt.Println("Done. Next: msvc-go-wine install", destAbs) fmt.Println(i18n.T("download.done", destAbs))
return 0 return 0
} }
@@ -226,7 +227,7 @@ func downloadWDK(opts *download.Options, selected []*download.Package, cache, de
} }
} }
if len(archs) == 0 { if len(archs) == 0 {
fmt.Println("--with-wdk: no x64/arm64 target architecture selected, skipping (no WDK package exists for x86/arm)") fmt.Println(i18n.T("download.wdk_skip"))
return nil return nil
} }
for _, arch := range archs { for _, arch := range archs {
@@ -238,7 +239,7 @@ func downloadWDK(opts *download.Options, selected []*download.Package, cache, de
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("Installed WDK (%s) %s at %s\n", arch, version, wdkDir) fmt.Print(i18n.T("download.wdk_installed", arch, version, wdkDir))
} }
return nil return nil
} }
@@ -254,8 +255,9 @@ func contains(list []string, v string) bool {
// printPackageList prints one line per package: its ID, and (when the // printPackageList prints one line per package: its ID, and (when the
// manifest carries one) its human-readable title in the requested language. // manifest carries one) its human-readable title in the requested language.
func printPackageList(kind string, pkgs []*download.Package, language string) { // headerKey is an i18n catalog key taking the package count as its one arg.
fmt.Printf("Available %ss (%d):\n", kind, len(pkgs)) func printPackageList(headerKey string, pkgs []*download.Package, language string) {
fmt.Print(i18n.T(headerKey, len(pkgs)))
for _, p := range pkgs { for _, p := range pkgs {
if lr := p.Localized(language); lr != nil && lr.Title != "" { if lr := p.Localized(language); lr != nil && lr.Title != "" {
fmt.Printf(" %-65s %s\n", p.ID, lr.Title) fmt.Printf(" %-65s %s\n", p.ID, lr.Title)
@@ -273,7 +275,7 @@ func detectHostArch() string {
} }
func promptAcceptLicense(license string) bool { func promptAcceptLicense(license string) bool {
fmt.Printf("Do you accept the license at %s (yes/no)? ", license) fmt.Print(i18n.T("download.license_prompt", license))
scanner := bufio.NewScanner(os.Stdin) scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() { for scanner.Scan() {
switch scanner.Text() { switch scanner.Text() {
@@ -282,7 +284,7 @@ func promptAcceptLicense(license string) bool {
case "no": case "no":
return false return false
} }
fmt.Print("Do you accept the license? Answer \"yes\" or \"no\": ") fmt.Print(i18n.T("download.license_reprompt"))
} }
return false return false
} }
+3 -2
View File
@@ -6,6 +6,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/Cheviiot/msvc-go-wine/internal/i18n"
"github.com/Cheviiot/msvc-go-wine/internal/wineenv" "github.com/Cheviiot/msvc-go-wine/internal/wineenv"
) )
@@ -20,7 +21,7 @@ func runEnv(args []string) int {
return 2 return 2
} }
if *bin == "" { if *bin == "" {
fmt.Fprintln(os.Stderr, "usage: msvc-go-wine env --bin <dest>/bin/<arch>") fmt.Fprintln(os.Stderr, i18n.T("env.usage"))
return 1 return 1
} }
@@ -38,7 +39,7 @@ func runEnv(args []string) int {
triple, ok := targetTriples[cfg.Arch] triple, ok := targetTriples[cfg.Arch]
if !ok { if !ok {
fmt.Fprintf(os.Stderr, "msvc-go-wine env: unknown arch %q\n", cfg.Arch) fmt.Fprint(os.Stderr, i18n.T("env.unknown_arch", cfg.Arch))
return 1 return 1
} }
+4 -3
View File
@@ -4,12 +4,13 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/Cheviiot/msvc-go-wine/internal/i18n"
"github.com/Cheviiot/msvc-go-wine/internal/install" "github.com/Cheviiot/msvc-go-wine/internal/install"
) )
func runInstall(args []string) int { func runInstall(args []string) int {
if len(args) > 1 || (len(args) == 1 && (args[0] == "-h" || args[0] == "--help")) { if len(args) > 1 || (len(args) == 1 && (args[0] == "-h" || args[0] == "--help")) {
fmt.Fprintln(os.Stderr, "usage: msvc-go-wine install [dest] (default: ~/.msvc-go-wine)") fmt.Fprintln(os.Stderr, i18n.T("install.usage"))
return 1 return 1
} }
@@ -23,7 +24,7 @@ func runInstall(args []string) int {
return 1 return 1
} }
dest = def dest = def
fmt.Println("No directory given, using default:", dest) fmt.Println(i18n.T("install.default_dir", dest))
} }
self, err := os.Executable() self, err := os.Executable()
@@ -36,6 +37,6 @@ func runInstall(args []string) int {
fmt.Fprintln(os.Stderr, "msvc-go-wine install:", err) fmt.Fprintln(os.Stderr, "msvc-go-wine install:", err)
return 1 return 1
} }
fmt.Println("Done. Add", dest+"/bin/<arch> to PATH to use cl, link, lib, ...") fmt.Println(i18n.T("install.done", dest+"/bin/<arch>"))
return 0 return 0
} }
+8 -20
View File
@@ -12,6 +12,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/Cheviiot/msvc-go-wine/internal/i18n"
"github.com/Cheviiot/msvc-go-wine/internal/wrapper" "github.com/Cheviiot/msvc-go-wine/internal/wrapper"
) )
@@ -40,38 +41,25 @@ func runCLI(args []string) int {
} }
switch args[0] { switch args[0] {
case "download": case "download", "dl":
return runDownload(args[1:]) return runDownload(args[1:])
case "install": case "install", "i":
return runInstall(args[1:]) return runInstall(args[1:])
case "env": case "env", "e":
return runEnv(args[1:]) return runEnv(args[1:])
case "version": case "version", "v", "--version":
fmt.Println("msvc-go-wine " + version) fmt.Println("msvc-go-wine " + version)
return 0 return 0
case "-h", "--help", "help": case "-h", "--help", "help", "h":
printUsage() printUsage()
return 0 return 0
default: default:
fmt.Fprintf(os.Stderr, "msvc-go-wine: unknown subcommand %q\n\n", args[0]) fmt.Fprint(os.Stderr, i18n.T("main.unknown_subcommand", args[0]))
printUsage() printUsage()
return 1 return 1
} }
} }
func printUsage() { func printUsage() {
fmt.Fprint(os.Stderr, `msvc-go-wine - cross compile with MSVC on Linux via Wine fmt.Fprint(os.Stderr, i18n.T("main.usage"))
Usage:
msvc-go-wine download --accept-license [--dest <dir>] [options]
fetch and unpack MSVC/WinSDK
msvc-go-wine install [dir] wire up wrappers for a downloaded MSVC
msvc-go-wine env --bin <dir/bin/arch> print INCLUDE/LIB for native clang-cl/lld-link use
msvc-go-wine version print the version
--dest/[dir] default to ~/.msvc-go-wine if omitted.
Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
cl, link, lib, ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd, findstr
`)
} }
+176
View File
@@ -0,0 +1,176 @@
// Package i18n provides minimal message localization for msvc-go-wine's own
// CLI chrome (usage text, progress lines, prompts). It does not localize
// error messages bubbled up from deeper packages - those stay in English,
// matching how most cross-platform dev tools keep diagnostic text technical
// regardless of UI language.
package i18n
import (
"fmt"
"os"
"strings"
)
type Lang string
const (
EN Lang = "en"
RU Lang = "ru"
)
var current = detect()
// detect picks the active language from MSVC_GO_WINE_LANG (checked first, so
// it always overrides the locale), falling back to the POSIX locale
// variables in their usual priority order (LC_ALL, LC_MESSAGES, LANG). Any
// value starting with "ru" (case-insensitive) selects Russian; anything else
// falls back to English.
func detect() Lang {
for _, key := range []string{"MSVC_GO_WINE_LANG", "LC_ALL", "LC_MESSAGES", "LANG"} {
v := os.Getenv(key)
if v == "" {
continue
}
if strings.HasPrefix(strings.ToLower(v), "ru") {
return RU
}
return EN
}
return EN
}
// Current returns the active language, for callers that need to branch
// beyond a simple T() lookup.
func Current() Lang { return current }
// T looks up key in the message catalog and formats it (via fmt.Sprintf)
// with args, if any. Keys with no translation for the current language fall
// back to English; keys missing from the catalog entirely are returned
// as-is, so a missing translation degrades to a visible-but-harmless string
// rather than a panic.
func T(key string, args ...any) string {
msg := key
if entry, ok := catalog[key]; ok {
if m, ok := entry[current]; ok {
msg = m
} else {
msg = entry[EN]
}
}
if len(args) == 0 {
return msg
}
return fmt.Sprintf(msg, args...)
}
var catalog = map[string]map[Lang]string{
"main.usage": {
EN: `msvc-go-wine - cross compile with MSVC on Linux via Wine
Usage:
msvc-go-wine download (dl) --accept-license [--dest <dir>] [options]
fetch and unpack MSVC/WinSDK/WDK
msvc-go-wine install (i) [dir] wire up wrappers for a downloaded MSVC
msvc-go-wine env (e) --bin <dir/bin/arch> print INCLUDE/LIB for native clang-cl/lld-link use
msvc-go-wine version (v) print the version
msvc-go-wine help (h) show this message
Run "msvc-go-wine <command> --help" for that command's own options - download
has many, including --with-wdk, --list-workloads, --list-components and
--print-deps-tree.
--dest/[dir] default to ~/.msvc-go-wine if omitted.
Language: set MSVC_GO_WINE_LANG=ru (or LANG=ru_RU...) for Russian output.
Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
cl, link, lib, ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd, findstr
`,
RU: `msvc-go-wine — кросс-компиляция настоящим MSVC на Linux через Wine
Использование:
msvc-go-wine download (dl) --accept-license [--dest <каталог>] [опции]
скачать и распаковать MSVC/WinSDK/WDK
msvc-go-wine install (i) [каталог] настроить обёртки для скачанного MSVC
msvc-go-wine env (e) --bin <dir/bin/arch> вывести INCLUDE/LIB для clang-cl/lld-link напрямую
msvc-go-wine version (v) показать версию
msvc-go-wine help (h) показать эту справку
Запустите «msvc-go-wine <команда> --help» для параметров конкретной команды —
у download их много, включая --with-wdk, --list-workloads, --list-components
и --print-deps-tree.
--dest/[каталог] по умолчанию — ~/.msvc-go-wine.
Язык: установите MSVC_GO_WINE_LANG=en (или LANG=en_US...) для вывода на английском.
После установки добавьте <dir>/bin/<arch> в PATH и вызывайте инструменты напрямую:
cl, link, lib, ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd, findstr
`,
},
"main.unknown_subcommand": {
EN: "msvc-go-wine: unknown subcommand %q\n\n",
RU: "msvc-go-wine: неизвестная подкоманда %q\n\n",
},
"install.usage": {
EN: "usage: msvc-go-wine install (i) [dest] (default: ~/.msvc-go-wine)",
RU: "использование: msvc-go-wine install (i) [каталог] (по умолчанию: ~/.msvc-go-wine)",
},
"install.default_dir": {
EN: "No directory given, using default: %s",
RU: "Каталог не указан, используется значение по умолчанию: %s",
},
"install.done": {
EN: "Done. Add %s to PATH to use cl, link, lib, ...",
RU: "Готово. Добавьте %s в PATH, чтобы использовать cl, link, lib и т.д.",
},
"env.usage": {
EN: "usage: msvc-go-wine env (e) --bin <dest>/bin/<arch>",
RU: "использование: msvc-go-wine env (e) --bin <dest>/bin/<arch>",
},
"env.unknown_arch": {
EN: "msvc-go-wine env: unknown arch %q\n",
RU: "msvc-go-wine env: неизвестная архитектура %q\n",
},
"download.host_arch": {
EN: "Install packages for %s host architecture",
RU: "Установка пакетов для архитектуры хоста %s",
},
"download.selected": {
EN: "Selected %d packages, for a total download size of %s, install size of %s\n",
RU: "Выбрано пакетов: %d, общий размер загрузки %s, размер после установки %s\n",
},
"download.default_dest": {
EN: "--dest not set, using default: %s",
RU: "--dest не указан, используется значение по умолчанию: %s",
},
"download.done": {
EN: "Done. Next: msvc-go-wine install %s",
RU: "Готово. Далее: msvc-go-wine install %s",
},
"download.wdk_skip": {
EN: "--with-wdk: no x64/arm64 target architecture selected, skipping (no WDK package exists for x86/arm)",
RU: "--with-wdk: не выбрана целевая архитектура x64/arm64, пропускаем (для x86/arm пакета WDK не существует)",
},
"download.wdk_installed": {
EN: "Installed WDK (%s) %s at %s\n",
RU: "WDK (%s) %s установлен в %s\n",
},
"download.workloads_header": {
EN: "Available Workloads (%d):\n",
RU: "Доступные рабочие нагрузки (Workload) (%d):\n",
},
"download.components_header": {
EN: "Available Components (%d):\n",
RU: "Доступные компоненты (Component) (%d):\n",
},
"download.license_prompt": {
EN: "Do you accept the license at %s (yes/no)? ",
RU: "Вы принимаете лицензию по адресу %s (yes/no)? ",
},
"download.license_reprompt": {
EN: "Do you accept the license? Answer \"yes\" or \"no\": ",
RU: "Вы принимаете лицензию? Ответьте «yes» или «no»: ",
},
}