feat: translation support
All checks were successful
continuous-integration/drone/push Build is passing

See #483
This commit is contained in:
2025-08-19 11:22:52 +02:00
parent 5cf6048ecb
commit 4e205cf13e
108 changed files with 11217 additions and 1645 deletions

View File

@ -2,11 +2,12 @@ package internal
import (
"context"
"fmt"
"errors"
"io"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/service"
"coopcloud.tech/abra/pkg/upstream/container"
@ -22,10 +23,10 @@ func RetrieveBackupBotContainer(cl *dockerClient.Client) (types.Container, error
ctx := context.Background()
chosenService, err := service.GetServiceByLabel(ctx, cl, config.BackupbotLabel, NoInput)
if err != nil {
return types.Container{}, fmt.Errorf("no backupbot discovered, is it deployed?")
return types.Container{}, errors.New(i18n.G("no backupbot discovered, is it deployed?"))
}
log.Debugf("retrieved %s as backup enabled service", chosenService.Spec.Name)
log.Debug(i18n.G("retrieved %s as backup enabled service", chosenService.Spec.Name))
filters := filters.NewArgs()
filters.Add("name", chosenService.Spec.Name)
@ -58,7 +59,7 @@ func RunBackupCmdRemote(
Tty: true,
}
log.Debugf("running backup %s on %s with exec config %v", backupCmd, containerID, execBackupListOpts)
log.Debug(i18n.G("running backup %s on %s with exec config %v", backupCmd, containerID, execBackupListOpts))
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()

View File

@ -3,6 +3,7 @@ package internal
import (
"bufio"
"context"
"errors"
"fmt"
"io/ioutil"
"os/exec"
@ -11,6 +12,7 @@ import (
appPkg "coopcloud.tech/abra/pkg/app"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
@ -34,7 +36,7 @@ func RunCmdRemote(
return err
}
log.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server)
log.Debug(i18n.G("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server))
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(abraSh, toTarOpts)
@ -65,7 +67,7 @@ func RunCmdRemote(
}
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
log.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name)
log.Info(i18n.G("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name))
shell = "/bin/sh"
}
@ -76,10 +78,10 @@ func RunCmdRemote(
cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.Name, app.StackName(), cmdName)}
}
log.Debugf("running command: %s", strings.Join(cmd, " "))
log.Debug(i18n.G("running command: %s", strings.Join(cmd, " ")))
if remoteUser != "" {
log.Debugf("running command with user %s", remoteUser)
log.Debug(i18n.G("running command with user %s", remoteUser))
execCreateOpts.User = remoteUser
}
@ -88,7 +90,7 @@ func RunCmdRemote(
execCreateOpts.Tty = true
if disableTTY {
execCreateOpts.Tty = false
log.Debugf("not requesting a remote TTY")
log.Debug(i18n.G("not requesting a remote TTY"))
}
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
@ -105,7 +107,7 @@ func EnsureCommand(abraSh, recipeName, execCmd string) error {
}
if !strings.Contains(string(bytes), execCmd) {
return fmt.Errorf("%s doesn't have a %s function", recipeName, execCmd)
return errors.New(i18n.G("%s doesn't have a %s function", recipeName, execCmd))
}
return nil

View File

@ -1,6 +1,7 @@
package internal
import (
"errors"
"fmt"
"os"
"sort"
@ -9,6 +10,7 @@ import (
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
@ -70,18 +72,18 @@ func DeployOverview(
}
rows := [][]string{
{"DOMAIN", domain},
{"RECIPE", app.Recipe.Name},
{"SERVER", server},
{"CONFIG", deployConfig},
{i18n.G("DOMAIN"), domain},
{i18n.G("RECIPE"), app.Recipe.Name},
{i18n.G("SERVER"), server},
{i18n.G("CONFIG"), deployConfig},
{"", ""},
{"CURRENT DEPLOYMENT", formatter.BoldDirtyDefault(deployedVersion)},
{"ENV VERSION", formatter.BoldDirtyDefault(envVersion)},
{"NEW DEPLOYMENT", formatter.BoldDirtyDefault(toDeployVersion)},
{i18n.G("CURRENT DEPLOYMENT"), formatter.BoldDirtyDefault(deployedVersion)},
{i18n.G("ENV VERSION"), formatter.BoldDirtyDefault(envVersion)},
{i18n.G("NEW DEPLOYMENT"), formatter.BoldDirtyDefault(toDeployVersion)},
}
deployType := getDeployType(deployedVersion, toDeployVersion)
overview := formatter.CreateOverview(fmt.Sprintf("%s OVERVIEW", deployType), rows)
overview := formatter.CreateOverview(i18n.G("%s OVERVIEW", deployType), rows)
fmt.Println(overview)
@ -104,7 +106,7 @@ func DeployOverview(
}
if !response {
log.Fatal("deployment cancelled")
log.Fatal(i18n.G("deployment cancelled"))
}
return nil
@ -112,32 +114,32 @@ func DeployOverview(
func getDeployType(currentVersion, newVersion string) string {
if newVersion == config.NO_DOMAIN_DEFAULT {
return "UNDEPLOY"
return i18n.G("UNDEPLOY")
}
if strings.Contains(newVersion, "+U") {
return "CHAOS DEPLOY"
return i18n.G("CHAOS DEPLOY")
}
if strings.Contains(currentVersion, "+U") {
return "UNCHAOS DEPLOY"
return i18n.G("UNCHAOS DEPLOY")
}
if currentVersion == newVersion {
return "REDEPLOY"
return ("REDEPLOY")
}
if currentVersion == config.NO_VERSION_DEFAULT {
return "NEW DEPLOY"
return i18n.G("NEW DEPLOY")
}
currentParsed, err := tagcmp.Parse(currentVersion)
if err != nil {
return "DEPLOY"
return i18n.G("DEPLOY")
}
newParsed, err := tagcmp.Parse(newVersion)
if err != nil {
return "DEPLOY"
return i18n.G("DEPLOY")
}
if currentParsed.IsLessThan(newParsed) {
return "UPGRADE"
return i18n.G("UPGRADE")
}
return "DOWNGRADE"
return i18n.G("DOWNGRADE")
}
// PostCmds parses a string of commands and executes them inside of the respective services
@ -146,7 +148,7 @@ func getDeployType(currentVersion, newVersion string) string {
func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name)
return errors.New(i18n.G("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name))
}
return err
}
@ -154,7 +156,7 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
for _, command := range strings.Split(commands, "|") {
commandParts := strings.Split(command, " ")
if len(commandParts) < 2 {
return fmt.Errorf("not enough arguments: %s", command)
return errors.New(i18n.G("not enough arguments: %s", command))
}
targetServiceName := commandParts[0]
cmdName := commandParts[1]
@ -162,7 +164,7 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
if len(commandParts) > 2 {
parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " "))
}
log.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName)
log.Info(i18n.G("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName))
if err := EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
return err
@ -184,7 +186,7 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
return fmt.Errorf("no service %s for %s?", targetServiceName, app.Name)
}
log.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName)
log.Debug(i18n.G("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName))
requestTTY := true
if err := RunCmdRemote(

View File

@ -1,9 +1,11 @@
package internal
import (
"errors"
"fmt"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2"
@ -13,7 +15,7 @@ import (
// PromptBumpType prompts for version bump type
func PromptBumpType(tagString, latestRelease string) error {
if (!Major && !Minor && !Patch) && tagString == "" {
fmt.Printf(`
fmt.Print(i18n.G(`
You need to make a decision about what kind of an update this new recipe
version is. If someone else performs this upgrade, do they have to do some
migration work or take care of some breaking changes? This can be signaled in
@ -36,12 +38,12 @@ Here is a semver cheat sheet (more on https://semver.org):
should also Just Work and is mostly to do with minor bug fixes
and/or security patches. "nothing to worry about".
`, latestRelease)
`, latestRelease))
var chosenBumpType string
prompt := &survey.Select{
Message: fmt.Sprintf("select recipe version increment type"),
Options: []string{"major", "minor", "patch"},
Options: []string{i18n.G("major"), i18n.G("minor"), i18n.G("patch")},
}
if err := survey.AskOne(prompt, &chosenBumpType); err != nil {
@ -59,13 +61,13 @@ func GetBumpType() string {
var bumpType string
if Major {
bumpType = "major"
bumpType = i18n.G("major")
} else if Minor {
bumpType = "minor"
bumpType = i18n.G("minor")
} else if Patch {
bumpType = "patch"
bumpType = i18n.G("patch")
} else {
log.Fatal("no version bump type specififed?")
log.Fatal(i18n.G("no version bump type specififed?"))
}
return bumpType
@ -73,14 +75,14 @@ func GetBumpType() string {
// SetBumpType figures out which bump type is specified
func SetBumpType(bumpType string) {
if bumpType == "major" {
if bumpType == i18n.G("major") {
Major = true
} else if bumpType == "minor" {
} else if bumpType == i18n.G("minor") {
Minor = true
} else if bumpType == "patch" {
} else if bumpType == i18n.G("patch") {
Patch = true
} else {
log.Fatal("no version bump type specififed?")
log.Fatal(i18n.G("no version bump type specififed?"))
}
}
@ -107,7 +109,7 @@ func GetMainAppImage(recipe recipe.Recipe) (string, error) {
}
if path == "" {
return path, fmt.Errorf("%s has no main 'app' service?", recipe.Name)
return path, errors.New(i18n.G("%s has no main 'app' service?", recipe.Name))
}
return path, nil

View File

@ -5,6 +5,7 @@ import (
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2"
@ -31,7 +32,7 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
localRecipes, err := recipe.GetRecipesLocal()
if err != nil {
log.Debugf("can't read local recipes: %s", err)
log.Debug(i18n.G("can't read local recipes: %s", err))
} else {
for _, recipeLocal := range localRecipes {
if _, ok := knownRecipes[recipeLocal]; !ok {
@ -46,7 +47,7 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
if recipeName == "" && !NoInput {
prompt := &survey.Select{
Message: "Select recipe",
Message: i18n.G("Select recipe"),
Options: recipes,
}
if err := survey.AskOne(prompt, &recipeName); err != nil {
@ -55,12 +56,12 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
}
if recipeName == "" {
log.Fatal("no recipe name provided")
log.Fatal(i18n.G("no recipe name provided"))
}
if _, ok := knownRecipes[recipeName]; !ok {
if !strings.Contains(recipeName, "/") {
log.Fatalf("no recipe '%s' exists?", recipeName)
log.Fatal(i18n.G("no recipe '%s' exists?", recipeName))
}
}
@ -71,20 +72,20 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
_, err = chosenRecipe.GetComposeConfig(nil)
if err != nil {
if cmdName == "generate" {
if cmdName == i18n.G("generate") {
if strings.Contains(err.Error(), "missing a compose") {
log.Fatal(err)
}
log.Warn(err)
} else {
if strings.Contains(err.Error(), "template_driver is not allowed") {
log.Warnf("ensure %s recipe compose.* files include \"version: '3.8'\"", recipeName)
log.Warn(i18n.G("ensure %s recipe compose.* files include \"version: '3.8'\"", recipeName))
}
log.Fatalf("unable to validate recipe: %s", err)
log.Fatal(i18n.G("unable to validate recipe: %s", err))
}
}
log.Debugf("validated %s as recipe argument", recipeName)
log.Debug(i18n.G("validated %s as recipe argument", recipeName))
return chosenRecipe
}
@ -92,7 +93,7 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
// ValidateApp ensures the app name arg is valid.
func ValidateApp(args []string) app.App {
if len(args) == 0 {
log.Fatal("no app provided")
log.Fatal(i18n.G("no app provided"))
}
appName := args[0]
@ -102,7 +103,7 @@ func ValidateApp(args []string) app.App {
log.Fatal(err)
}
log.Debugf("validated %s as app argument", appName)
log.Debug(i18n.G("validated %s as app argument", appName))
return app
}
@ -116,8 +117,8 @@ func ValidateDomain(args []string) string {
if domainName == "" && !NoInput {
prompt := &survey.Input{
Message: "Specify a domain name",
Default: "example.com",
Message: i18n.G("Specify a domain name"),
Default: "1312.net",
}
if err := survey.AskOne(prompt, &domainName); err != nil {
log.Fatal(err)
@ -125,10 +126,10 @@ func ValidateDomain(args []string) string {
}
if domainName == "" {
log.Fatal("no domain provided")
log.Fatal(i18n.G("no domain provided"))
}
log.Debugf("validated %s as domain argument", domainName)
log.Debug(i18n.G("validated %s as domain argument", domainName))
return domainName
}
@ -147,7 +148,7 @@ func ValidateServer(args []string) string {
if serverName == "" && !NoInput {
prompt := &survey.Select{
Message: "Specify a server name",
Message: i18n.G("Specify a server name"),
Options: serverNames,
}
if err := survey.AskOne(prompt, &serverName); err != nil {
@ -163,14 +164,14 @@ func ValidateServer(args []string) string {
}
if serverName == "" {
log.Fatal("no server provided")
log.Fatal(i18n.G("no server provided"))
}
if !matched {
log.Fatal("server doesn't exist?")
log.Fatal(i18n.G("server doesn't exist?"))
}
log.Debugf("validated %s as server argument", serverName)
log.Debug(i18n.G("validated %s as server argument", serverName))
return serverName
}