Skip to content
Merged
5 changes: 1 addition & 4 deletions cmd/cmd_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ var (
user string
secret string
operationDirectory string
threads uint32
insecure bool
proxy string
verbose int
Expand All @@ -31,7 +30,7 @@ func NewRootCmd() *cobra.Command {
rootCmd := &cobra.Command{
Use: "smartling-cli",
Short: "Manage translation files using Smartling CLI.",
Version: "3.1",
Version: "3.2",
Long: `Manage translation files using Smartling CLI.
Complete documentation is available at https://www.smartling.com`,
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
Expand Down Expand Up @@ -62,8 +61,6 @@ This option overrides config value "user_id".`)
This option overrides config value "secret".`)
rootCmd.PersistentFlags().StringVar(&operationDirectory, "operation-directory", ".", `Sets directory to operate on, usually, to store or to
read files. Depends on command.`)
rootCmd.PersistentFlags().Uint32Var(&threads, "threads", 4, `If command can be executed concurrently, it will be
executed for at most <number> of threads.`)
rootCmd.PersistentFlags().BoolVarP(&insecure, "insecure", "k", false, "Skip HTTPS certificate validation.")
rootCmd.PersistentFlags().StringVar(&proxy, "proxy", "", "Use specified URL as proxy server.")
rootCmd.PersistentFlags().StringVar(&smartlingURL, "smartling-url", "", `Specify base Smartling URL, merely for testing
Expand Down
1 change: 0 additions & 1 deletion cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ func Config() (config.Config, error) {
Secret: secret,
Account: account,
Project: project,
Threads: threads,
IsInit: isInit,
IsFiles: isFiles,
IsProjects: isProjects,
Expand Down
97 changes: 81 additions & 16 deletions cmd/files/pull/cmd_pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package pull

import (
"os"
"strconv"

rootcmd "github.com/Smartling/smartling-cli/cmd"
filescmd "github.com/Smartling/smartling-cli/cmd/files"
"github.com/Smartling/smartling-cli/cmd/helpers/resolve"
"github.com/Smartling/smartling-cli/services/files"
"github.com/Smartling/smartling-cli/services/helpers/format"
"github.com/Smartling/smartling-cli/services/helpers/help"
Expand All @@ -12,15 +15,21 @@ import (
"github.com/spf13/cobra"
)

const threadsFlag = "threads"

var (
uri string
all bool
source bool
progress string
retrieve string
directory string
formatPath string
locales []string
uri string
jobIDOrName string
all bool
source bool
resume bool
dryRun bool
progress string
retrieve string
directory string
formatPath string
locales []string
threads uint32
)

// NewPullCmd creates a new command to pull files.
Expand Down Expand Up @@ -60,6 +69,7 @@ Following variables are available:

> .FileURI — full file URI in Smartling system;
> .Locale — locale ID for translated file and empty for source file;
> .JobUID — translation job UID, when --job is set (otherwise empty);


Available options:
Expand Down Expand Up @@ -89,6 +99,23 @@ Available options:
> pseudo — returns modified version of original text with certain
characters transformed;
> contextMatchingInstrumented — to use with Chrome Context Capture;

--job <job UID or job name>
Download every file × target-locale pair from a Smartling job.
Combines with the <uri> positional argument: when both are set, the
URI is treated as a glob filter applied to the job's file list.
Combines with --locale: the requested locales are intersected with the job's target locales.
When --format is not set, defaults to <jobUid>/<locale>/<fileUri>.

--resume
Skip files that already exist on disk. Useful for re-running a large
pull after a failure.

--dry-run
Print the file × locale matrix that would be downloaded, then exit 0.
Does not call GetFileStatus, so --progress filtering is not applied.
Without --job, locale list comes from --locale flags only; omitting --locale
produces no output.
` + help.AuthenticationOptions,
Example: `
# Pull translated files
Expand All @@ -102,6 +129,18 @@ Available options:
# Download all translated files

smartling-cli files download --all

# Pull every file × target locale in a translation job

smartling-cli --threads 20 files pull --job <job UID or name>

# Pull only .txt files from a job (URI glob filters the job file list)

smartling-cli files pull "**.txt" --job <job UID or name>

# Preview what a job pull would download

smartling-cli files pull --job <job UID or name> --dry-run
`,
Run: func(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
Expand All @@ -115,15 +154,36 @@ Available options:
os.Exit(1)
}

var threadsCfg *string
config, cnfErr := rootcmd.Config()
if cnfErr == nil && config.Threads > 0 {
s := strconv.FormatUint(uint64(config.Threads), 10)
threadsCfg = new(s)
}
threadsParam := resolve.FallbackString(cmd.Flags().Lookup(threadsFlag), resolve.StringParam{
FlagName: threadsFlag,
Config: threadsCfg,
})
threadsParamI, err := strconv.ParseUint(threadsParam, 10, 32)
if err != nil {
rlog.Errorf("failed to parse `threads` parameter: %s", err)
os.Exit(1)
}

params := files.PullParams{
URI: uri,
All: all,
Format: formatPath,
Directory: directory,
Source: source,
Locales: locales,
Progress: progress,
Retrieve: retrieve,
URI: uri,
JobUIDOrName: jobIDOrName,
ProjectUID: config.ProjectID,
All: all,
Format: formatPath,
Directory: directory,
Source: source,
Locales: locales,
Progress: progress,
Retrieve: retrieve,
Resume: resume,
DryRun: dryRun,
Threads: uint32(threadsParamI),
}
err = s.RunPull(ctx, params)
if err != nil {
Expand All @@ -134,11 +194,16 @@ Available options:
}

pullCmd.Flags().BoolVar(&all, "all", false, `Download all files. Required if no file pattern is specified.`)
pullCmd.Flags().StringVar(&jobIDOrName, "job", "", "Filter downloads to files belonging to the specified job UID or job name")
pullCmd.Flags().BoolVar(&source, "source", false, `Pulls source file as well.`)
pullCmd.Flags().StringVar(&progress, "progress", "", `Pulls only translations that are at least specified percent of work complete.`)
pullCmd.Flags().StringVar(&retrieve, "retrieve", "", `Retrieval type: pending, published, pseudo or contextMatchingInstrumented.`)
pullCmd.Flags().StringVarP(&directory, "directory", "d", ".", `Download all files to specified directory.`)
pullCmd.Flags().StringArrayVarP(&locales, "locale", "l", []string{}, `Authorize only specified locales.`)
pullCmd.Flags().BoolVar(&resume, "resume", false, `Resume a previously interrupted pull operation, skipping already downloaded files.`)
pullCmd.Flags().BoolVar(&dryRun, "dry-run", false, `Print the file × locale matrix that would be downloaded, then exit.`)
pullCmd.Flags().Uint32Var(&threads, threadsFlag, 20, `If command can be executed concurrently, it will be
executed for at most <number> of threads.`)
pullCmd.Flags().StringVar(&formatPath, "format", "", `Can be used to format path to downloaded files.
Note, that single file can be translated in
different locales, so format should include locale
Expand Down
10 changes: 10 additions & 0 deletions cmd/files/pull/cmd_pull_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,19 @@ import (
cmdmocks "github.com/Smartling/smartling-cli/cmd/files/mocks"
"github.com/Smartling/smartling-cli/services/files"
srvmocks "github.com/Smartling/smartling-cli/services/files/mocks"
"github.com/Smartling/smartling-cli/services/helpers/rlog"

"github.com/stretchr/testify/mock"
)

func TestMain(m *testing.M) {
// Production main() calls rlog.Init(); the Run handler in NewPullCmd
// reaches rootcmd.Config() which calls rlog.Debugf — without Init the
// global logger is nil and the test panics.
rlog.Init()
m.Run()
}

func TestNewPullCmd(t *testing.T) {
buf := new(bytes.Buffer)
filesSrv := srvmocks.NewMockService(t)
Expand All @@ -24,6 +33,7 @@ func TestNewPullCmd(t *testing.T) {
Locales: []string{"en-US", "fr-FR"},
Progress: "20%",
Retrieve: "none",
Threads: 20,
}
filesSrv.On("RunPull", mock.Anything, params).Run(func(args mock.Arguments) {
if _, err := fmt.Fprintf(buf, "RunPull was called with %d args\n", len(args)); err != nil {
Expand Down
110 changes: 110 additions & 0 deletions cmd/helpers/resolve/fallbacks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package resolve

import (
"testing"

"github.com/spf13/pflag"
)

// newStringFlag returns a *pflag.Flag for a string flag with the given default
// value. If userValue is non-empty, the flag is treated as explicitly set
// (Changed=true) with that value.
func newStringFlag(t *testing.T, name, defaultValue, userValue string) *pflag.Flag {
t.Helper()
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
fs.String(name, defaultValue, "")
if userValue != "" {
if err := fs.Set(name, userValue); err != nil {
t.Fatalf("flag.Set: %v", err)
}
}
return fs.Lookup(name)
}

func strPtr(s string) *string { return &s }

func TestFallbackString(t *testing.T) {
tests := []struct {
name string
flag *pflag.Flag
param StringParam
envVar string // when non-empty, sets SMARTLING_CLI_<UPPER(FLAG)>=envVar for the test
want string
}{
{
name: "user-set flag wins over env, config, and default",
flag: newStringFlag(t, "threads", "20", "5"),
param: StringParam{FlagName: "threads", Config: strPtr("99")},
// SMARTLING_CLI_THREADS would be set, but flag.Changed=true short-circuits first.
envVar: "42",
want: "5",
},
{
name: "env wins over config and default when flag not changed",
flag: newStringFlag(t, "threads", "20", ""),
param: StringParam{FlagName: "threads", Config: strPtr("99")},
envVar: "42",
want: "42",
},
{
name: "config wins over default when flag not changed and env unset",
flag: newStringFlag(t, "threads", "20", ""),
param: StringParam{FlagName: "threads", Config: strPtr("99")},
want: "99",
},
{
name: "flag default used when flag not changed, env unset, config nil",
flag: newStringFlag(t, "threads", "20", ""),
param: StringParam{FlagName: "threads"},
want: "20",
},
{
name: "config used when flag is nil",
flag: nil,
param: StringParam{FlagName: "threads", Config: strPtr("99")},
want: "99",
},
{
name: "empty string when flag is nil and config is nil",
flag: nil,
param: StringParam{FlagName: "threads"},
want: "",
},
{
name: "env wins when flag is nil and env is set",
flag: nil,
param: StringParam{FlagName: "threads"},
envVar: "42",
want: "42",
},
{
name: "config pointing to empty string still wins over default",
flag: newStringFlag(t, "threads", "20", ""),
param: StringParam{FlagName: "threads", Config: strPtr("")},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.envVar != "" {
t.Setenv("SMARTLING_CLI_"+upper(tt.param.FlagName), tt.envVar)
}
if got := FallbackString(tt.flag, tt.param); got != tt.want {
t.Errorf("FallbackString() = %q, want %q", got, tt.want)
}
})
}
}

// upper is an ASCII-only uppercaser to keep this test file dependency-light.
func upper(s string) string {
out := make([]byte, len(s))
for i := 0; i < len(s); i++ {
c := s[i]
if c >= 'a' && c <= 'z' {
c -= 'a' - 'A'
}
out[i] = c
}
return string(out)
}
3 changes: 2 additions & 1 deletion cmd/jobs/progress/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"

jobapi "github.com/Smartling/api-sdk-go/api/job"
jobscmd "github.com/Smartling/smartling-cli/cmd/jobs"
"github.com/Smartling/smartling-cli/output"
"github.com/Smartling/smartling-cli/output/jobs"
Expand All @@ -30,7 +31,7 @@ func run(ctx context.Context,

progressOutput, err := jobSrv.RunProgress(ctx, params)
if err != nil {
if errors.Is(err, srv.ErrJobNotFound) {
if errors.Is(err, jobapi.ErrNotFound) {
return clierror.UIError{
Operation: "find job",
Err: err,
Expand Down
6 changes: 3 additions & 3 deletions docs/smartling-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ smartling-cli [flags]
--proxy string Use specified URL as proxy server.
--secret string Token Secret which will be used for authentication.
This option overrides config value "secret".
--show-config Print the resolved account, project, user, and config file path
to stderr before the command runs.
--smartling-url string Specify base Smartling URL, merely for testing
purposes.
--threads uint32 If command can be executed concurrently, it will be
executed for at most <number> of threads. (default 4)
--user string User ID which will be used for authentication.
This option overrides config value "user_id".
-v, --verbose count Verbose logging
Expand All @@ -47,4 +47,4 @@ smartling-cli [flags]
* [smartling-cli mt](smartling-cli_mt.md) - File Machine Translations
* [smartling-cli projects](smartling-cli_projects.md) - Used to access various projects sub-commands.

###### Auto generated by spf13/cobra on 23-Jan-2026
###### Auto generated by spf13/cobra on 19-May-2026
6 changes: 3 additions & 3 deletions docs/smartling-cli_completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ See each sub-command's help for details on how to use the generated script.
--proxy string Use specified URL as proxy server.
--secret string Token Secret which will be used for authentication.
This option overrides config value "secret".
--show-config Print the resolved account, project, user, and config file path
to stderr before the command runs.
--smartling-url string Specify base Smartling URL, merely for testing
purposes.
--threads uint32 If command can be executed concurrently, it will be
executed for at most <number> of threads. (default 4)
--user string User ID which will be used for authentication.
This option overrides config value "user_id".
-v, --verbose count Verbose logging
Expand All @@ -48,4 +48,4 @@ See each sub-command's help for details on how to use the generated script.
* [smartling-cli completion powershell](smartling-cli_completion_powershell.md) - Generate the autocompletion script for powershell
* [smartling-cli completion zsh](smartling-cli_completion_zsh.md) - Generate the autocompletion script for zsh

###### Auto generated by spf13/cobra on 23-Jan-2026
###### Auto generated by spf13/cobra on 19-May-2026
Loading