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

@ -1,6 +1,7 @@
package recipe
import (
"errors"
"fmt"
"io/ioutil"
"os"
@ -8,6 +9,7 @@ import (
"strings"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack"
@ -24,7 +26,7 @@ func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) {
if err := ensurePathExists(r.ComposePath); err != nil {
return []string{}, err
}
log.Debugf("no COMPOSE_FILE detected, loading default: %s", r.ComposePath)
log.Debug(i18n.G("no COMPOSE_FILE detected, loading default: %s", r.ComposePath))
return []string{r.ComposePath}, nil
}
@ -33,7 +35,7 @@ func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) {
if err := ensurePathExists(path); err != nil {
return []string{}, err
}
log.Debugf("COMPOSE_FILE detected, loading %s", path)
log.Debug(i18n.G("COMPOSE_FILE detected, loading %s", path))
return []string{path}, nil
}
@ -42,7 +44,7 @@ func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) {
numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1
envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles)
if len(envVars) != numComposeFiles {
return composeFiles, fmt.Errorf("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar)
return composeFiles, errors.New(i18n.G("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar))
}
for _, file := range envVars {
@ -53,8 +55,8 @@ func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) {
composeFiles = append(composeFiles, path)
}
log.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", "))
log.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), r.Name)
log.Debug(i18n.G("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", ")))
log.Debug(i18n.G("retrieved %s configs for %s", strings.Join(composeFiles, ", "), r.Name))
return composeFiles, nil
}
@ -67,7 +69,7 @@ func (r Recipe) GetComposeConfig(env map[string]string) (*composetypes.Config, e
}
if len(composeFiles) == 0 {
return nil, fmt.Errorf("%s is missing a compose.yml or compose.*.yml file?", r.Name)
return nil, errors.New(i18n.G("%s is missing a compose.yml or compose.*.yml file?", r.Name))
}
if env == nil {
@ -102,7 +104,7 @@ func (r Recipe) GetVersionLabelLocal() (string, error) {
}
if label == "" {
return label, fmt.Errorf("%s has no version label? try running \"abra recipe sync %s\" first?", r.Name, r.Name)
return label, errors.New(i18n.G("%s has no version label? try running \"abra recipe sync %s\" first?", r.Name, r.Name))
}
return label, nil
@ -118,7 +120,7 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
return false, err
}
log.Debugf("considering %s config(s) for tag update", strings.Join(composeFiles, ", "))
log.Debug(i18n.G("considering %s config(s) for tag update", strings.Join(composeFiles, ", ")))
for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}}
@ -148,13 +150,13 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
case reference.NamedTagged:
composeTag = img.(reference.NamedTagged).Tag()
default:
log.Debugf("unable to parse %s, skipping", img)
log.Debug(i18n.G("unable to parse %s, skipping", img))
continue
}
composeImage := formatter.StripTagMeta(reference.Path(img))
log.Debugf("parsed %s from %s", composeTag, service.Image)
log.Debug(i18n.G("parsed %s from %s", composeTag, service.Image))
if image == composeImage {
bytes, err := ioutil.ReadFile(composeFile)
@ -166,7 +168,7 @@ func (r Recipe) UpdateTag(image, tag string) (bool, error) {
new := fmt.Sprintf("%s:%s", composeImage, tag)
replacedBytes := strings.Replace(string(bytes), old, new, -1)
log.Debugf("updating %s to %s in %s", old, new, compose.Filename)
log.Debug(i18n.G("updating %s to %s in %s", old, new, compose.Filename))
if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
return false, err
@ -186,7 +188,7 @@ func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
return err
}
log.Debugf("considering %s config(s) for label update", strings.Join(composeFiles, ", "))
log.Debug(i18n.G("considering %s config(s) for label update", strings.Join(composeFiles, ", ")))
for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}}
@ -224,27 +226,27 @@ func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
return err
}
old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value)
old := i18n.G("coop-cloud.${STACK_NAME}.version=%s", value)
replacedBytes := strings.Replace(string(bytes), old, label, -1)
if old == label {
log.Warnf("%s is already set, nothing to do?", label)
log.Warnf(i18n.G("%s is already set, nothing to do?", label))
return nil
}
log.Debugf("updating %s to %s in %s", old, label, compose.Filename)
log.Debug(i18n.G("updating %s to %s in %s", old, label, compose.Filename))
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
return err
}
log.Infof("synced label %s to service %s", label, serviceName)
log.Infof(i18n.G("synced label %s to service %s", label, serviceName))
}
}
if !discovered {
log.Warn("no existing label found, automagic insertion not supported yet")
log.Fatalf("add '- \"%s\"' manually to the 'app' service in %s", label, composeFile)
log.Warn(i18n.G("no existing label found, automagic insertion not supported yet"))
log.Fatal(i18n.G("add '- \"%s\"' manually to the 'app' service in %s", label, composeFile))
}
}

View File

@ -1,18 +1,20 @@
package recipe
import (
"errors"
"fmt"
"os"
"path"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
)
func (r Recipe) SampleEnv() (map[string]string, error) {
sampleEnv, err := envfile.ReadEnv(r.SampleEnvPath)
if err != nil {
return sampleEnv, fmt.Errorf("unable to discover .env.sample for %s", r.Name)
return sampleEnv, errors.New(i18n.G("unable to discover .env.sample for %s", r.Name))
}
return sampleEnv, nil
}
@ -31,7 +33,7 @@ func (r Recipe) GetReleaseNotes(version string) (string, error) {
return "", err
}
title := formatter.BoldStyle.Render(fmt.Sprintf("%s release notes:", version))
title := formatter.BoldStyle.Render(i18n.G("%s release notes:", version))
withTitle := fmt.Sprintf("%s\n%s\n", title, releaseNotes)
return withTitle, nil

View File

@ -1,6 +1,7 @@
package recipe
import (
"errors"
"fmt"
"os"
"slices"
@ -10,6 +11,7 @@ import (
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp"
"github.com/distribution/reference"
@ -45,9 +47,9 @@ func (r Recipe) Ensure(ctx EnsureContext) error {
}
if r.EnvVersion != "" && !ctx.IgnoreEnvVersion {
log.Debugf("ensuring env version %s", r.EnvVersion)
log.Debug(i18n.G("ensuring env version %s", r.EnvVersion))
if strings.Contains(r.EnvVersion, "+U") {
return fmt.Errorf("can not redeploy chaos version (%s) without --chaos", r.EnvVersion)
return errors.New(i18n.G("can not redeploy chaos version (%s) without --chaos", r.EnvVersion))
}
if _, err := r.EnsureVersion(r.EnvVersion); err != nil {
@ -146,16 +148,16 @@ func (r Recipe) EnsureVersion(version string) (bool, error) {
joinedTags := strings.Join(parsedTags, ", ")
if joinedTags != "" {
log.Debugf("read %s as tags for recipe %s", joinedTags, r.Name)
log.Debug(i18n.G("read %s as tags for recipe %s", joinedTags, r.Name))
}
var opts *git.CheckoutOptions
if tagRef.String() == "" {
log.Debugf("attempting to checkout '%s' as chaos commit", version)
log.Debug(i18n.G("attempting to checkout '%s' as chaos commit", version))
hash, err := repo.ResolveRevision(plumbing.Revision(version))
if err != nil {
log.Fatalf("unable to resolve '%s': %s", version, err)
log.Fatal(i18n.G("unable to resolve '%s': %s", version, err))
}
opts = &git.CheckoutOptions{Hash: *hash, Create: false, Force: true}
@ -173,7 +175,7 @@ func (r Recipe) EnsureVersion(version string) (bool, error) {
return isChaosCommit, nil
}
log.Debugf("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), r.Dir)
log.Debug(i18n.G("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), r.Dir))
return isChaosCommit, nil
}
@ -182,11 +184,11 @@ func (r Recipe) EnsureVersion(version string) (bool, error) {
func (r Recipe) EnsureIsClean() error {
isClean, err := gitPkg.IsClean(r.Dir)
if err != nil {
return fmt.Errorf("unable to check git clean status in %s: %s", r.Dir, err)
return errors.New(i18n.G("unable to check git clean status in %s: %s", r.Dir, err))
}
if !isClean {
return fmt.Errorf("%s (%s) has locally unstaged changes?", r.Name, r.Dir)
return errors.New(i18n.G("%s (%s) has locally unstaged changes?", r.Name, r.Dir))
}
return nil
@ -220,7 +222,7 @@ func (r Recipe) EnsureLatest() error {
}
if err := worktree.Checkout(checkOutOpts); err != nil {
log.Debugf("failed to check out %s in %s", branch, r.Dir)
log.Debug(i18n.G("failed to check out %s in %s", branch, r.Dir))
return err
}
@ -231,33 +233,33 @@ func (r Recipe) EnsureLatest() error {
func (r Recipe) EnsureUpToDate() error {
repo, err := git.PlainOpen(r.Dir)
if err != nil {
return fmt.Errorf("unable to open %s: %s", r.Dir, err)
return errors.New(i18n.G("unable to open %s: %s", r.Dir, err))
}
remotes, err := repo.Remotes()
if err != nil {
return fmt.Errorf("unable to read remotes in %s: %s", r.Dir, err)
return errors.New(i18n.G("unable to read remotes in %s: %s", r.Dir, err))
}
if len(remotes) == 0 {
log.Debugf("cannot ensure %s is up-to-date, no git remotes configured", r.Name)
log.Debug(i18n.G("cannot ensure %s is up-to-date, no git remotes configured", r.Name))
return nil
}
worktree, err := repo.Worktree()
if err != nil {
return fmt.Errorf("unable to open git work tree in %s: %s", r.Dir, err)
return errors.New(i18n.G("unable to open git work tree in %s: %s", r.Dir, err))
}
branch, err := gitPkg.CheckoutDefaultBranch(repo, r.Dir)
if err != nil {
return fmt.Errorf("unable to check out default branch in %s: %s", r.Dir, err)
return errors.New(i18n.G("unable to check out default branch in %s: %s", r.Dir, err))
}
fetchOpts := &git.FetchOptions{Tags: git.AllTags}
if err := repo.Fetch(fetchOpts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return fmt.Errorf("unable to fetch tags in %s: %s", r.Dir, err)
return errors.New(i18n.G("unable to fetch tags in %s: %s", r.Dir, err))
}
}
@ -269,11 +271,11 @@ func (r Recipe) EnsureUpToDate() error {
if err := worktree.Pull(opts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return fmt.Errorf("unable to git pull in %s: %s", r.Dir, err)
return errors.New(i18n.G("unable to git pull in %s: %s", r.Dir, err))
}
}
log.Debugf("fetched latest git changes for %s", r.Name)
log.Debug(i18n.G("fetched latest git changes for %s", r.Name))
return nil
}
@ -362,7 +364,7 @@ func (r Recipe) Tags() ([]string, error) {
return version1.IsLessThan(version2)
})
log.Debugf("detected %s as tags for recipe %s", strings.Join(tags, ", "), r.Name)
log.Debug(i18n.G("detected %s as tags for recipe %s", strings.Join(tags, ", "), r.Name))
return tags, nil
}
@ -373,7 +375,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
versions := RecipeVersions{}
log.Debugf("git: opening repository in %s", r.Dir)
log.Debug(i18n.G("git: opening repository in %s", r.Dir))
repo, err := git.PlainOpen(r.Dir)
if err != nil {
@ -393,7 +395,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/")
log.Debugf("processing %s for %s", tag, r.Name)
log.Debug(i18n.G("processing %s for %s", tag, r.Name))
checkOutOpts := &git.CheckoutOptions{
Create: false,
@ -401,11 +403,11 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
Branch: plumbing.ReferenceName(ref.Name()),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
log.Debugf("failed to check out %s in %s", tag, r.Dir)
log.Debug(i18n.G("failed to check out %s in %s", tag, r.Dir))
return err
}
log.Debugf("git checkout: %s in %s", ref.Name(), r.Dir)
log.Debug(i18n.G("git checkout: %s in %s", ref.Name(), r.Dir))
config, err := r.GetComposeConfig(nil)
if err != nil {
@ -429,7 +431,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag()
case reference.Named:
warnMsg = append(warnMsg, fmt.Sprintf("%s service is missing image tag?", path))
warnMsg = append(warnMsg, i18n.G("%s service is missing image tag?", path))
continue
}
@ -453,7 +455,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
sortRecipeVersions(versions)
log.Debugf("collected %s for %s", versions, r.Dir)
log.Debug(i18n.G("collected %s for %s", versions, r.Dir))
var uniqueWarnings []string
for _, w := range warnMsg {

View File

@ -12,6 +12,7 @@ import (
"strconv"
"strings"
"coopcloud.tech/abra/pkg/i18n"
"github.com/go-git/go-git/v5"
"coopcloud.tech/abra/pkg/catalogue"
@ -70,7 +71,7 @@ func (r RecipeMeta) LatestVersion() string {
version = tag
}
log.Debugf("choosing %s as latest version of %s", version, r.Name)
log.Debug(i18n.G("choosing %s as latest version of %s", version, r.Name))
return version
}
@ -126,7 +127,7 @@ func Get(name string) Recipe {
if strings.Contains(name, ":") {
split := strings.Split(name, ":")
if len(split) > 2 {
log.Fatalf("version seems invalid: %s", name)
log.Fatal(i18n.G("version seems invalid: %s", name))
}
name = split[0]
@ -134,7 +135,7 @@ func Get(name string) Recipe {
versionRaw = version
if strings.HasSuffix(version, config.DIRTY_DEFAULT) {
version = strings.Replace(split[1], config.DIRTY_DEFAULT, "", 1)
log.Debugf("removed dirty suffix from .env version: %s -> %s", split[1], version)
log.Debug(i18n.G("removed dirty suffix from .env version: %s -> %s", split[1], version))
}
}
@ -143,7 +144,7 @@ func Get(name string) Recipe {
if strings.Contains(name, "/") {
u, err := url.Parse(name)
if err != nil {
log.Fatalf("invalid recipe: %s", err)
log.Fatal(i18n.G("invalid recipe: %s", err))
}
u.Scheme = "https"
gitURL = u.String() + ".git"
@ -171,7 +172,7 @@ func Get(name string) Recipe {
dirty, err := r.IsDirty()
if err != nil && !errors.Is(err, git.ErrRepositoryNotExists) {
log.Fatalf("failed to check git status of %s: %s", r.Name, err)
log.Fatal(i18n.G("failed to check git status of %s: %s", r.Name, err))
}
r.Dirty = dirty
@ -195,16 +196,16 @@ type Recipe struct {
// String outputs a human-friendly string representation.
func (r Recipe) String() string {
out := fmt.Sprintf("{name: %s, ", r.Name)
out += fmt.Sprintf("version : %s, ", r.EnvVersion)
out += fmt.Sprintf("dirty: %v, ", r.Dirty)
out += fmt.Sprintf("dir: %s, ", r.Dir)
out += fmt.Sprintf("git url: %s, ", r.GitURL)
out += fmt.Sprintf("ssh url: %s, ", r.SSHURL)
out += fmt.Sprintf("compose: %s, ", r.ComposePath)
out += fmt.Sprintf("readme: %s, ", r.ReadmePath)
out += fmt.Sprintf("sample env: %s, ", r.SampleEnvPath)
out += fmt.Sprintf("abra.sh: %s}", r.AbraShPath)
out := i18n.G("{name: %s, ", r.Name)
out += i18n.G("version : %s, ", r.EnvVersion)
out += i18n.G("dirty: %v, ", r.Dirty)
out += i18n.G("dir: %s, ", r.Dir)
out += i18n.G("git url: %s, ", r.GitURL)
out += i18n.G("ssh url: %s, ", r.SSHURL)
out += i18n.G("compose: %s, ", r.ComposePath)
out += i18n.G("readme: %s, ", r.ReadmePath)
out += i18n.G("sample env: %s, ", r.SampleEnvPath)
out += i18n.G("abra.sh: %s}", r.AbraShPath)
return out
}
@ -233,7 +234,7 @@ func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, []string, error)
feat = Features{}
)
log.Debugf("%s: attempt recipe metadata parse", r.ReadmePath)
log.Debug(i18n.G("%s: attempt recipe metadata parse", r.ReadmePath))
readmeFS, err := ioutil.ReadFile(r.ReadmePath)
if err != nil {
@ -321,12 +322,12 @@ func GetImageMetadata(imageRowString, recipeName string) (Image, []string, error
if imageRowString != "" {
warnMsgs = append(
warnMsgs,
fmt.Sprintf("%s: image meta has incorrect format: %s", recipeName, imageRowString),
i18n.G("%s: image meta has incorrect format: %s", recipeName, imageRowString),
)
} else {
warnMsgs = append(
warnMsgs,
fmt.Sprintf("%s: image meta is empty?", recipeName),
i18n.G("%s: image meta is empty?", recipeName),
)
}
@ -357,14 +358,14 @@ func GetImageMetadata(imageRowString, recipeName string) (Image, []string, error
func GetStringInBetween(recipeName, str, start, end string) (result string, err error) {
s := strings.Index(str, start)
if s == -1 {
return "", fmt.Errorf("%s: marker string %s not found", recipeName, start)
return "", errors.New(i18n.G("%s: marker string %s not found", recipeName, start))
}
s += len(start)
e := strings.Index(str[s:], end)
if e == -1 {
return "", fmt.Errorf("%s: end marker %s not found", recipeName, end)
return "", errors.New(i18n.G("%s: end marker %s not found", recipeName, end))
}
return str[s : s+e], nil
@ -402,7 +403,7 @@ func readRecipeCatalogueFS(target interface{}) error {
return err
}
log.Debugf("read recipe catalogue from file system cache in %s", config.RECIPES_JSON)
log.Debug(i18n.G("read recipe catalogue from file system cache in %s", config.RECIPES_JSON))
return nil
}
@ -431,7 +432,7 @@ func VersionsOfService(recipe, serviceName string, offline bool) ([]string, erro
}
}
log.Debugf("detected versions %s for %s", strings.Join(versions, ", "), recipe)
log.Debug(i18n.G("detected versions %s for %s", strings.Join(versions, ", "), recipe))
return versions, nil
}
@ -454,11 +455,11 @@ func GetRecipeMeta(recipeName string, offline bool) (RecipeMeta, error) {
recipeMeta, ok := catl[recipeName]
if !ok {
return RecipeMeta{}, RecipeMissingFromCatalogue{
err: fmt.Sprintf("recipe %s does not exist?", recipeName),
err: i18n.G("recipe %s does not exist?", recipeName),
}
}
log.Debugf("recipe metadata retrieved for %s", recipeName)
log.Debug(i18n.G("recipe metadata retrieved for %s", recipeName))
return recipeMeta, nil
}
@ -545,13 +546,13 @@ func ReadReposMetadata(debug bool) (RepoCatalogue, error) {
reposMeta := make(RepoCatalogue)
pageIdx := 1
bar := formatter.CreateProgressbar(-1, "collecting recipe listing")
bar := formatter.CreateProgressbar(-1, i18n.G("collecting recipe listing"))
for {
var reposList []RepoMeta
pagedURL := fmt.Sprintf("%s?page=%v", ReposMetadataURL, pageIdx)
log.Debugf("fetching repo metadata from %s", pagedURL)
log.Debug(i18n.G("fetching repo metadata from %s", pagedURL))
if err := web.ReadJSON(pagedURL, &reposList); err != nil {
return reposMeta, err
@ -655,7 +656,7 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string, debug bool) erro
cloneLimiter := limit.New(3)
retrieveBar := formatter.CreateProgressbar(barLength, "retrieving recipes")
retrieveBar := formatter.CreateProgressbar(barLength, i18n.G("retrieving recipes"))
ch := make(chan string, barLength)
for _, repoMeta := range repos {
go func(rm RepoMeta) {