package cobra import ( "embed" "fmt" "os" "github.com/BurntSushi/toml" "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" ) var defaultLanguage = language.English // envVariablesHoldingLocale is sorted by decreasing priority // These environment variables are expected to hold a parsable locale (fr_FR, es, en-US, …) var envVariablesHoldingLocale = []string{ "LANGUAGE", "LC_ALL", "LANG", } // localeFS points to an embedded filesystem of TOML translation files // //go:embed translations/*.toml var localeFS embed.FS // Localizer can be used to fetch localized messages var localizer *i18n.Localizer type i18nCommandGlossary struct { SectionUsage string SectionAliases string SectionExamples string SectionAvailableCommands string SectionAdditionalCommands string SectionFlags string SectionGlobalFlags string SectionAdditionalHelpTopics string Use string ForInfoAboutCommand string } type CommandUsageTemplateData struct { *Command I18n *i18nCommandGlossary } var commonCommandGlossary *i18nCommandGlossary func getCommandGlossary() *i18nCommandGlossary { if commonCommandGlossary == nil { commonCommandGlossary = &i18nCommandGlossary{ SectionUsage: i18nSectionUsage(), SectionAliases: i18nSectionAliases(), SectionExamples: i18nSectionExamples(), SectionAvailableCommands: i18nSectionAvailableCommands(), SectionAdditionalCommands: i18nSectionAdditionalCommands(), SectionFlags: i18nSectionFlags(), SectionGlobalFlags: i18nSectionGlobalFlags(), SectionAdditionalHelpTopics: i18nSectionAdditionalHelpTopics(), Use: i18nUse(), ForInfoAboutCommand: i18nForInfoAboutCommand(), } } return commonCommandGlossary } func i18nSectionUsage() string { return localizeMessage(&i18n.Message{ ID: "SectionUsage", Description: "title of the section in the usage template", Other: "Usage", }) } func i18nSectionAliases() string { return localizeMessage(&i18n.Message{ ID: "SectionAliases", Description: "title of the section in the usage template", Other: "Aliases", }) } func i18nSectionExamples() string { return localizeMessage(&i18n.Message{ ID: "SectionExamples", Description: "title of the section in the usage template", Other: "Examples", }) } func i18nSectionAvailableCommands() string { return localizeMessage(&i18n.Message{ ID: "SectionAvailableCommands", Description: "title of the section in the usage template", Other: "Available Commands", }) } func i18nSectionAdditionalCommands() string { return localizeMessage(&i18n.Message{ ID: "SectionAdditionalCommands", Description: "title of the section in the usage template", Other: "Additional Commands", }) } func i18nSectionFlags() string { return localizeMessage(&i18n.Message{ ID: "SectionFlags", Description: "title of the section in the usage template", Other: "Flags", }) } func i18nSectionGlobalFlags() string { return localizeMessage(&i18n.Message{ ID: "SectionGlobalFlags", Description: "title of the section in the usage template", Other: "Global Flags", }) } func i18nSectionAdditionalHelpTopics() string { return localizeMessage(&i18n.Message{ ID: "SectionAdditionalHelpTopics", Description: "title of the section in the usage template", Other: "Additional Help Topics", }) } func i18nUse() string { return localizeMessage(&i18n.Message{ ID: "Use", Description: "beginning of a sentence like 'Use to do '", Other: "Use", }) } func i18nForInfoAboutCommand() string { return localizeMessage(&i18n.Message{ ID: "ForInfoAboutCommand", Description: "end of a sentence", Other: "for more information about a command", }) } func i18nCommand() string { return localizeMessage(&i18n.Message{ ID: "Command", Description: "lowercase", Other: "command", }) } func i18nPathToCommand() string { return localizeMessage(&i18n.Message{ ID: "PathToCommand", Description: "lowercase", Other: "path to command", }) } func i18nCommandHelpShort() string { return localizeMessage(&i18n.Message{ ID: "CommandHelpShort", Description: "short help for command help", Other: "Help about any command", }) } func i18nCommandHelpLong() string { return localizeMessage(&i18n.Message{ ID: "CommandHelpLong", Description: "long help for command help (cmd example)", Other: `Help provides help for any command in the application. Simply type %s for full details.`, }) } func i18nCommandHelpUnknownTopicError() string { return localizeMessage(&i18n.Message{ ID: "CommandHelpUnknownTopicError", Description: "shown when help topic is unknown (args)", Other: "Unknown help topic %#q", }) } func i18nHelpFor() string { return localizeMessage(&i18n.Message{ ID: "HelpFor", Description: "lowercase, beginning of sentence", Other: "help for", }) } func i18nVersionFor() string { return localizeMessage(&i18n.Message{ ID: "VersionFor", Description: "lowercase, beginning of sentence", Other: "version for", }) } func i18nThisCommand() string { return localizeMessage(&i18n.Message{ ID: "ThisCommand", Description: "lowercase, end of sentence, used when command name is undefined", Other: "this command", }) } func i18nDidYouMeanThis() string { return localizeMessage(&i18n.Message{ ID: "DidYouMeanThis", Description: "shown as suggestion", Other: "Did you mean this?", }) } func i18nCommandDeprecatedWarning() string { return localizeMessage(&i18n.Message{ ID: "CommandDeprecatedWarning", Description: "printed when a deprecated command is executed (cmd, deprecation message)", Other: "Command %q is deprecated, %s", }) } func i18nError() string { return localizeMessage(&i18n.Message{ ID: "Error", Description: "prefix of error messages", Other: "Error", }) } func i18nLegacyArgsValidationError() string { return localizeMessage(&i18n.Message{ ID: "LegacyArgsValidationError", Description: "error shown when args are not understood (subcmd, cmd, suggestion)", Other: "unknown command %q for %q%s", }) } func i18nNoArgsValidationError() string { return localizeMessage(&i18n.Message{ ID: "NoArgsValidationError", Description: "error shown when args are present but should not (subcmd, cmd)", Other: "unknown command %q for %q", }) } func i18nOnlyValidArgsValidationError() string { return localizeMessage(&i18n.Message{ ID: "OnlyValidArgsValidationError", Description: "error shown when arg is invalid (arg, cmd, suggestion)", Other: "invalid argument %q for %q%s", }) } func i18nMinimumNArgsValidationError(amountRequired int) string { return localizeMessageWithPlural(&i18n.Message{ ID: "MinimumNArgsValidationError", Description: "error shown when arg count is too low (expected amount, actual amount)", Other: "requires at least %d args, only received %d", One: "requires at least %d arg, only received %d", }, amountRequired) } func i18nMaximumNArgsValidationError(amountRequired int) string { return localizeMessageWithPlural(&i18n.Message{ ID: "MaximumNArgsValidationError", Description: "error shown when arg count is too low (expected amount, actual amount)", Other: "accepts at most %d args, received %d", One: "accepts at most %d arg, received %d", }, amountRequired) } func i18nExactArgsValidationError(amountRequired int) string { return localizeMessageWithPlural(&i18n.Message{ ID: "ExactArgsValidationError", Description: "error shown when arg count is not exact (expected amount, actual amount)", Other: "accepts %d args, received %d", One: "accepts %d arg, received %d", }, amountRequired) } func i18nRangeArgsValidationError(amountMax int) string { return localizeMessageWithPlural(&i18n.Message{ ID: "RangeArgsValidationError", Description: "error shown when arg count is not in range (expected min, expected max, actual amount)", Other: "accepts between %d and %d args, received %d", One: "accepts between %d and %d arg, received %d", }, amountMax) } func i18nRunHelpTip() string { return localizeMessage(&i18n.Message{ ID: "RunHelpTip", Description: "tip shown when a command fails (command path)", Other: "Run '%v --help' for usage.", }) } func i18nExclusiveFlagsValidationError() string { return localizeMessage(&i18n.Message{ ID: "ExclusiveFlagsValidationError", Description: "error shown when multiple exclusive flags are provided (group flags, offending flags)", Other: "if any flags in the group [%v] are set none of the others can be; %v were all set", }) } func i18nFlagNotSetError(amountFlags int) string { return localizeMessageWithPlural(&i18n.Message{ ID: "FlagNotSetError", Description: "error shown when required flags are not set (flags)", Other: "required flags \"%s\" are not set", One: "required flag \"%s\" is not set", }, amountFlags) } // … lots more translations here func localizeMessage(message *i18n.Message) string { localizedValue, err := localizer.Localize(&i18n.LocalizeConfig{ DefaultMessage: message, }) if err != nil { return message.Other } return localizedValue } func localizeMessageWithPlural(message *i18n.Message, pluralCount int) string { localizedValue, err := localizer.Localize(&i18n.LocalizeConfig{ PluralCount: pluralCount, DefaultMessage: message, }) if err != nil { return message.Other } return localizedValue } func loadTranslationFiles(bundle *i18n.Bundle, langs []string) { for _, lang := range langs { _, _ = bundle.LoadMessageFileFS(localeFS, fmt.Sprintf("translations/active.%s.toml", lang)) } } func detectLangs() []string { var detectedLangs []string for _, envKey := range envVariablesHoldingLocale { lang := os.Getenv(envKey) if lang != "" { detectedLang := language.Make(lang) appendLang(&detectedLangs, detectedLang) } } appendLang(&detectedLangs, defaultLanguage) return detectedLangs } func appendLang(langs *[]string, lang language.Tag) { langString := lang.String() *langs = append(*langs, langString) langBase, confidentInBase := lang.Base() if confidentInBase != language.No { *langs = append(*langs, langBase.String()) *langs = append(*langs, langBase.ISO3()) } } func setupLocalizer() { bundle := i18n.NewBundle(defaultLanguage) bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) detectedLangs := detectLangs() //fmt.Println("Detected languages", detectedLangs) loadTranslationFiles(bundle, detectedLangs) localizer = i18n.NewLocalizer(bundle, detectedLangs...) } func init() { setupLocalizer() // FIXME: perhaps hook this somewhere else? (not init) }