From ef97f8a2c3f19950a967601263edb79c7bba9437 Mon Sep 17 00:00:00 2001 From: az-smartling Date: Thu, 4 Jun 2026 22:36:49 +0200 Subject: [PATCH 1/6] RPS-10166 Job strings --- .mockery.yml | 5 + Makefile | 1 + cmd/jobs/strings/add/cmd_add.go | 59 ++++ cmd/jobs/strings/add/resolve.go | 29 ++ cmd/jobs/strings/add/resolve_test.go | 56 ++++ cmd/jobs/strings/add/run.go | 47 ++++ cmd/jobs/strings/cmd_job_strings.go | 33 +++ cmd/jobs/strings/list/cmd_list.go | 59 ++++ cmd/jobs/strings/list/resolve.go | 29 ++ cmd/jobs/strings/list/resolve_test.go | 53 ++++ cmd/jobs/strings/list/run.go | 47 ++++ cmd/jobs/strings/remove/cmd_remove.go | 56 ++++ cmd/jobs/strings/remove/resolve.go | 28 ++ cmd/jobs/strings/remove/resolve_test.go | 53 ++++ cmd/jobs/strings/remove/run.go | 47 ++++ cmd/jobs/strings/srv_initializer.go | 33 +++ main.go | 10 + services/jobs/locales/sdkmocks/locale.go | 32 +-- services/jobs/strings/run_add.go | 44 +++ services/jobs/strings/run_add_test.go | 112 ++++++++ services/jobs/strings/run_list.go | 96 +++++++ services/jobs/strings/run_list_test.go | 72 +++++ services/jobs/strings/run_remove.go | 42 +++ services/jobs/strings/run_remove_test.go | 81 ++++++ services/jobs/strings/sdkmocks/jobstring.go | 273 +++++++++++++++++++ services/jobs/strings/service.go | 107 ++++++++ tests/cmd/jobs/strings/add/add_test.go | 72 +++++ tests/cmd/jobs/strings/list/list_test.go | 65 +++++ tests/cmd/jobs/strings/remove/remove_test.go | 65 +++++ 29 files changed, 1690 insertions(+), 16 deletions(-) create mode 100644 cmd/jobs/strings/add/cmd_add.go create mode 100644 cmd/jobs/strings/add/resolve.go create mode 100644 cmd/jobs/strings/add/resolve_test.go create mode 100644 cmd/jobs/strings/add/run.go create mode 100644 cmd/jobs/strings/cmd_job_strings.go create mode 100644 cmd/jobs/strings/list/cmd_list.go create mode 100644 cmd/jobs/strings/list/resolve.go create mode 100644 cmd/jobs/strings/list/resolve_test.go create mode 100644 cmd/jobs/strings/list/run.go create mode 100644 cmd/jobs/strings/remove/cmd_remove.go create mode 100644 cmd/jobs/strings/remove/resolve.go create mode 100644 cmd/jobs/strings/remove/resolve_test.go create mode 100644 cmd/jobs/strings/remove/run.go create mode 100644 cmd/jobs/strings/srv_initializer.go create mode 100644 services/jobs/strings/run_add.go create mode 100644 services/jobs/strings/run_add_test.go create mode 100644 services/jobs/strings/run_list.go create mode 100644 services/jobs/strings/run_list_test.go create mode 100644 services/jobs/strings/run_remove.go create mode 100644 services/jobs/strings/run_remove_test.go create mode 100644 services/jobs/strings/sdkmocks/jobstring.go create mode 100644 services/jobs/strings/service.go create mode 100644 tests/cmd/jobs/strings/add/add_test.go create mode 100644 tests/cmd/jobs/strings/list/list_test.go create mode 100644 tests/cmd/jobs/strings/remove/remove_test.go diff --git a/.mockery.yml b/.mockery.yml index 1c68c5b..3aca2bd 100644 --- a/.mockery.yml +++ b/.mockery.yml @@ -40,6 +40,11 @@ packages: JobLocale: config: dir: services/jobs/locales/sdkmocks + github.com/Smartling/api-sdk-go/api/job/string: + interfaces: + JobString: + config: + dir: services/jobs/strings/sdkmocks github.com/Smartling/api-sdk-go/api/glossary: interfaces: Glossary: diff --git a/Makefile b/Makefile index 2bf7ad5..8266ae3 100644 --- a/Makefile +++ b/Makefile @@ -65,3 +65,4 @@ test_integration: go test ./tests/cmd/mt/detect/... go test ./tests/cmd/mt/translate/... go test ./tests/cmd/jobs/locales/... + go test ./tests/cmd/jobs/strings/... diff --git a/cmd/jobs/strings/add/cmd_add.go b/cmd/jobs/strings/add/cmd_add.go new file mode 100644 index 0000000..5ea02da --- /dev/null +++ b/cmd/jobs/strings/add/cmd_add.go @@ -0,0 +1,59 @@ +package add + +import ( + "fmt" + + stringscmd "github.com/Smartling/smartling-cli/cmd/jobs/strings" + "github.com/Smartling/smartling-cli/output" + + "github.com/spf13/cobra" +) + +const ( + hashcodeFlag = "hashcode" + targetLocaleFlag = "target-locale" + moveEnabledFlag = "move-enabled" +) + +// NewJobStringsAddCmd returns new command to job string add +func NewJobStringsAddCmd(initializer stringscmd.SrvInitializer) *cobra.Command { + var ( + hashcodes []string + targetLocales []string + moveEnabled bool + ) + addCmd := &cobra.Command{ + Use: "add ", + Short: "Add strings to a translation job.", + Long: `Assign strings (by hashcode) to an existing translation job, identified by UID or name.`, + Args: cobra.ExactArgs(1), + Example: ` +# Add two strings to a job + + smartling-cli jobs strings add aabbccdd1122 --hashcode h1 --hashcode h2 + +# Add a string for specific locales, moving it if it already belongs to another job + + smartling-cli jobs strings add "Website Q1 2026" --hashcode h1 --target-locale fr-FR --move-enabled +`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + params, err := resolveParams(args[0], hashcodes, targetLocales, moveEnabled) + if err != nil { + return fmt.Errorf("failed to resolve add params: %w", err) + } + format, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + return run(ctx, initializer, params, output.Params{Format: format}) + }, + } + + addCmd.Flags().StringArrayVar(&hashcodes, hashcodeFlag, nil, "String hashcode to add (repeatable, required).") + addCmd.Flags().StringArrayVar(&targetLocales, targetLocaleFlag, nil, "Target locale to add the strings to (repeatable; default all job locales).") + addCmd.Flags().BoolVar(&moveEnabled, moveEnabledFlag, false, "Move the string into this job if it already belongs to another job for a locale.") + + return addCmd +} diff --git a/cmd/jobs/strings/add/resolve.go b/cmd/jobs/strings/add/resolve.go new file mode 100644 index 0000000..6a528ef --- /dev/null +++ b/cmd/jobs/strings/add/resolve.go @@ -0,0 +1,29 @@ +package add + +import ( + rootcmd "github.com/Smartling/smartling-cli/cmd" + clierror "github.com/Smartling/smartling-cli/services/helpers/cli_error" + "github.com/Smartling/smartling-cli/services/helpers/rlog" + srv "github.com/Smartling/smartling-cli/services/jobs/strings" +) + +func resolveParams(jobUIDOrName string, hashcodes, targetLocales []string, moveEnabled bool) (srv.AddParams, error) { + rlog.Debugf("resolving add params") + + cnf, err := rootcmd.Config() + if err != nil { + return srv.AddParams{}, clierror.UIError{ + Operation: "config", + Err: err, + Description: "failed to read config", + } + } + + return srv.AddParams{ + ProjectID: cnf.ProjectID, + JobUIDOrName: jobUIDOrName, + Hashcodes: hashcodes, + TargetLocaleIDs: targetLocales, + MoveEnabled: moveEnabled, + }, nil +} diff --git a/cmd/jobs/strings/add/resolve_test.go b/cmd/jobs/strings/add/resolve_test.go new file mode 100644 index 0000000..27389ce --- /dev/null +++ b/cmd/jobs/strings/add/resolve_test.go @@ -0,0 +1,56 @@ +package add + +import ( + "os" + "path/filepath" + "testing" + + rootcmd "github.com/Smartling/smartling-cli/cmd" +) + +func TestMain(m *testing.M) { + rootcmd.ConfigureLogger() + os.Exit(m.Run()) +} + +func writeConfig(t *testing.T, content string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "smartling.yml") + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatalf("write config: %v", err) + } + return path +} + +func Test_resolveParams(t *testing.T) { + // Config() always requires these. + t.Setenv("SMARTLING_USER_ID", "test-user") + t.Setenv("SMARTLING_SECRET", "test-secret") + + root := rootcmd.NewRootCmd() + cfgPath := writeConfig(t, "project_id: config-project-id\n") + if err := root.PersistentFlags().Set("config", cfgPath); err != nil { + t.Fatalf("set config flag: %v", err) + } + t.Cleanup(func() { _ = root.PersistentFlags().Set("config", "") }) + + params, err := resolveParams("aabbccdd1122", []string{"h1", "h2"}, []string{"fr-FR"}, true) + if err != nil { + t.Fatalf("resolveParams() error = %v", err) + } + if params.ProjectID != "config-project-id" { + t.Errorf("ProjectID = %q, want config-project-id", params.ProjectID) + } + if params.JobUIDOrName != "aabbccdd1122" { + t.Errorf("JobUIDOrName = %q, want aabbccdd1122", params.JobUIDOrName) + } + if len(params.Hashcodes) != 2 || params.Hashcodes[0] != "h1" { + t.Errorf("Hashcodes = %v, want [h1 h2]", params.Hashcodes) + } + if len(params.TargetLocaleIDs) != 1 || params.TargetLocaleIDs[0] != "fr-FR" { + t.Errorf("TargetLocaleIDs = %v, want [fr-FR]", params.TargetLocaleIDs) + } + if !params.MoveEnabled { + t.Error("MoveEnabled = false, want true") + } +} diff --git a/cmd/jobs/strings/add/run.go b/cmd/jobs/strings/add/run.go new file mode 100644 index 0000000..d19051f --- /dev/null +++ b/cmd/jobs/strings/add/run.go @@ -0,0 +1,47 @@ +package add + +import ( + "context" + "errors" + "fmt" + + stringscmd "github.com/Smartling/smartling-cli/cmd/jobs/strings" + "github.com/Smartling/smartling-cli/output" + "github.com/Smartling/smartling-cli/output/static" + clierror "github.com/Smartling/smartling-cli/services/helpers/cli_error" + "github.com/Smartling/smartling-cli/services/helpers/rlog" + srv "github.com/Smartling/smartling-cli/services/jobs/strings" + + jobapi "github.com/Smartling/api-sdk-go/api/job" +) + +func run(ctx context.Context, + initializer stringscmd.SrvInitializer, + params srv.AddParams, + outputParams output.Params, +) error { + rlog.Debugf("running jobs strings add with params: %v", params) + stringsSrv, err := initializer.InitJobStringsSrv(ctx) + if err != nil { + return clierror.UIError{ + Operation: "init", + Err: err, + Description: "unable to initialize Job Strings service", + } + } + + addOutput, err := stringsSrv.RunAdd(ctx, params) + if err != nil { + if errors.Is(err, jobapi.ErrNotFound) { + return clierror.UIError{ + Operation: "find job", + Err: err, + Description: fmt.Sprintf("no job found for %q", params.JobUIDOrName), + } + } + return err + } + + static.GetOutputFormat[srv.MutateOutput](outputParams.Format).FormatAndRender(addOutput) + return nil +} diff --git a/cmd/jobs/strings/cmd_job_strings.go b/cmd/jobs/strings/cmd_job_strings.go new file mode 100644 index 0000000..a30ea92 --- /dev/null +++ b/cmd/jobs/strings/cmd_job_strings.go @@ -0,0 +1,33 @@ +package jobstrings + +import ( + "github.com/spf13/cobra" +) + +// NewJobStringsCmd returns new job strings command +func NewJobStringsCmd() *cobra.Command { + jobStringsCmd := &cobra.Command{ + Use: "strings", + Short: "Manage strings on a translation job.", + Long: `Add, remove, or list the strings on an existing translation job. + +Strings are identified by hashcode. Use these commands to assign strings to a +job, detach them, or inspect which strings a job currently contains.`, + Example: ` +# Add strings to a job + + smartling-cli jobs strings add --hashcode + +# Remove strings from a job + + smartling-cli jobs strings remove --hashcode + +# List a job's strings + + smartling-cli jobs strings list + +`, + } + + return jobStringsCmd +} diff --git a/cmd/jobs/strings/list/cmd_list.go b/cmd/jobs/strings/list/cmd_list.go new file mode 100644 index 0000000..a05b0f7 --- /dev/null +++ b/cmd/jobs/strings/list/cmd_list.go @@ -0,0 +1,59 @@ +package list + +import ( + "fmt" + + stringscmd "github.com/Smartling/smartling-cli/cmd/jobs/strings" + "github.com/Smartling/smartling-cli/output" + + "github.com/spf13/cobra" +) + +const ( + targetLocaleFlag = "target-locale" + limitFlag = "limit" + offsetFlag = "offset" +) + +// NewJobStringsListCmd returns new command to job string list +func NewJobStringsListCmd(initializer stringscmd.SrvInitializer) *cobra.Command { + var ( + targetLocale string + limit uint32 + offset uint32 + ) + listCmd := &cobra.Command{ + Use: "list ", + Short: "List the strings on a translation job.", + Long: `List the strings (by hashcode and target locale) assigned to a translation job, identified by UID or name.`, + Args: cobra.ExactArgs(1), + Example: ` +# List a job's strings + + smartling-cli jobs strings list aabbccdd1122 + +# List strings for one locale as a table + + smartling-cli jobs strings list "Website Q1 2026" --target-locale fr-FR --output table +`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + params, err := resolveParams(args[0], targetLocale, limit, offset) + if err != nil { + return fmt.Errorf("failed to resolve list params: %w", err) + } + format, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + return run(ctx, initializer, params, output.Params{Format: format}) + }, + } + + listCmd.Flags().StringVar(&targetLocale, targetLocaleFlag, "", "Filter strings by target locale.") + listCmd.Flags().Uint32Var(&limit, limitFlag, 0, "Maximum number of strings to return.") + listCmd.Flags().Uint32Var(&offset, offsetFlag, 0, "Number of strings to skip.") + + return listCmd +} diff --git a/cmd/jobs/strings/list/resolve.go b/cmd/jobs/strings/list/resolve.go new file mode 100644 index 0000000..02cc08f --- /dev/null +++ b/cmd/jobs/strings/list/resolve.go @@ -0,0 +1,29 @@ +package list + +import ( + rootcmd "github.com/Smartling/smartling-cli/cmd" + clierror "github.com/Smartling/smartling-cli/services/helpers/cli_error" + "github.com/Smartling/smartling-cli/services/helpers/rlog" + srv "github.com/Smartling/smartling-cli/services/jobs/strings" +) + +func resolveParams(jobUIDOrName, targetLocale string, limit, offset uint32) (srv.ListParams, error) { + rlog.Debugf("resolving list params") + + cnf, err := rootcmd.Config() + if err != nil { + return srv.ListParams{}, clierror.UIError{ + Operation: "config", + Err: err, + Description: "failed to read config", + } + } + + return srv.ListParams{ + ProjectID: cnf.ProjectID, + JobUIDOrName: jobUIDOrName, + TargetLocaleID: targetLocale, + Limit: limit, + Offset: offset, + }, nil +} diff --git a/cmd/jobs/strings/list/resolve_test.go b/cmd/jobs/strings/list/resolve_test.go new file mode 100644 index 0000000..725bc74 --- /dev/null +++ b/cmd/jobs/strings/list/resolve_test.go @@ -0,0 +1,53 @@ +package list + +import ( + "os" + "path/filepath" + "testing" + + rootcmd "github.com/Smartling/smartling-cli/cmd" +) + +func TestMain(m *testing.M) { + rootcmd.ConfigureLogger() + os.Exit(m.Run()) +} + +func writeConfig(t *testing.T, content string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "smartling.yml") + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatalf("write config: %v", err) + } + return path +} + +func Test_resolveParams(t *testing.T) { + // Config() always requires these. + t.Setenv("SMARTLING_USER_ID", "test-user") + t.Setenv("SMARTLING_SECRET", "test-secret") + + root := rootcmd.NewRootCmd() + cfgPath := writeConfig(t, "project_id: config-project-id\n") + if err := root.PersistentFlags().Set("config", cfgPath); err != nil { + t.Fatalf("set config flag: %v", err) + } + t.Cleanup(func() { _ = root.PersistentFlags().Set("config", "") }) + + params, err := resolveParams("aabbccdd1122", "fr-FR", 10, 20) + if err != nil { + t.Fatalf("resolveParams() error = %v", err) + } + if params.ProjectID != "config-project-id" { + t.Errorf("ProjectID = %q, want config-project-id", params.ProjectID) + } + if params.JobUIDOrName != "aabbccdd1122" { + t.Errorf("JobUIDOrName = %q, want aabbccdd1122", params.JobUIDOrName) + } + if params.TargetLocaleID != "fr-FR" { + t.Errorf("TargetLocaleID = %q, want fr-FR", params.TargetLocaleID) + } + if params.Limit != 10 || params.Offset != 20 { + t.Errorf("Limit/Offset = %d/%d, want 10/20", params.Limit, params.Offset) + } +} diff --git a/cmd/jobs/strings/list/run.go b/cmd/jobs/strings/list/run.go new file mode 100644 index 0000000..ae8ac7f --- /dev/null +++ b/cmd/jobs/strings/list/run.go @@ -0,0 +1,47 @@ +package list + +import ( + "context" + "errors" + "fmt" + + stringscmd "github.com/Smartling/smartling-cli/cmd/jobs/strings" + "github.com/Smartling/smartling-cli/output" + "github.com/Smartling/smartling-cli/output/static" + clierror "github.com/Smartling/smartling-cli/services/helpers/cli_error" + "github.com/Smartling/smartling-cli/services/helpers/rlog" + srv "github.com/Smartling/smartling-cli/services/jobs/strings" + + jobapi "github.com/Smartling/api-sdk-go/api/job" +) + +func run(ctx context.Context, + initializer stringscmd.SrvInitializer, + params srv.ListParams, + outputParams output.Params, +) error { + rlog.Debugf("running jobs strings list with params: %v", params) + stringsSrv, err := initializer.InitJobStringsSrv(ctx) + if err != nil { + return clierror.UIError{ + Operation: "init", + Err: err, + Description: "unable to initialize Job Strings service", + } + } + + listOutput, err := stringsSrv.RunList(ctx, params) + if err != nil { + if errors.Is(err, jobapi.ErrNotFound) { + return clierror.UIError{ + Operation: "find job", + Err: err, + Description: fmt.Sprintf("no job found for %q", params.JobUIDOrName), + } + } + return err + } + + static.GetOutputFormat[srv.ListOutput](outputParams.Format).FormatAndRender(listOutput) + return nil +} diff --git a/cmd/jobs/strings/remove/cmd_remove.go b/cmd/jobs/strings/remove/cmd_remove.go new file mode 100644 index 0000000..5be628e --- /dev/null +++ b/cmd/jobs/strings/remove/cmd_remove.go @@ -0,0 +1,56 @@ +package remove + +import ( + "fmt" + + stringscmd "github.com/Smartling/smartling-cli/cmd/jobs/strings" + "github.com/Smartling/smartling-cli/output" + + "github.com/spf13/cobra" +) + +const ( + hashcodeFlag = "hashcode" + localeFlag = "locale" +) + +// NewJobStringsRemoveCmd returns new command to job string remove +func NewJobStringsRemoveCmd(initializer stringscmd.SrvInitializer) *cobra.Command { + var ( + hashcodes []string + localeIDs []string + ) + removeCmd := &cobra.Command{ + Use: "remove ", + Short: "Remove strings from a translation job.", + Long: `Detach strings (by hashcode) from an existing translation job, identified by UID or name.`, + Args: cobra.ExactArgs(1), + Example: ` +# Remove two strings from a job + + smartling-cli jobs strings remove aabbccdd1122 --hashcode h1 --hashcode h2 + +# Remove a string from specific locales only + + smartling-cli jobs strings remove "Website Q1 2026" --hashcode h1 --locale fr-FR +`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + params, err := resolveParams(args[0], hashcodes, localeIDs) + if err != nil { + return fmt.Errorf("failed to resolve remove params: %w", err) + } + format, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + return run(ctx, initializer, params, output.Params{Format: format}) + }, + } + + removeCmd.Flags().StringArrayVar(&hashcodes, hashcodeFlag, nil, "String hashcode to remove (repeatable, required).") + removeCmd.Flags().StringArrayVar(&localeIDs, localeFlag, nil, "Locale to remove the strings from (repeatable; default all job locales).") + + return removeCmd +} diff --git a/cmd/jobs/strings/remove/resolve.go b/cmd/jobs/strings/remove/resolve.go new file mode 100644 index 0000000..0d6f8b8 --- /dev/null +++ b/cmd/jobs/strings/remove/resolve.go @@ -0,0 +1,28 @@ +package remove + +import ( + rootcmd "github.com/Smartling/smartling-cli/cmd" + clierror "github.com/Smartling/smartling-cli/services/helpers/cli_error" + "github.com/Smartling/smartling-cli/services/helpers/rlog" + srv "github.com/Smartling/smartling-cli/services/jobs/strings" +) + +func resolveParams(jobUIDOrName string, hashcodes, localeIDs []string) (srv.RemoveParams, error) { + rlog.Debugf("resolving remove params") + + cnf, err := rootcmd.Config() + if err != nil { + return srv.RemoveParams{}, clierror.UIError{ + Operation: "config", + Err: err, + Description: "failed to read config", + } + } + + return srv.RemoveParams{ + ProjectID: cnf.ProjectID, + JobUIDOrName: jobUIDOrName, + Hashcodes: hashcodes, + LocaleIDs: localeIDs, + }, nil +} diff --git a/cmd/jobs/strings/remove/resolve_test.go b/cmd/jobs/strings/remove/resolve_test.go new file mode 100644 index 0000000..d4bd889 --- /dev/null +++ b/cmd/jobs/strings/remove/resolve_test.go @@ -0,0 +1,53 @@ +package remove + +import ( + "os" + "path/filepath" + "testing" + + rootcmd "github.com/Smartling/smartling-cli/cmd" +) + +func TestMain(m *testing.M) { + rootcmd.ConfigureLogger() + os.Exit(m.Run()) +} + +func writeConfig(t *testing.T, content string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "smartling.yml") + if err := os.WriteFile(path, []byte(content), 0o600); err != nil { + t.Fatalf("write config: %v", err) + } + return path +} + +func Test_resolveParams(t *testing.T) { + // Config() always requires these. + t.Setenv("SMARTLING_USER_ID", "test-user") + t.Setenv("SMARTLING_SECRET", "test-secret") + + root := rootcmd.NewRootCmd() + cfgPath := writeConfig(t, "project_id: config-project-id\n") + if err := root.PersistentFlags().Set("config", cfgPath); err != nil { + t.Fatalf("set config flag: %v", err) + } + t.Cleanup(func() { _ = root.PersistentFlags().Set("config", "") }) + + params, err := resolveParams("aabbccdd1122", []string{"h1"}, []string{"fr-FR"}) + if err != nil { + t.Fatalf("resolveParams() error = %v", err) + } + if params.ProjectID != "config-project-id" { + t.Errorf("ProjectID = %q, want config-project-id", params.ProjectID) + } + if params.JobUIDOrName != "aabbccdd1122" { + t.Errorf("JobUIDOrName = %q, want aabbccdd1122", params.JobUIDOrName) + } + if len(params.Hashcodes) != 1 || params.Hashcodes[0] != "h1" { + t.Errorf("Hashcodes = %v, want [h1]", params.Hashcodes) + } + if len(params.LocaleIDs) != 1 || params.LocaleIDs[0] != "fr-FR" { + t.Errorf("LocaleIDs = %v, want [fr-FR]", params.LocaleIDs) + } +} diff --git a/cmd/jobs/strings/remove/run.go b/cmd/jobs/strings/remove/run.go new file mode 100644 index 0000000..63f72e3 --- /dev/null +++ b/cmd/jobs/strings/remove/run.go @@ -0,0 +1,47 @@ +package remove + +import ( + "context" + "errors" + "fmt" + + stringscmd "github.com/Smartling/smartling-cli/cmd/jobs/strings" + "github.com/Smartling/smartling-cli/output" + "github.com/Smartling/smartling-cli/output/static" + clierror "github.com/Smartling/smartling-cli/services/helpers/cli_error" + "github.com/Smartling/smartling-cli/services/helpers/rlog" + srv "github.com/Smartling/smartling-cli/services/jobs/strings" + + jobapi "github.com/Smartling/api-sdk-go/api/job" +) + +func run(ctx context.Context, + initializer stringscmd.SrvInitializer, + params srv.RemoveParams, + outputParams output.Params, +) error { + rlog.Debugf("running jobs strings remove with params: %v", params) + stringsSrv, err := initializer.InitJobStringsSrv(ctx) + if err != nil { + return clierror.UIError{ + Operation: "init", + Err: err, + Description: "unable to initialize Job Strings service", + } + } + + removeOutput, err := stringsSrv.RunRemove(ctx, params) + if err != nil { + if errors.Is(err, jobapi.ErrNotFound) { + return clierror.UIError{ + Operation: "find job", + Err: err, + Description: fmt.Sprintf("no job found for %q", params.JobUIDOrName), + } + } + return err + } + + static.GetOutputFormat[srv.MutateOutput](outputParams.Format).FormatAndRender(removeOutput) + return nil +} diff --git a/cmd/jobs/strings/srv_initializer.go b/cmd/jobs/strings/srv_initializer.go new file mode 100644 index 0000000..3a46070 --- /dev/null +++ b/cmd/jobs/strings/srv_initializer.go @@ -0,0 +1,33 @@ +package jobstrings + +import ( + "context" + + rootcmd "github.com/Smartling/smartling-cli/cmd" + srv "github.com/Smartling/smartling-cli/services/jobs/strings" + + jobapi "github.com/Smartling/api-sdk-go/api/job" + stringapi "github.com/Smartling/api-sdk-go/api/job/string" +) + +// SrvInitializer defines job strings service initializer +type SrvInitializer interface { + InitJobStringsSrv(ctx context.Context) (srv.Service, error) +} + +// NewSrvInitializer returns new SrvInitializer implementation +func NewSrvInitializer() SrvInitializer { + return srvInitializer{} +} + +type srvInitializer struct{} + +// InitJobStringsSrv initializes the job strings service with the client. +func (i srvInitializer) InitJobStringsSrv(ctx context.Context) (srv.Service, error) { + client, err := rootcmd.Client(ctx) + if err != nil { + return nil, err + } + stringsSrv := srv.NewService(stringapi.NewJobString(client.Client), jobapi.NewJob(client.Client)) + return stringsSrv, nil +} diff --git a/main.go b/main.go index 1ee2de5..d6ebb40 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,10 @@ import ( joblocaleadd "github.com/Smartling/smartling-cli/cmd/jobs/locales/add" joblocaleremove "github.com/Smartling/smartling-cli/cmd/jobs/locales/remove" "github.com/Smartling/smartling-cli/cmd/jobs/progress" + jobstrings "github.com/Smartling/smartling-cli/cmd/jobs/strings" + jobstringadd "github.com/Smartling/smartling-cli/cmd/jobs/strings/add" + jobstringlist "github.com/Smartling/smartling-cli/cmd/jobs/strings/list" + jobstringremove "github.com/Smartling/smartling-cli/cmd/jobs/strings/remove" jobview "github.com/Smartling/smartling-cli/cmd/jobs/view" "github.com/Smartling/smartling-cli/cmd/mt" "github.com/Smartling/smartling-cli/cmd/mt/detect" @@ -87,6 +91,12 @@ func main() { jobLocales.AddCommand(joblocaleadd.NewJobLocalesAddCmd(jobLocalesInitializer)) jobLocales.AddCommand(joblocaleremove.NewJobLocalesRemoveCmd(jobLocalesInitializer)) jobsCmd.AddCommand(jobLocales) + jobStrings := jobstrings.NewJobStringsCmd() + jobStringsInitializer := jobstrings.NewSrvInitializer() + jobStrings.AddCommand(jobstringadd.NewJobStringsAddCmd(jobStringsInitializer)) + jobStrings.AddCommand(jobstringremove.NewJobStringsRemoveCmd(jobStringsInitializer)) + jobStrings.AddCommand(jobstringlist.NewJobStringsListCmd(jobStringsInitializer)) + jobsCmd.AddCommand(jobStrings) glossariesCmd := glossaries.NewGlossariesCmd() rootCmd.AddCommand(glossariesCmd) diff --git a/services/jobs/locales/sdkmocks/locale.go b/services/jobs/locales/sdkmocks/locale.go index 09fa55b..022342d 100644 --- a/services/jobs/locales/sdkmocks/locale.go +++ b/services/jobs/locales/sdkmocks/locale.go @@ -38,8 +38,8 @@ func (_m *MockJobLocale) EXPECT() *MockJobLocale_Expecter { } // Add provides a mock function for the type MockJobLocale -func (_mock *MockJobLocale) Add(ctx context.Context, projectID string, translationJobUID string, targetLocaleID string) error { - ret := _mock.Called(ctx, projectID, translationJobUID, targetLocaleID) +func (_mock *MockJobLocale) Add(ctx context.Context, projectID string, jobUID string, targetLocaleID string) error { + ret := _mock.Called(ctx, projectID, jobUID, targetLocaleID) if len(ret) == 0 { panic("no return value specified for Add") @@ -47,7 +47,7 @@ func (_mock *MockJobLocale) Add(ctx context.Context, projectID string, translati var r0 error if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok { - r0 = returnFunc(ctx, projectID, translationJobUID, targetLocaleID) + r0 = returnFunc(ctx, projectID, jobUID, targetLocaleID) } else { r0 = ret.Error(0) } @@ -62,13 +62,13 @@ type MockJobLocale_Add_Call struct { // Add is a helper method to define mock.On call // - ctx context.Context // - projectID string -// - translationJobUID string +// - jobUID string // - targetLocaleID string -func (_e *MockJobLocale_Expecter) Add(ctx interface{}, projectID interface{}, translationJobUID interface{}, targetLocaleID interface{}) *MockJobLocale_Add_Call { - return &MockJobLocale_Add_Call{Call: _e.mock.On("Add", ctx, projectID, translationJobUID, targetLocaleID)} +func (_e *MockJobLocale_Expecter) Add(ctx interface{}, projectID interface{}, jobUID interface{}, targetLocaleID interface{}) *MockJobLocale_Add_Call { + return &MockJobLocale_Add_Call{Call: _e.mock.On("Add", ctx, projectID, jobUID, targetLocaleID)} } -func (_c *MockJobLocale_Add_Call) Run(run func(ctx context.Context, projectID string, translationJobUID string, targetLocaleID string)) *MockJobLocale_Add_Call { +func (_c *MockJobLocale_Add_Call) Run(run func(ctx context.Context, projectID string, jobUID string, targetLocaleID string)) *MockJobLocale_Add_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -101,14 +101,14 @@ func (_c *MockJobLocale_Add_Call) Return(err error) *MockJobLocale_Add_Call { return _c } -func (_c *MockJobLocale_Add_Call) RunAndReturn(run func(ctx context.Context, projectID string, translationJobUID string, targetLocaleID string) error) *MockJobLocale_Add_Call { +func (_c *MockJobLocale_Add_Call) RunAndReturn(run func(ctx context.Context, projectID string, jobUID string, targetLocaleID string) error) *MockJobLocale_Add_Call { _c.Call.Return(run) return _c } // Remove provides a mock function for the type MockJobLocale -func (_mock *MockJobLocale) Remove(ctx context.Context, projectID string, translationJobUID string, targetLocaleID string) error { - ret := _mock.Called(ctx, projectID, translationJobUID, targetLocaleID) +func (_mock *MockJobLocale) Remove(ctx context.Context, projectID string, jobUID string, targetLocaleID string) error { + ret := _mock.Called(ctx, projectID, jobUID, targetLocaleID) if len(ret) == 0 { panic("no return value specified for Remove") @@ -116,7 +116,7 @@ func (_mock *MockJobLocale) Remove(ctx context.Context, projectID string, transl var r0 error if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok { - r0 = returnFunc(ctx, projectID, translationJobUID, targetLocaleID) + r0 = returnFunc(ctx, projectID, jobUID, targetLocaleID) } else { r0 = ret.Error(0) } @@ -131,13 +131,13 @@ type MockJobLocale_Remove_Call struct { // Remove is a helper method to define mock.On call // - ctx context.Context // - projectID string -// - translationJobUID string +// - jobUID string // - targetLocaleID string -func (_e *MockJobLocale_Expecter) Remove(ctx interface{}, projectID interface{}, translationJobUID interface{}, targetLocaleID interface{}) *MockJobLocale_Remove_Call { - return &MockJobLocale_Remove_Call{Call: _e.mock.On("Remove", ctx, projectID, translationJobUID, targetLocaleID)} +func (_e *MockJobLocale_Expecter) Remove(ctx interface{}, projectID interface{}, jobUID interface{}, targetLocaleID interface{}) *MockJobLocale_Remove_Call { + return &MockJobLocale_Remove_Call{Call: _e.mock.On("Remove", ctx, projectID, jobUID, targetLocaleID)} } -func (_c *MockJobLocale_Remove_Call) Run(run func(ctx context.Context, projectID string, translationJobUID string, targetLocaleID string)) *MockJobLocale_Remove_Call { +func (_c *MockJobLocale_Remove_Call) Run(run func(ctx context.Context, projectID string, jobUID string, targetLocaleID string)) *MockJobLocale_Remove_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -170,7 +170,7 @@ func (_c *MockJobLocale_Remove_Call) Return(err error) *MockJobLocale_Remove_Cal return _c } -func (_c *MockJobLocale_Remove_Call) RunAndReturn(run func(ctx context.Context, projectID string, translationJobUID string, targetLocaleID string) error) *MockJobLocale_Remove_Call { +func (_c *MockJobLocale_Remove_Call) RunAndReturn(run func(ctx context.Context, projectID string, jobUID string, targetLocaleID string) error) *MockJobLocale_Remove_Call { _c.Call.Return(run) return _c } diff --git a/services/jobs/strings/run_add.go b/services/jobs/strings/run_add.go new file mode 100644 index 0000000..3f5b015 --- /dev/null +++ b/services/jobs/strings/run_add.go @@ -0,0 +1,44 @@ +package jobstrings + +import ( + "context" + + "github.com/Smartling/smartling-cli/services/jobs/jobresolver" + + api "github.com/Smartling/api-sdk-go/api/job/string" +) + +// AddParams define add string params. +type AddParams struct { + ProjectID string + JobUIDOrName string + Hashcodes []string + TargetLocaleIDs []string + MoveEnabled bool +} + +// Validate checks that AddParams are valid. +func (p AddParams) Validate() error { + return validateMutate(p.ProjectID, p.JobUIDOrName, p.Hashcodes) +} + +// RunAdd assigns strings to a translation job. +func (s service) RunAdd(ctx context.Context, params AddParams) (MutateOutput, error) { + if err := params.Validate(); err != nil { + return MutateOutput{}, err + } + jobUID, err := jobresolver.GetJobUID(ctx, s.job, params.ProjectID, params.JobUIDOrName) + if err != nil { + return MutateOutput{}, err + } + req := api.AddRequest{ + Hashcodes: params.Hashcodes, + TargetLocaleIDs: params.TargetLocaleIDs, + MoveEnabled: params.MoveEnabled, + } + res, err := s.jobString.Add(ctx, params.ProjectID, jobUID, req) + if err != nil { + return MutateOutput{}, err + } + return newMutateOutput("added", params.ProjectID, jobUID, params.Hashcodes, params.TargetLocaleIDs, res.SuccessCount, res.FailCount) +} diff --git a/services/jobs/strings/run_add_test.go b/services/jobs/strings/run_add_test.go new file mode 100644 index 0000000..59a5064 --- /dev/null +++ b/services/jobs/strings/run_add_test.go @@ -0,0 +1,112 @@ +package jobstrings + +import ( + "context" + "errors" + "strings" + "testing" + + jobsdkmocks "github.com/Smartling/smartling-cli/services/jobs/sdkmocks" + stringsdkmocks "github.com/Smartling/smartling-cli/services/jobs/strings/sdkmocks" + + jobapi "github.com/Smartling/api-sdk-go/api/job" + api "github.com/Smartling/api-sdk-go/api/job/string" +) + +func TestRunAdd(t *testing.T) { + ctx := context.Background() + const ( + projectUID = "test-project-id" + jobUID = "aabbccdd1122" + ) + + tests := []struct { + name string + params AddParams + setup func(*jobsdkmocks.MockJob, *stringsdkmocks.MockJobString) + wantErr bool + check func(*testing.T, MutateOutput) + }{ + { + name: "validation error — no hashcodes", + params: AddParams{ProjectID: projectUID, JobUIDOrName: jobUID}, + setup: func(*jobsdkmocks.MockJob, *stringsdkmocks.MockJobString) {}, + wantErr: true, + }, + { + name: "resolves UID and adds strings", + params: AddParams{ + ProjectID: projectUID, JobUIDOrName: jobUID, + Hashcodes: []string{"h1", "h2"}, TargetLocaleIDs: []string{"fr-FR"}, MoveEnabled: true, + }, + setup: func(j *jobsdkmocks.MockJob, s *stringsdkmocks.MockJobString) { + j.EXPECT().GetJob(ctx, projectUID, jobUID).Return(jobapi.GetJobResponse{TranslationJobUID: jobUID}, nil) + s.EXPECT().Add(ctx, projectUID, jobUID, api.AddRequest{ + Hashcodes: []string{"h1", "h2"}, TargetLocaleIDs: []string{"fr-FR"}, MoveEnabled: true, + }).Return(api.Result{SuccessCount: 2}, nil) + }, + check: func(t *testing.T, got MutateOutput) { + if got.Action != "added" || got.TranslationJobUID != jobUID || len(got.Hashcodes) != 2 { + t.Fatalf("unexpected output: %+v", got) + } + if got.SuccessCount != 2 { + t.Errorf("SuccessCount = %d, want 2", got.SuccessCount) + } + if len(got.JSON) == 0 { + t.Error("JSON should not be empty") + } + }, + }, + { + name: "nonexistent hashcodes report zero affected with a hint", + params: AddParams{ProjectID: projectUID, JobUIDOrName: jobUID, Hashcodes: []string{"bad1", "bad2"}}, + setup: func(j *jobsdkmocks.MockJob, s *stringsdkmocks.MockJobString) { + j.EXPECT().GetJob(ctx, projectUID, jobUID).Return(jobapi.GetJobResponse{TranslationJobUID: jobUID}, nil) + s.EXPECT().Add(ctx, projectUID, jobUID, api.AddRequest{Hashcodes: []string{"bad1", "bad2"}}). + Return(api.Result{SuccessCount: 0, FailCount: 0}, nil) + }, + check: func(t *testing.T, got MutateOutput) { + if got.SuccessCount != 0 || got.FailCount != 0 { + t.Fatalf("counts = %d/%d, want 0/0", got.SuccessCount, got.FailCount) + } + lines := got.SimpleLines() + if len(lines) != 2 || !strings.Contains(lines[1], "Verify the hashcodes") { + t.Errorf("SimpleLines = %v, want a zero-affected hint", lines) + } + }, + }, + { + name: "job not found by name", + params: AddParams{ProjectID: projectUID, JobUIDOrName: "No Such Job", Hashcodes: []string{"h1"}}, + setup: func(j *jobsdkmocks.MockJob, s *stringsdkmocks.MockJobString) { + j.EXPECT().ListProjectJobs(ctx, projectUID, jobapi.ListProjectJobsParams{JobName: "No Such Job"}). + Return(jobapi.ListJobsResponse{}, nil) + }, + wantErr: true, + }, + { + name: "add API error", + params: AddParams{ProjectID: projectUID, JobUIDOrName: jobUID, Hashcodes: []string{"h1"}}, + setup: func(j *jobsdkmocks.MockJob, s *stringsdkmocks.MockJobString) { + j.EXPECT().GetJob(ctx, projectUID, jobUID).Return(jobapi.GetJobResponse{TranslationJobUID: jobUID}, nil) + s.EXPECT().Add(ctx, projectUID, jobUID, api.AddRequest{Hashcodes: []string{"h1"}}). + Return(api.Result{}, errors.New("api error")) + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + job := jobsdkmocks.NewMockJob(t) + str := stringsdkmocks.NewMockJobString(t) + tt.setup(job, str) + got, err := service{jobString: str, job: job}.RunAdd(ctx, tt.params) + if (err != nil) != tt.wantErr { + t.Fatalf("RunAdd() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && tt.check != nil { + tt.check(t, got) + } + }) + } +} diff --git a/services/jobs/strings/run_list.go b/services/jobs/strings/run_list.go new file mode 100644 index 0000000..5f020a6 --- /dev/null +++ b/services/jobs/strings/run_list.go @@ -0,0 +1,96 @@ +package jobstrings + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/Smartling/smartling-cli/services/jobs/jobresolver" + + api "github.com/Smartling/api-sdk-go/api/job/string" +) + +// ListParams defines the list-strings params. +type ListParams struct { + ProjectID string + JobUIDOrName string + TargetLocaleID string + Limit uint32 + Offset uint32 +} + +// Validate checks that ListParams are valid. +func (p ListParams) Validate() error { + return validateIDs(p.ProjectID, p.JobUIDOrName) +} + +// Item is a single string row in a job. +type Item struct { + TargetLocaleID string `json:"targetLocaleId"` + Hashcode string `json:"hashcode"` +} + +// ListOutput is the result of listing a job's strings. +type ListOutput struct { + TotalCount uint32 `json:"totalCount"` + Items []Item `json:"items"` + + JSON []byte `json:"-"` +} + +func newListOutput(resp api.ListResponse) (ListOutput, error) { + o := ListOutput{TotalCount: resp.TotalCount} + for _, it := range resp.Items { + o.Items = append(o.Items, Item{TargetLocaleID: it.TargetLocaleID, Hashcode: it.Hashcode}) + } + var err error + if o.JSON, err = json.Marshal(o); err != nil { + return ListOutput{}, err + } + return o, nil +} + +// JSONBytes returns the JSON representation of the list. +func (o ListOutput) JSONBytes() []byte { return o.JSON } + +// SimpleLines returns a human-readable summary of the list. +func (o ListOutput) SimpleLines() []string { + if len(o.Items) == 0 { + return []string{"No strings found."} + } + lines := make([]string, 0, len(o.Items)) + for _, it := range o.Items { + lines = append(lines, fmt.Sprintf("%s %s", it.TargetLocaleID, it.Hashcode)) + } + return lines +} + +// TableData returns the list as one row per string. +func (o ListOutput) TableData() ([]string, [][]string) { + headers := []string{"TARGET LOCALE ID", "HASHCODE"} + rows := make([][]string, 0, len(o.Items)) + for _, it := range o.Items { + rows = append(rows, []string{it.TargetLocaleID, it.Hashcode}) + } + return headers, rows +} + +// RunList retrieves the strings assigned to a translation job. +func (s service) RunList(ctx context.Context, params ListParams) (ListOutput, error) { + if err := params.Validate(); err != nil { + return ListOutput{}, err + } + jobUID, err := jobresolver.GetJobUID(ctx, s.job, params.ProjectID, params.JobUIDOrName) + if err != nil { + return ListOutput{}, err + } + resp, err := s.jobString.List(ctx, params.ProjectID, jobUID, api.ListParams{ + TargetLocaleID: params.TargetLocaleID, + Limit: params.Limit, + Offset: params.Offset, + }) + if err != nil { + return ListOutput{}, err + } + return newListOutput(resp) +} diff --git a/services/jobs/strings/run_list_test.go b/services/jobs/strings/run_list_test.go new file mode 100644 index 0000000..8c76d0a --- /dev/null +++ b/services/jobs/strings/run_list_test.go @@ -0,0 +1,72 @@ +package jobstrings + +import ( + "context" + "testing" + + jobsdkmocks "github.com/Smartling/smartling-cli/services/jobs/sdkmocks" + stringsdkmocks "github.com/Smartling/smartling-cli/services/jobs/strings/sdkmocks" + + jobapi "github.com/Smartling/api-sdk-go/api/job" + api "github.com/Smartling/api-sdk-go/api/job/string" +) + +func TestRunList(t *testing.T) { + ctx := context.Background() + const ( + projectUID = "test-project-id" + jobUID = "aabbccdd1122" + ) + + tests := []struct { + name string + params ListParams + setup func(*jobsdkmocks.MockJob, *stringsdkmocks.MockJobString) + wantErr bool + check func(*testing.T, ListOutput) + }{ + { + name: "validation error — empty job", + params: ListParams{ProjectID: projectUID}, + setup: func(*jobsdkmocks.MockJob, *stringsdkmocks.MockJobString) {}, + wantErr: true, + }, + { + name: "resolves UID and lists strings with filter", + params: ListParams{ProjectID: projectUID, JobUIDOrName: jobUID, TargetLocaleID: "fr-FR", Limit: 10}, + setup: func(j *jobsdkmocks.MockJob, s *stringsdkmocks.MockJobString) { + j.EXPECT().GetJob(ctx, projectUID, jobUID).Return(jobapi.GetJobResponse{TranslationJobUID: jobUID}, nil) + s.EXPECT().List(ctx, projectUID, jobUID, api.ListParams{TargetLocaleID: "fr-FR", Limit: 10}). + Return(api.ListResponse{ + TotalCount: 2, + Items: []api.StringHashcode{ + {TargetLocaleID: "fr-FR", Hashcode: "h1"}, + {TargetLocaleID: "fr-FR", Hashcode: "h2"}, + }, + }, nil) + }, + check: func(t *testing.T, got ListOutput) { + if got.TotalCount != 2 || len(got.Items) != 2 { + t.Fatalf("unexpected output: %+v", got) + } + if got.Items[0].Hashcode != "h1" || got.Items[1].TargetLocaleID != "fr-FR" { + t.Errorf("unexpected items: %+v", got.Items) + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + job := jobsdkmocks.NewMockJob(t) + str := stringsdkmocks.NewMockJobString(t) + tt.setup(job, str) + got, err := service{jobString: str, job: job}.RunList(ctx, tt.params) + if (err != nil) != tt.wantErr { + t.Fatalf("RunList() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && tt.check != nil { + tt.check(t, got) + } + }) + } +} diff --git a/services/jobs/strings/run_remove.go b/services/jobs/strings/run_remove.go new file mode 100644 index 0000000..eb634f1 --- /dev/null +++ b/services/jobs/strings/run_remove.go @@ -0,0 +1,42 @@ +package jobstrings + +import ( + "context" + + "github.com/Smartling/smartling-cli/services/jobs/jobresolver" + + api "github.com/Smartling/api-sdk-go/api/job/string" +) + +// RemoveParams defines the remove-strings params. +type RemoveParams struct { + ProjectID string + JobUIDOrName string + Hashcodes []string + LocaleIDs []string +} + +// Validate checks that RemoveParams are valid. +func (p RemoveParams) Validate() error { + return validateMutate(p.ProjectID, p.JobUIDOrName, p.Hashcodes) +} + +// RunRemove unassigns strings from a translation job. +func (s service) RunRemove(ctx context.Context, params RemoveParams) (MutateOutput, error) { + if err := params.Validate(); err != nil { + return MutateOutput{}, err + } + jobUID, err := jobresolver.GetJobUID(ctx, s.job, params.ProjectID, params.JobUIDOrName) + if err != nil { + return MutateOutput{}, err + } + req := api.RemoveRequest{ + Hashcodes: params.Hashcodes, + LocaleIDs: params.LocaleIDs, + } + res, err := s.jobString.Remove(ctx, params.ProjectID, jobUID, req) + if err != nil { + return MutateOutput{}, err + } + return newMutateOutput("removed", params.ProjectID, jobUID, params.Hashcodes, params.LocaleIDs, res.SuccessCount, res.FailCount) +} diff --git a/services/jobs/strings/run_remove_test.go b/services/jobs/strings/run_remove_test.go new file mode 100644 index 0000000..e8e3adf --- /dev/null +++ b/services/jobs/strings/run_remove_test.go @@ -0,0 +1,81 @@ +package jobstrings + +import ( + "context" + "errors" + "testing" + + jobsdkmocks "github.com/Smartling/smartling-cli/services/jobs/sdkmocks" + stringsdkmocks "github.com/Smartling/smartling-cli/services/jobs/strings/sdkmocks" + + jobapi "github.com/Smartling/api-sdk-go/api/job" + api "github.com/Smartling/api-sdk-go/api/job/string" +) + +func TestRunRemove(t *testing.T) { + ctx := context.Background() + const ( + projectUID = "test-project-id" + jobUID = "aabbccdd1122" + ) + + tests := []struct { + name string + params RemoveParams + setup func(*jobsdkmocks.MockJob, *stringsdkmocks.MockJobString) + wantErr bool + check func(*testing.T, MutateOutput) + }{ + { + name: "validation error — no hashcodes", + params: RemoveParams{ProjectID: projectUID, JobUIDOrName: jobUID}, + setup: func(*jobsdkmocks.MockJob, *stringsdkmocks.MockJobString) {}, + wantErr: true, + }, + { + name: "resolves UID and removes strings", + params: RemoveParams{ + ProjectID: projectUID, JobUIDOrName: jobUID, + Hashcodes: []string{"h1"}, LocaleIDs: []string{"fr-FR"}, + }, + setup: func(j *jobsdkmocks.MockJob, s *stringsdkmocks.MockJobString) { + j.EXPECT().GetJob(ctx, projectUID, jobUID).Return(jobapi.GetJobResponse{TranslationJobUID: jobUID}, nil) + s.EXPECT().Remove(ctx, projectUID, jobUID, api.RemoveRequest{ + Hashcodes: []string{"h1"}, LocaleIDs: []string{"fr-FR"}, + }).Return(api.Result{SuccessCount: 1}, nil) + }, + check: func(t *testing.T, got MutateOutput) { + if got.Action != "removed" || got.TranslationJobUID != jobUID || len(got.Hashcodes) != 1 { + t.Fatalf("unexpected output: %+v", got) + } + if got.SuccessCount != 1 { + t.Errorf("SuccessCount = %d, want 1", got.SuccessCount) + } + }, + }, + { + name: "remove API error", + params: RemoveParams{ProjectID: projectUID, JobUIDOrName: jobUID, Hashcodes: []string{"h1"}}, + setup: func(j *jobsdkmocks.MockJob, s *stringsdkmocks.MockJobString) { + j.EXPECT().GetJob(ctx, projectUID, jobUID).Return(jobapi.GetJobResponse{TranslationJobUID: jobUID}, nil) + s.EXPECT().Remove(ctx, projectUID, jobUID, api.RemoveRequest{Hashcodes: []string{"h1"}}). + Return(api.Result{}, errors.New("api error")) + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + job := jobsdkmocks.NewMockJob(t) + str := stringsdkmocks.NewMockJobString(t) + tt.setup(job, str) + got, err := service{jobString: str, job: job}.RunRemove(ctx, tt.params) + if (err != nil) != tt.wantErr { + t.Fatalf("RunRemove() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && tt.check != nil { + tt.check(t, got) + } + }) + } +} diff --git a/services/jobs/strings/sdkmocks/jobstring.go b/services/jobs/strings/sdkmocks/jobstring.go new file mode 100644 index 0000000..ecfc1d2 --- /dev/null +++ b/services/jobs/strings/sdkmocks/jobstring.go @@ -0,0 +1,273 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package jobstringmocks + +import ( + "context" + + "github.com/Smartling/api-sdk-go/api/job/string" + mock "github.com/stretchr/testify/mock" +) + +// NewMockJobString creates a new instance of MockJobString. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockJobString(t interface { + mock.TestingT + Cleanup(func()) +}) *MockJobString { + mock := &MockJobString{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockJobString is an autogenerated mock type for the JobString type +type MockJobString struct { + mock.Mock +} + +type MockJobString_Expecter struct { + mock *mock.Mock +} + +func (_m *MockJobString) EXPECT() *MockJobString_Expecter { + return &MockJobString_Expecter{mock: &_m.Mock} +} + +// Add provides a mock function for the type MockJobString +func (_mock *MockJobString) Add(ctx context.Context, projectID string, translationJobUID string, req jobstring.AddRequest) (jobstring.Result, error) { + ret := _mock.Called(ctx, projectID, translationJobUID, req) + + if len(ret) == 0 { + panic("no return value specified for Add") + } + + var r0 jobstring.Result + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, jobstring.AddRequest) (jobstring.Result, error)); ok { + return returnFunc(ctx, projectID, translationJobUID, req) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, jobstring.AddRequest) jobstring.Result); ok { + r0 = returnFunc(ctx, projectID, translationJobUID, req) + } else { + r0 = ret.Get(0).(jobstring.Result) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, jobstring.AddRequest) error); ok { + r1 = returnFunc(ctx, projectID, translationJobUID, req) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockJobString_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' +type MockJobString_Add_Call struct { + *mock.Call +} + +// Add is a helper method to define mock.On call +// - ctx context.Context +// - projectID string +// - translationJobUID string +// - req jobstring.AddRequest +func (_e *MockJobString_Expecter) Add(ctx interface{}, projectID interface{}, translationJobUID interface{}, req interface{}) *MockJobString_Add_Call { + return &MockJobString_Add_Call{Call: _e.mock.On("Add", ctx, projectID, translationJobUID, req)} +} + +func (_c *MockJobString_Add_Call) Run(run func(ctx context.Context, projectID string, translationJobUID string, req jobstring.AddRequest)) *MockJobString_Add_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 jobstring.AddRequest + if args[3] != nil { + arg3 = args[3].(jobstring.AddRequest) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockJobString_Add_Call) Return(result jobstring.Result, err error) *MockJobString_Add_Call { + _c.Call.Return(result, err) + return _c +} + +func (_c *MockJobString_Add_Call) RunAndReturn(run func(ctx context.Context, projectID string, translationJobUID string, req jobstring.AddRequest) (jobstring.Result, error)) *MockJobString_Add_Call { + _c.Call.Return(run) + return _c +} + +// List provides a mock function for the type MockJobString +func (_mock *MockJobString) List(ctx context.Context, projectID string, translationJobUID string, params jobstring.ListParams) (jobstring.ListResponse, error) { + ret := _mock.Called(ctx, projectID, translationJobUID, params) + + if len(ret) == 0 { + panic("no return value specified for List") + } + + var r0 jobstring.ListResponse + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, jobstring.ListParams) (jobstring.ListResponse, error)); ok { + return returnFunc(ctx, projectID, translationJobUID, params) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, jobstring.ListParams) jobstring.ListResponse); ok { + r0 = returnFunc(ctx, projectID, translationJobUID, params) + } else { + r0 = ret.Get(0).(jobstring.ListResponse) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, jobstring.ListParams) error); ok { + r1 = returnFunc(ctx, projectID, translationJobUID, params) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockJobString_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List' +type MockJobString_List_Call struct { + *mock.Call +} + +// List is a helper method to define mock.On call +// - ctx context.Context +// - projectID string +// - translationJobUID string +// - params jobstring.ListParams +func (_e *MockJobString_Expecter) List(ctx interface{}, projectID interface{}, translationJobUID interface{}, params interface{}) *MockJobString_List_Call { + return &MockJobString_List_Call{Call: _e.mock.On("List", ctx, projectID, translationJobUID, params)} +} + +func (_c *MockJobString_List_Call) Run(run func(ctx context.Context, projectID string, translationJobUID string, params jobstring.ListParams)) *MockJobString_List_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 jobstring.ListParams + if args[3] != nil { + arg3 = args[3].(jobstring.ListParams) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockJobString_List_Call) Return(listResponse jobstring.ListResponse, err error) *MockJobString_List_Call { + _c.Call.Return(listResponse, err) + return _c +} + +func (_c *MockJobString_List_Call) RunAndReturn(run func(ctx context.Context, projectID string, translationJobUID string, params jobstring.ListParams) (jobstring.ListResponse, error)) *MockJobString_List_Call { + _c.Call.Return(run) + return _c +} + +// Remove provides a mock function for the type MockJobString +func (_mock *MockJobString) Remove(ctx context.Context, projectID string, translationJobUID string, req jobstring.RemoveRequest) (jobstring.Result, error) { + ret := _mock.Called(ctx, projectID, translationJobUID, req) + + if len(ret) == 0 { + panic("no return value specified for Remove") + } + + var r0 jobstring.Result + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, jobstring.RemoveRequest) (jobstring.Result, error)); ok { + return returnFunc(ctx, projectID, translationJobUID, req) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, jobstring.RemoveRequest) jobstring.Result); ok { + r0 = returnFunc(ctx, projectID, translationJobUID, req) + } else { + r0 = ret.Get(0).(jobstring.Result) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, jobstring.RemoveRequest) error); ok { + r1 = returnFunc(ctx, projectID, translationJobUID, req) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockJobString_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove' +type MockJobString_Remove_Call struct { + *mock.Call +} + +// Remove is a helper method to define mock.On call +// - ctx context.Context +// - projectID string +// - translationJobUID string +// - req jobstring.RemoveRequest +func (_e *MockJobString_Expecter) Remove(ctx interface{}, projectID interface{}, translationJobUID interface{}, req interface{}) *MockJobString_Remove_Call { + return &MockJobString_Remove_Call{Call: _e.mock.On("Remove", ctx, projectID, translationJobUID, req)} +} + +func (_c *MockJobString_Remove_Call) Run(run func(ctx context.Context, projectID string, translationJobUID string, req jobstring.RemoveRequest)) *MockJobString_Remove_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 jobstring.RemoveRequest + if args[3] != nil { + arg3 = args[3].(jobstring.RemoveRequest) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockJobString_Remove_Call) Return(result jobstring.Result, err error) *MockJobString_Remove_Call { + _c.Call.Return(result, err) + return _c +} + +func (_c *MockJobString_Remove_Call) RunAndReturn(run func(ctx context.Context, projectID string, translationJobUID string, req jobstring.RemoveRequest) (jobstring.Result, error)) *MockJobString_Remove_Call { + _c.Call.Return(run) + return _c +} diff --git a/services/jobs/strings/service.go b/services/jobs/strings/service.go new file mode 100644 index 0000000..0e60b74 --- /dev/null +++ b/services/jobs/strings/service.go @@ -0,0 +1,107 @@ +package jobstrings + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + jobapi "github.com/Smartling/api-sdk-go/api/job" + api "github.com/Smartling/api-sdk-go/api/job/string" +) + +// Service defines behavior for managing a translation job's strings. +type Service interface { + RunAdd(ctx context.Context, params AddParams) (MutateOutput, error) + RunRemove(ctx context.Context, params RemoveParams) (MutateOutput, error) + RunList(ctx context.Context, params ListParams) (ListOutput, error) +} + +// NewService creates a new implementation of the Service. The job API resolves a +// job UID from a UID-or-name; jobString performs the add/remove/list. +func NewService(jobString api.JobString, job jobapi.Job) Service { + return service{ + jobString: jobString, + job: job, + } +} + +type service struct { + jobString api.JobString + job jobapi.Job +} + +// MutateOutput is the result of an add/remove strings operation. SuccessCount +// and FailCount come from the API and reflect what actually happened - a +// nonexistent hashcode is ignored by the API and counted in neither. +type MutateOutput struct { + Action string `json:"action"` + ProjectUID string `json:"projectUid"` + TranslationJobUID string `json:"translationJobUid"` + Hashcodes []string `json:"hashcodes"` + LocaleIDs []string `json:"localeIds,omitempty"` + SuccessCount int `json:"successCount"` + FailCount int `json:"failCount"` + + JSON []byte `json:"-"` +} + +func newMutateOutput(action, projectUID, jobUID string, hashcodes, localeIDs []string, successCount, failCount int) (MutateOutput, error) { + o := MutateOutput{ + Action: action, + ProjectUID: projectUID, + TranslationJobUID: jobUID, + Hashcodes: hashcodes, + LocaleIDs: localeIDs, + SuccessCount: successCount, + FailCount: failCount, + } + var err error + if o.JSON, err = json.Marshal(o); err != nil { + return MutateOutput{}, err + } + return o, nil +} + +// JSONBytes returns the JSON representation of the result. +func (o MutateOutput) JSONBytes() []byte { return o.JSON } + +// SimpleLines returns a human-readable summary of the result. +func (o MutateOutput) SimpleLines() []string { + lines := []string{fmt.Sprintf("Strings %s — job %s: %d succeeded, %d failed", + o.Action, o.TranslationJobUID, o.SuccessCount, o.FailCount)} + if o.SuccessCount == 0 && o.FailCount == 0 { + lines = append(lines, fmt.Sprintf( + "No strings were affected. Verify the hashcodes exist in the project: %s", + strings.Join(o.Hashcodes, ", "))) + } + return lines +} + +// TableData returns the result as a single-row table. +func (o MutateOutput) TableData() ([]string, [][]string) { + return []string{"ACTION", "TRANSLATION JOB UID", "SUCCEEDED", "FAILED"}, + [][]string{{o.Action, o.TranslationJobUID, strconv.Itoa(o.SuccessCount), strconv.Itoa(o.FailCount)}} +} + +func validateIDs(projectID, jobUIDOrName string) error { + switch { + case projectID == "": + return errors.New("project ID is required") + case jobUIDOrName == "": + return errors.New("translation job UID or name is required") + } + return nil +} + +func validateMutate(projectID, jobUIDOrName string, hashcodes []string) error { + if err := validateIDs(projectID, jobUIDOrName); err != nil { + return err + } + if len(hashcodes) == 0 { + return errors.New("at least one --hashcode is required") + } + return nil +} diff --git a/tests/cmd/jobs/strings/add/add_test.go b/tests/cmd/jobs/strings/add/add_test.go new file mode 100644 index 0000000..251569f --- /dev/null +++ b/tests/cmd/jobs/strings/add/add_test.go @@ -0,0 +1,72 @@ +package add + +import ( + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestJobsStringsAdd(t *testing.T) { + absDir, err := filepath.Abs("../../../bin/") + if err != nil { + t.Fatalf("Failed to get abs path: %v", err) + } + subCommands := []string{"jobs", "strings", "add"} + tests := []struct { + name string + args []string + expectedOutputs []string + unexpectedOutputs []string + wantErr bool + }{ + { + name: "help flag shows command description", + args: append(subCommands, "--help"), + expectedOutputs: []string{"Assign strings (by hashcode) to an existing translation job", "hashcode", "target-locale"}, + unexpectedOutputs: []string{"DEBUG", "ERROR"}, + wantErr: false, + }, + { + name: "missing job argument is rejected", + args: append(subCommands, "--hashcode", "h1"), + expectedOutputs: []string{"accepts 1 arg(s)"}, + wantErr: true, + }, + { + name: "add strings to a job", + args: append(subCommands, "test-integration-job", "--hashcode", "ca51a04da69cf64dce022bb4f146c962"), + expectedOutputs: []string{"added"}, + unexpectedOutputs: []string{"DEBUG", "ERROR"}, + wantErr: false, + }, + { + name: "json output format contains field names", + args: []string{"jobs", "--output", "json", "strings", "add", "test-integration-job", "--hashcode", "ca51a04da69cf64dce022bb4f146c962"}, + expectedOutputs: []string{"action", "translationJobUid", "hashcodes"}, + unexpectedOutputs: []string{"DEBUG", "ERROR"}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testCmd := exec.Command("./smartling-cli", tt.args...) + testCmd.Dir = absDir + out, err := testCmd.CombinedOutput() + if !tt.wantErr && err != nil { + t.Fatalf("error: %v, output: %s", err, string(out)) + } + for _, expectedOutput := range tt.expectedOutputs { + if !strings.Contains(string(out), expectedOutput) { + t.Errorf("output: %s\nwithout expected: %s", string(out), expectedOutput) + } + } + for _, unexpectedOutput := range tt.unexpectedOutputs { + if strings.Contains(string(out), unexpectedOutput) { + t.Errorf("output: %s\nwith unexpected: %s", string(out), unexpectedOutput) + } + } + }) + } +} diff --git a/tests/cmd/jobs/strings/list/list_test.go b/tests/cmd/jobs/strings/list/list_test.go new file mode 100644 index 0000000..3388f0d --- /dev/null +++ b/tests/cmd/jobs/strings/list/list_test.go @@ -0,0 +1,65 @@ +package list + +import ( + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestJobsStringsList(t *testing.T) { + absDir, err := filepath.Abs("../../../bin/") + if err != nil { + t.Fatalf("Failed to get abs path: %v", err) + } + subCommands := []string{"jobs", "strings", "list"} + tests := []struct { + name string + args []string + expectedOutputs []string + unexpectedOutputs []string + wantErr bool + }{ + { + name: "help flag shows command description", + args: append(subCommands, "--help"), + expectedOutputs: []string{"List the strings", "target-locale", "limit"}, + unexpectedOutputs: []string{"DEBUG", "ERROR"}, + wantErr: false, + }, + { + name: "missing job argument is rejected", + args: subCommands, + expectedOutputs: []string{"accepts 1 arg(s)"}, + wantErr: true, + }, + { + name: "list strings as table", + args: []string{"jobs", "--output", "table", "strings", "list", "test-integration-job"}, + expectedOutputs: []string{"TARGET LOCALE ID", "HASHCODE"}, + unexpectedOutputs: []string{"DEBUG", "ERROR"}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testCmd := exec.Command("./smartling-cli", tt.args...) + testCmd.Dir = absDir + out, err := testCmd.CombinedOutput() + if !tt.wantErr && err != nil { + t.Fatalf("error: %v, output: %s", err, string(out)) + } + for _, expectedOutput := range tt.expectedOutputs { + if !strings.Contains(string(out), expectedOutput) { + t.Errorf("output: %s\nwithout expected: %s", string(out), expectedOutput) + } + } + for _, unexpectedOutput := range tt.unexpectedOutputs { + if strings.Contains(string(out), unexpectedOutput) { + t.Errorf("output: %s\nwith unexpected: %s", string(out), unexpectedOutput) + } + } + }) + } +} diff --git a/tests/cmd/jobs/strings/remove/remove_test.go b/tests/cmd/jobs/strings/remove/remove_test.go new file mode 100644 index 0000000..8a4a6a3 --- /dev/null +++ b/tests/cmd/jobs/strings/remove/remove_test.go @@ -0,0 +1,65 @@ +package remove + +import ( + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestJobsStringsRemove(t *testing.T) { + absDir, err := filepath.Abs("../../../bin/") + if err != nil { + t.Fatalf("Failed to get abs path: %v", err) + } + subCommands := []string{"jobs", "strings", "remove"} + tests := []struct { + name string + args []string + expectedOutputs []string + unexpectedOutputs []string + wantErr bool + }{ + { + name: "help flag shows command description", + args: append(subCommands, "--help"), + expectedOutputs: []string{"Detach strings (by hashcode) from an existing translation job", "hashcode", "locale"}, + unexpectedOutputs: []string{"DEBUG", "ERROR"}, + wantErr: false, + }, + { + name: "missing job argument is rejected", + args: append(subCommands, "--hashcode", "h1"), + expectedOutputs: []string{"accepts 1 arg(s)"}, + wantErr: true, + }, + { + name: "remove strings from a job", + args: append(subCommands, "test-integration-job", "--hashcode", "ca51a04da69cf64dce022bb4f146c962"), + expectedOutputs: []string{"removed"}, + unexpectedOutputs: []string{"DEBUG", "ERROR"}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testCmd := exec.Command("./smartling-cli", tt.args...) + testCmd.Dir = absDir + out, err := testCmd.CombinedOutput() + if !tt.wantErr && err != nil { + t.Fatalf("error: %v, output: %s", err, string(out)) + } + for _, expectedOutput := range tt.expectedOutputs { + if !strings.Contains(string(out), expectedOutput) { + t.Errorf("output: %s\nwithout expected: %s", string(out), expectedOutput) + } + } + for _, unexpectedOutput := range tt.unexpectedOutputs { + if strings.Contains(string(out), unexpectedOutput) { + t.Errorf("output: %s\nwith unexpected: %s", string(out), unexpectedOutput) + } + } + }) + } +} From 939a1b827afa5ee4f6304a4a004518e5bf9a57e9 Mon Sep 17 00:00:00 2001 From: az-smartling Date: Thu, 4 Jun 2026 22:52:53 +0200 Subject: [PATCH 2/6] CR changes --- services/jobs/strings/run_add.go | 2 +- services/jobs/strings/run_list.go | 3 ++- services/jobs/strings/run_list_test.go | 5 +++++ services/jobs/strings/run_remove.go | 2 +- services/jobs/strings/service.go | 4 +++- tests/cmd/jobs/strings/add/add_test.go | 4 ++-- tests/cmd/jobs/strings/list/list_test.go | 2 +- tests/cmd/jobs/strings/remove/remove_test.go | 2 +- 8 files changed, 16 insertions(+), 8 deletions(-) diff --git a/services/jobs/strings/run_add.go b/services/jobs/strings/run_add.go index 3f5b015..0059459 100644 --- a/services/jobs/strings/run_add.go +++ b/services/jobs/strings/run_add.go @@ -40,5 +40,5 @@ func (s service) RunAdd(ctx context.Context, params AddParams) (MutateOutput, er if err != nil { return MutateOutput{}, err } - return newMutateOutput("added", params.ProjectID, jobUID, params.Hashcodes, params.TargetLocaleIDs, res.SuccessCount, res.FailCount) + return newMutateOutput("added", params.ProjectID, jobUID, params.Hashcodes, params.TargetLocaleIDs, nil, res.SuccessCount, res.FailCount) } diff --git a/services/jobs/strings/run_list.go b/services/jobs/strings/run_list.go index 5f020a6..7e536a3 100644 --- a/services/jobs/strings/run_list.go +++ b/services/jobs/strings/run_list.go @@ -58,10 +58,11 @@ func (o ListOutput) SimpleLines() []string { if len(o.Items) == 0 { return []string{"No strings found."} } - lines := make([]string, 0, len(o.Items)) + lines := make([]string, 0, len(o.Items)+1) for _, it := range o.Items { lines = append(lines, fmt.Sprintf("%s %s", it.TargetLocaleID, it.Hashcode)) } + lines = append(lines, fmt.Sprintf("Showing %d of %d string(s). Use --limit/--offset to page.", len(o.Items), o.TotalCount)) return lines } diff --git a/services/jobs/strings/run_list_test.go b/services/jobs/strings/run_list_test.go index 8c76d0a..0536c7f 100644 --- a/services/jobs/strings/run_list_test.go +++ b/services/jobs/strings/run_list_test.go @@ -2,6 +2,7 @@ package jobstrings import ( "context" + "strings" "testing" jobsdkmocks "github.com/Smartling/smartling-cli/services/jobs/sdkmocks" @@ -52,6 +53,10 @@ func TestRunList(t *testing.T) { if got.Items[0].Hashcode != "h1" || got.Items[1].TargetLocaleID != "fr-FR" { t.Errorf("unexpected items: %+v", got.Items) } + lines := got.SimpleLines() + if !strings.Contains(lines[len(lines)-1], "Showing 2 of 2") { + t.Errorf("SimpleLines = %v, want trailing total line", lines) + } }, }, } diff --git a/services/jobs/strings/run_remove.go b/services/jobs/strings/run_remove.go index eb634f1..437f47c 100644 --- a/services/jobs/strings/run_remove.go +++ b/services/jobs/strings/run_remove.go @@ -38,5 +38,5 @@ func (s service) RunRemove(ctx context.Context, params RemoveParams) (MutateOutp if err != nil { return MutateOutput{}, err } - return newMutateOutput("removed", params.ProjectID, jobUID, params.Hashcodes, params.LocaleIDs, res.SuccessCount, res.FailCount) + return newMutateOutput("removed", params.ProjectID, jobUID, params.Hashcodes, nil, params.LocaleIDs, res.SuccessCount, res.FailCount) } diff --git a/services/jobs/strings/service.go b/services/jobs/strings/service.go index 0e60b74..3da8fe9 100644 --- a/services/jobs/strings/service.go +++ b/services/jobs/strings/service.go @@ -41,6 +41,7 @@ type MutateOutput struct { ProjectUID string `json:"projectUid"` TranslationJobUID string `json:"translationJobUid"` Hashcodes []string `json:"hashcodes"` + TargetLocaleIDs []string `json:"targetLocaleIds,omitempty"` LocaleIDs []string `json:"localeIds,omitempty"` SuccessCount int `json:"successCount"` FailCount int `json:"failCount"` @@ -48,12 +49,13 @@ type MutateOutput struct { JSON []byte `json:"-"` } -func newMutateOutput(action, projectUID, jobUID string, hashcodes, localeIDs []string, successCount, failCount int) (MutateOutput, error) { +func newMutateOutput(action, projectUID, jobUID string, hashcodes, targetLocaleIDs, localeIDs []string, successCount, failCount int) (MutateOutput, error) { o := MutateOutput{ Action: action, ProjectUID: projectUID, TranslationJobUID: jobUID, Hashcodes: hashcodes, + TargetLocaleIDs: targetLocaleIDs, LocaleIDs: localeIDs, SuccessCount: successCount, FailCount: failCount, diff --git a/tests/cmd/jobs/strings/add/add_test.go b/tests/cmd/jobs/strings/add/add_test.go index 251569f..8eead7e 100644 --- a/tests/cmd/jobs/strings/add/add_test.go +++ b/tests/cmd/jobs/strings/add/add_test.go @@ -35,14 +35,14 @@ func TestJobsStringsAdd(t *testing.T) { }, { name: "add strings to a job", - args: append(subCommands, "test-integration-job", "--hashcode", "ca51a04da69cf64dce022bb4f146c962"), + args: append(subCommands, "CLI uploads", "--hashcode", "ca51a04da69cf64dce022bb4f146c962"), expectedOutputs: []string{"added"}, unexpectedOutputs: []string{"DEBUG", "ERROR"}, wantErr: false, }, { name: "json output format contains field names", - args: []string{"jobs", "--output", "json", "strings", "add", "test-integration-job", "--hashcode", "ca51a04da69cf64dce022bb4f146c962"}, + args: []string{"jobs", "--output", "json", "strings", "add", "CLI uploads", "--hashcode", "ca51a04da69cf64dce022bb4f146c962"}, expectedOutputs: []string{"action", "translationJobUid", "hashcodes"}, unexpectedOutputs: []string{"DEBUG", "ERROR"}, wantErr: false, diff --git a/tests/cmd/jobs/strings/list/list_test.go b/tests/cmd/jobs/strings/list/list_test.go index 3388f0d..a04d656 100644 --- a/tests/cmd/jobs/strings/list/list_test.go +++ b/tests/cmd/jobs/strings/list/list_test.go @@ -35,7 +35,7 @@ func TestJobsStringsList(t *testing.T) { }, { name: "list strings as table", - args: []string{"jobs", "--output", "table", "strings", "list", "test-integration-job"}, + args: []string{"jobs", "--output", "table", "strings", "list", "CLI uploads"}, expectedOutputs: []string{"TARGET LOCALE ID", "HASHCODE"}, unexpectedOutputs: []string{"DEBUG", "ERROR"}, wantErr: false, diff --git a/tests/cmd/jobs/strings/remove/remove_test.go b/tests/cmd/jobs/strings/remove/remove_test.go index 8a4a6a3..35e7590 100644 --- a/tests/cmd/jobs/strings/remove/remove_test.go +++ b/tests/cmd/jobs/strings/remove/remove_test.go @@ -35,7 +35,7 @@ func TestJobsStringsRemove(t *testing.T) { }, { name: "remove strings from a job", - args: append(subCommands, "test-integration-job", "--hashcode", "ca51a04da69cf64dce022bb4f146c962"), + args: append(subCommands, "CLI uploads", "--hashcode", "ca51a04da69cf64dce022bb4f146c962"), expectedOutputs: []string{"removed"}, unexpectedOutputs: []string{"DEBUG", "ERROR"}, wantErr: false, From 9d22050483d20f32229848eea52bf0373e505a3f Mon Sep 17 00:00:00 2001 From: az-smartling Date: Thu, 4 Jun 2026 22:53:22 +0200 Subject: [PATCH 3/6] docs --- docs/smartling-cli_jobs.md | 1 + docs/smartling-cli_jobs_strings.md | 71 +++++++++++++++++++++++ docs/smartling-cli_jobs_strings_add.md | 67 +++++++++++++++++++++ docs/smartling-cli_jobs_strings_list.md | 67 +++++++++++++++++++++ docs/smartling-cli_jobs_strings_remove.md | 66 +++++++++++++++++++++ 5 files changed, 272 insertions(+) create mode 100644 docs/smartling-cli_jobs_strings.md create mode 100644 docs/smartling-cli_jobs_strings_add.md create mode 100644 docs/smartling-cli_jobs_strings_list.md create mode 100644 docs/smartling-cli_jobs_strings_remove.md diff --git a/docs/smartling-cli_jobs.md b/docs/smartling-cli_jobs.md index 49f993f..b54d9ce 100644 --- a/docs/smartling-cli_jobs.md +++ b/docs/smartling-cli_jobs.md @@ -75,6 +75,7 @@ Available options: * [smartling-cli jobs list](smartling-cli_jobs_list.md) - List translation jobs in a project or account. * [smartling-cli jobs locales](smartling-cli_jobs_locales.md) - Manage target locales on a translation job. * [smartling-cli jobs progress](smartling-cli_jobs_progress.md) - Track translation progress for a specific job. +* [smartling-cli jobs strings](smartling-cli_jobs_strings.md) - Manage strings on a translation job. * [smartling-cli jobs view](smartling-cli_jobs_view.md) - Show full details of a translation job. ###### Auto generated by spf13/cobra on 4-Jun-2026 diff --git a/docs/smartling-cli_jobs_strings.md b/docs/smartling-cli_jobs_strings.md new file mode 100644 index 0000000..22f57c1 --- /dev/null +++ b/docs/smartling-cli_jobs_strings.md @@ -0,0 +1,71 @@ +## smartling-cli jobs strings + +Manage strings on a translation job. + +### Synopsis + +Add, remove, or list the strings on an existing translation job. + +Strings are identified by hashcode. Use these commands to assign strings to a +job, detach them, or inspect which strings a job currently contains. + +### Examples + +``` + +# Add strings to a job + + smartling-cli jobs strings add --hashcode + +# Remove strings from a job + + smartling-cli jobs strings remove --hashcode + +# List a job's strings + + smartling-cli jobs strings list + + +``` + +### Options + +``` + -h, --help help for strings +``` + +### Options inherited from parent commands + +``` + -a, --account string Account ID to operate on. + This option overrides config value "account_id". + -c, --config string Config file in YAML format. + By default CLI will look for file named + "smartling.yml" in current directory and in all + intermediate parents, emulating git behavior. + -k, --insecure Skip HTTPS certificate validation. + --operation-directory string Sets directory to operate on, usually, to store or to + read files. Depends on command. (default ".") + --output string Output format: table, json, simple (default "simple") + -p, --project string Project ID to operate on. + This option overrides config value "project_id". + --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. + --user string User ID which will be used for authentication. + This option overrides config value "user_id". + -v, --verbose count Verbose logging +``` + +### SEE ALSO + +* [smartling-cli jobs](smartling-cli_jobs.md) - Manage translation jobs and monitor their progress. +* [smartling-cli jobs strings add](smartling-cli_jobs_strings_add.md) - Add strings to a translation job. +* [smartling-cli jobs strings list](smartling-cli_jobs_strings_list.md) - List the strings on a translation job. +* [smartling-cli jobs strings remove](smartling-cli_jobs_strings_remove.md) - Remove strings from a translation job. + +###### Auto generated by spf13/cobra on 4-Jun-2026 diff --git a/docs/smartling-cli_jobs_strings_add.md b/docs/smartling-cli_jobs_strings_add.md new file mode 100644 index 0000000..f875a71 --- /dev/null +++ b/docs/smartling-cli_jobs_strings_add.md @@ -0,0 +1,67 @@ +## smartling-cli jobs strings add + +Add strings to a translation job. + +### Synopsis + +Assign strings (by hashcode) to an existing translation job, identified by UID or name. + +``` +smartling-cli jobs strings add [flags] +``` + +### Examples + +``` + +# Add two strings to a job + + smartling-cli jobs strings add aabbccdd1122 --hashcode h1 --hashcode h2 + +# Add a string for specific locales, moving it if it already belongs to another job + + smartling-cli jobs strings add "Website Q1 2026" --hashcode h1 --target-locale fr-FR --move-enabled + +``` + +### Options + +``` + --hashcode stringArray String hashcode to add (repeatable, required). + -h, --help help for add + --move-enabled Move the string into this job if it already belongs to another job for a locale. + --target-locale stringArray Target locale to add the strings to (repeatable; default all job locales). +``` + +### Options inherited from parent commands + +``` + -a, --account string Account ID to operate on. + This option overrides config value "account_id". + -c, --config string Config file in YAML format. + By default CLI will look for file named + "smartling.yml" in current directory and in all + intermediate parents, emulating git behavior. + -k, --insecure Skip HTTPS certificate validation. + --operation-directory string Sets directory to operate on, usually, to store or to + read files. Depends on command. (default ".") + --output string Output format: table, json, simple (default "simple") + -p, --project string Project ID to operate on. + This option overrides config value "project_id". + --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. + --user string User ID which will be used for authentication. + This option overrides config value "user_id". + -v, --verbose count Verbose logging +``` + +### SEE ALSO + +* [smartling-cli jobs strings](smartling-cli_jobs_strings.md) - Manage strings on a translation job. + +###### Auto generated by spf13/cobra on 4-Jun-2026 diff --git a/docs/smartling-cli_jobs_strings_list.md b/docs/smartling-cli_jobs_strings_list.md new file mode 100644 index 0000000..e8fde59 --- /dev/null +++ b/docs/smartling-cli_jobs_strings_list.md @@ -0,0 +1,67 @@ +## smartling-cli jobs strings list + +List the strings on a translation job. + +### Synopsis + +List the strings (by hashcode and target locale) assigned to a translation job, identified by UID or name. + +``` +smartling-cli jobs strings list [flags] +``` + +### Examples + +``` + +# List a job's strings + + smartling-cli jobs strings list aabbccdd1122 + +# List strings for one locale as a table + + smartling-cli jobs strings list "Website Q1 2026" --target-locale fr-FR --output table + +``` + +### Options + +``` + -h, --help help for list + --limit uint32 Maximum number of strings to return. + --offset uint32 Number of strings to skip. + --target-locale string Filter strings by target locale. +``` + +### Options inherited from parent commands + +``` + -a, --account string Account ID to operate on. + This option overrides config value "account_id". + -c, --config string Config file in YAML format. + By default CLI will look for file named + "smartling.yml" in current directory and in all + intermediate parents, emulating git behavior. + -k, --insecure Skip HTTPS certificate validation. + --operation-directory string Sets directory to operate on, usually, to store or to + read files. Depends on command. (default ".") + --output string Output format: table, json, simple (default "simple") + -p, --project string Project ID to operate on. + This option overrides config value "project_id". + --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. + --user string User ID which will be used for authentication. + This option overrides config value "user_id". + -v, --verbose count Verbose logging +``` + +### SEE ALSO + +* [smartling-cli jobs strings](smartling-cli_jobs_strings.md) - Manage strings on a translation job. + +###### Auto generated by spf13/cobra on 4-Jun-2026 diff --git a/docs/smartling-cli_jobs_strings_remove.md b/docs/smartling-cli_jobs_strings_remove.md new file mode 100644 index 0000000..308c7bc --- /dev/null +++ b/docs/smartling-cli_jobs_strings_remove.md @@ -0,0 +1,66 @@ +## smartling-cli jobs strings remove + +Remove strings from a translation job. + +### Synopsis + +Detach strings (by hashcode) from an existing translation job, identified by UID or name. + +``` +smartling-cli jobs strings remove [flags] +``` + +### Examples + +``` + +# Remove two strings from a job + + smartling-cli jobs strings remove aabbccdd1122 --hashcode h1 --hashcode h2 + +# Remove a string from specific locales only + + smartling-cli jobs strings remove "Website Q1 2026" --hashcode h1 --locale fr-FR + +``` + +### Options + +``` + --hashcode stringArray String hashcode to remove (repeatable, required). + -h, --help help for remove + --locale stringArray Locale to remove the strings from (repeatable; default all job locales). +``` + +### Options inherited from parent commands + +``` + -a, --account string Account ID to operate on. + This option overrides config value "account_id". + -c, --config string Config file in YAML format. + By default CLI will look for file named + "smartling.yml" in current directory and in all + intermediate parents, emulating git behavior. + -k, --insecure Skip HTTPS certificate validation. + --operation-directory string Sets directory to operate on, usually, to store or to + read files. Depends on command. (default ".") + --output string Output format: table, json, simple (default "simple") + -p, --project string Project ID to operate on. + This option overrides config value "project_id". + --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. + --user string User ID which will be used for authentication. + This option overrides config value "user_id". + -v, --verbose count Verbose logging +``` + +### SEE ALSO + +* [smartling-cli jobs strings](smartling-cli_jobs_strings.md) - Manage strings on a translation job. + +###### Auto generated by spf13/cobra on 4-Jun-2026 From 42b4be2e01d00f1b30e5f589fb698d47493a60b6 Mon Sep 17 00:00:00 2001 From: az-smartling Date: Thu, 4 Jun 2026 22:54:35 +0200 Subject: [PATCH 4/6] updated api sdk --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 07fe558..c463281 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.2 require ( dario.cat/mergo v1.0.2 - github.com/Smartling/api-sdk-go v0.0.0-20260604130112-e8821925c880 + github.com/Smartling/api-sdk-go v0.0.0-20260604182815-19afb8cad3b8 github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.6 github.com/charmbracelet/lipgloss v1.1.0 diff --git a/go.sum b/go.sum index b6dce8a..a410267 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/Smartling/api-sdk-go v0.0.0-20260604130112-e8821925c880 h1:jclVi49ub1j3mx5c4qoxoZ4vBq34Qj5J+HgUfYwuBHY= -github.com/Smartling/api-sdk-go v0.0.0-20260604130112-e8821925c880/go.mod h1:gmlyFNCAACW83HRMrwncSirybaSPOhtq8eUtf6XXsN8= +github.com/Smartling/api-sdk-go v0.0.0-20260604182815-19afb8cad3b8 h1:wMz2bE6IG84SonPdoKrFEsGIfCTpSKy+yzgUhFpxVP0= +github.com/Smartling/api-sdk-go v0.0.0-20260604182815-19afb8cad3b8/go.mod h1:gmlyFNCAACW83HRMrwncSirybaSPOhtq8eUtf6XXsN8= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= From 87fd1a05aa91da02ed37804bfdf82e2e2722a8d9 Mon Sep 17 00:00:00 2001 From: az-smartling Date: Fri, 5 Jun 2026 08:53:36 +0200 Subject: [PATCH 5/6] CR changes --- cmd/jobs/strings/add/cmd_add.go | 6 ++++++ cmd/jobs/strings/remove/cmd_remove.go | 6 ++++++ docs/smartling-cli.md | 2 +- docs/smartling-cli_build.md | 2 +- docs/smartling-cli_completion.md | 2 +- docs/smartling-cli_completion_bash.md | 2 +- docs/smartling-cli_completion_fish.md | 2 +- docs/smartling-cli_completion_powershell.md | 2 +- docs/smartling-cli_completion_zsh.md | 2 +- docs/smartling-cli_files.md | 2 +- docs/smartling-cli_files_delete.md | 2 +- docs/smartling-cli_files_import.md | 2 +- docs/smartling-cli_files_list.md | 2 +- docs/smartling-cli_files_pull.md | 2 +- docs/smartling-cli_files_push.md | 2 +- docs/smartling-cli_files_rename.md | 2 +- docs/smartling-cli_files_status.md | 2 +- docs/smartling-cli_glossaries.md | 2 +- docs/smartling-cli_glossaries_create.md | 2 +- docs/smartling-cli_glossaries_export.md | 2 +- docs/smartling-cli_glossaries_import.md | 2 +- docs/smartling-cli_glossaries_list.md | 2 +- docs/smartling-cli_init.md | 2 +- docs/smartling-cli_jobs.md | 2 +- docs/smartling-cli_jobs_files.md | 2 +- docs/smartling-cli_jobs_list.md | 2 +- docs/smartling-cli_jobs_locales.md | 2 +- docs/smartling-cli_jobs_locales_add.md | 2 +- docs/smartling-cli_jobs_locales_remove.md | 2 +- docs/smartling-cli_jobs_progress.md | 2 +- docs/smartling-cli_jobs_strings.md | 2 +- docs/smartling-cli_jobs_strings_add.md | 2 +- docs/smartling-cli_jobs_strings_list.md | 2 +- docs/smartling-cli_jobs_strings_remove.md | 2 +- docs/smartling-cli_jobs_view.md | 2 +- docs/smartling-cli_mt.md | 2 +- docs/smartling-cli_mt_detect.md | 2 +- docs/smartling-cli_mt_translate.md | 2 +- docs/smartling-cli_projects.md | 2 +- docs/smartling-cli_projects_info.md | 2 +- docs/smartling-cli_projects_list.md | 2 +- docs/smartling-cli_projects_locales.md | 2 +- services/jobs/strings/run_list.go | 9 ++++++--- 43 files changed, 58 insertions(+), 43 deletions(-) diff --git a/cmd/jobs/strings/add/cmd_add.go b/cmd/jobs/strings/add/cmd_add.go index 5ea02da..5a620e7 100644 --- a/cmd/jobs/strings/add/cmd_add.go +++ b/cmd/jobs/strings/add/cmd_add.go @@ -2,9 +2,11 @@ package add import ( "fmt" + "os" stringscmd "github.com/Smartling/smartling-cli/cmd/jobs/strings" "github.com/Smartling/smartling-cli/output" + "github.com/Smartling/smartling-cli/services/helpers/rlog" "github.com/spf13/cobra" ) @@ -54,6 +56,10 @@ func NewJobStringsAddCmd(initializer stringscmd.SrvInitializer) *cobra.Command { addCmd.Flags().StringArrayVar(&hashcodes, hashcodeFlag, nil, "String hashcode to add (repeatable, required).") addCmd.Flags().StringArrayVar(&targetLocales, targetLocaleFlag, nil, "Target locale to add the strings to (repeatable; default all job locales).") addCmd.Flags().BoolVar(&moveEnabled, moveEnabledFlag, false, "Move the string into this job if it already belongs to another job for a locale.") + if err := addCmd.MarkFlagRequired(hashcodeFlag); err != nil { + rlog.Errorf("failed to mark --%s required: %s", hashcodeFlag, err) + os.Exit(1) + } return addCmd } diff --git a/cmd/jobs/strings/remove/cmd_remove.go b/cmd/jobs/strings/remove/cmd_remove.go index 5be628e..5a96d8e 100644 --- a/cmd/jobs/strings/remove/cmd_remove.go +++ b/cmd/jobs/strings/remove/cmd_remove.go @@ -2,9 +2,11 @@ package remove import ( "fmt" + "os" stringscmd "github.com/Smartling/smartling-cli/cmd/jobs/strings" "github.com/Smartling/smartling-cli/output" + "github.com/Smartling/smartling-cli/services/helpers/rlog" "github.com/spf13/cobra" ) @@ -51,6 +53,10 @@ func NewJobStringsRemoveCmd(initializer stringscmd.SrvInitializer) *cobra.Comman removeCmd.Flags().StringArrayVar(&hashcodes, hashcodeFlag, nil, "String hashcode to remove (repeatable, required).") removeCmd.Flags().StringArrayVar(&localeIDs, localeFlag, nil, "Locale to remove the strings from (repeatable; default all job locales).") + if err := removeCmd.MarkFlagRequired(hashcodeFlag); err != nil { + rlog.Errorf("failed to mark --%s required: %s", hashcodeFlag, err) + os.Exit(1) + } return removeCmd } diff --git a/docs/smartling-cli.md b/docs/smartling-cli.md index 7f3d9a7..25d3074 100644 --- a/docs/smartling-cli.md +++ b/docs/smartling-cli.md @@ -49,4 +49,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 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_build.md b/docs/smartling-cli_build.md index 3dea673..e7e7a16 100644 --- a/docs/smartling-cli_build.md +++ b/docs/smartling-cli_build.md @@ -42,4 +42,4 @@ smartling-cli build [flags] * [smartling-cli](smartling-cli.md) - Manage translation files using Smartling CLI. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_completion.md b/docs/smartling-cli_completion.md index 1758200..5a21851 100644 --- a/docs/smartling-cli_completion.md +++ b/docs/smartling-cli_completion.md @@ -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 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_completion_bash.md b/docs/smartling-cli_completion_bash.md index 32b1767..a83b2d7 100644 --- a/docs/smartling-cli_completion_bash.md +++ b/docs/smartling-cli_completion_bash.md @@ -67,4 +67,4 @@ smartling-cli completion bash * [smartling-cli completion](smartling-cli_completion.md) - Generate the autocompletion script for the specified shell -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_completion_fish.md b/docs/smartling-cli_completion_fish.md index d9d2cb6..9202624 100644 --- a/docs/smartling-cli_completion_fish.md +++ b/docs/smartling-cli_completion_fish.md @@ -58,4 +58,4 @@ smartling-cli completion fish [flags] * [smartling-cli completion](smartling-cli_completion.md) - Generate the autocompletion script for the specified shell -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_completion_powershell.md b/docs/smartling-cli_completion_powershell.md index ecc5273..b9e1ece 100644 --- a/docs/smartling-cli_completion_powershell.md +++ b/docs/smartling-cli_completion_powershell.md @@ -55,4 +55,4 @@ smartling-cli completion powershell [flags] * [smartling-cli completion](smartling-cli_completion.md) - Generate the autocompletion script for the specified shell -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_completion_zsh.md b/docs/smartling-cli_completion_zsh.md index b6a215a..7634228 100644 --- a/docs/smartling-cli_completion_zsh.md +++ b/docs/smartling-cli_completion_zsh.md @@ -69,4 +69,4 @@ smartling-cli completion zsh [flags] * [smartling-cli completion](smartling-cli_completion.md) - Generate the autocompletion script for the specified shell -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_files.md b/docs/smartling-cli_files.md index 1ff2b8c..b4bfa12 100644 --- a/docs/smartling-cli_files.md +++ b/docs/smartling-cli_files.md @@ -64,4 +64,4 @@ smartling-cli files [flags] * [smartling-cli files rename](smartling-cli_files_rename.md) - Renames given file by old URI into new URI. * [smartling-cli files status](smartling-cli_files_status.md) - Shows file translation status. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_files_delete.md b/docs/smartling-cli_files_delete.md index 008350a..0c19066 100644 --- a/docs/smartling-cli_files_delete.md +++ b/docs/smartling-cli_files_delete.md @@ -76,4 +76,4 @@ smartling-cli files delete [flags] * [smartling-cli files](smartling-cli_files.md) - Used to access various files sub-commands. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_files_import.md b/docs/smartling-cli_files_import.md index 0621a4d..93dd183 100644 --- a/docs/smartling-cli_files_import.md +++ b/docs/smartling-cli_files_import.md @@ -81,4 +81,4 @@ smartling-cli files import [flags] * [smartling-cli files](smartling-cli_files.md) - Used to access various files sub-commands. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_files_list.md b/docs/smartling-cli_files_list.md index 05f1f2f..d56454b 100644 --- a/docs/smartling-cli_files_list.md +++ b/docs/smartling-cli_files_list.md @@ -121,4 +121,4 @@ smartling-cli files list [flags] * [smartling-cli files](smartling-cli_files.md) - Used to access various files sub-commands. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_files_pull.md b/docs/smartling-cli_files_pull.md index 9f788ed..bd5a9b4 100644 --- a/docs/smartling-cli_files_pull.md +++ b/docs/smartling-cli_files_pull.md @@ -195,4 +195,4 @@ smartling-cli files pull [flags] * [smartling-cli files](smartling-cli_files.md) - Used to access various files sub-commands. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_files_push.md b/docs/smartling-cli_files_push.md index 7c95867..8761818 100644 --- a/docs/smartling-cli_files_push.md +++ b/docs/smartling-cli_files_push.md @@ -151,4 +151,4 @@ smartling-cli files push --job [--authorize] [--locale < * [smartling-cli files](smartling-cli_files.md) - Used to access various files sub-commands. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_files_rename.md b/docs/smartling-cli_files_rename.md index d81035e..bc14022 100644 --- a/docs/smartling-cli_files_rename.md +++ b/docs/smartling-cli_files_rename.md @@ -62,4 +62,4 @@ smartling-cli files rename [flags] * [smartling-cli files](smartling-cli_files.md) - Used to access various files sub-commands. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_files_status.md b/docs/smartling-cli_files_status.md index c527c15..bd28ef0 100644 --- a/docs/smartling-cli_files_status.md +++ b/docs/smartling-cli_files_status.md @@ -114,4 +114,4 @@ smartling-cli files status [flags] * [smartling-cli files](smartling-cli_files.md) - Used to access various files sub-commands. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_glossaries.md b/docs/smartling-cli_glossaries.md index bdd8395..20a67f1 100644 --- a/docs/smartling-cli_glossaries.md +++ b/docs/smartling-cli_glossaries.md @@ -53,4 +53,4 @@ consistent understanding of your terminology across every locale. * [smartling-cli glossaries import](smartling-cli_glossaries_import.md) - Glossary import process * [smartling-cli glossaries list](smartling-cli_glossaries_list.md) - List glossaries in the current account -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_glossaries_create.md b/docs/smartling-cli_glossaries_create.md index cef587c..e08bdfc 100644 --- a/docs/smartling-cli_glossaries_create.md +++ b/docs/smartling-cli_glossaries_create.md @@ -68,4 +68,4 @@ smartling-cli glossaries create [flags] * [smartling-cli glossaries](smartling-cli_glossaries.md) - Manage Smartling glossaries -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_glossaries_export.md b/docs/smartling-cli_glossaries_export.md index 61e0f6a..0f0066c 100644 --- a/docs/smartling-cli_glossaries_export.md +++ b/docs/smartling-cli_glossaries_export.md @@ -110,4 +110,4 @@ smartling-cli glossaries export [outFile] [flags] * [smartling-cli glossaries](smartling-cli_glossaries.md) - Manage Smartling glossaries -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_glossaries_import.md b/docs/smartling-cli_glossaries_import.md index d21617f..057c2af 100644 --- a/docs/smartling-cli_glossaries_import.md +++ b/docs/smartling-cli_glossaries_import.md @@ -75,4 +75,4 @@ smartling-cli glossaries import [flags] * [smartling-cli glossaries](smartling-cli_glossaries.md) - Manage Smartling glossaries -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_glossaries_list.md b/docs/smartling-cli_glossaries_list.md index c48f741..c3e5c46 100644 --- a/docs/smartling-cli_glossaries_list.md +++ b/docs/smartling-cli_glossaries_list.md @@ -68,4 +68,4 @@ smartling-cli glossaries list [flags] * [smartling-cli glossaries](smartling-cli_glossaries.md) - Manage Smartling glossaries -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_init.md b/docs/smartling-cli_init.md index f12665c..e4a0cd9 100644 --- a/docs/smartling-cli_init.md +++ b/docs/smartling-cli_init.md @@ -106,4 +106,4 @@ smartling-cli init [flags] * [smartling-cli](smartling-cli.md) - Manage translation files using Smartling CLI. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_jobs.md b/docs/smartling-cli_jobs.md index b54d9ce..6f20f83 100644 --- a/docs/smartling-cli_jobs.md +++ b/docs/smartling-cli_jobs.md @@ -78,4 +78,4 @@ Available options: * [smartling-cli jobs strings](smartling-cli_jobs_strings.md) - Manage strings on a translation job. * [smartling-cli jobs view](smartling-cli_jobs_view.md) - Show full details of a translation job. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_jobs_files.md b/docs/smartling-cli_jobs_files.md index eb59acb..bf81b59 100644 --- a/docs/smartling-cli_jobs_files.md +++ b/docs/smartling-cli_jobs_files.md @@ -63,4 +63,4 @@ smartling-cli jobs files [flags] * [smartling-cli jobs](smartling-cli_jobs.md) - Manage translation jobs and monitor their progress. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_jobs_list.md b/docs/smartling-cli_jobs_list.md index 347bfb4..2f05b78 100644 --- a/docs/smartling-cli_jobs_list.md +++ b/docs/smartling-cli_jobs_list.md @@ -84,4 +84,4 @@ smartling-cli jobs list [flags] * [smartling-cli jobs](smartling-cli_jobs.md) - Manage translation jobs and monitor their progress. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_jobs_locales.md b/docs/smartling-cli_jobs_locales.md index 81a98d8..4c81e39 100644 --- a/docs/smartling-cli_jobs_locales.md +++ b/docs/smartling-cli_jobs_locales.md @@ -63,4 +63,4 @@ Use these commands to attach a locale to a job or detach one from it. * [smartling-cli jobs locales add](smartling-cli_jobs_locales_add.md) - Add a target locale to a translation job. * [smartling-cli jobs locales remove](smartling-cli_jobs_locales_remove.md) - Remove a target locale from a translation job. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_jobs_locales_add.md b/docs/smartling-cli_jobs_locales_add.md index c9806a2..3a65183 100644 --- a/docs/smartling-cli_jobs_locales_add.md +++ b/docs/smartling-cli_jobs_locales_add.md @@ -61,4 +61,4 @@ smartling-cli jobs locales add [flags] * [smartling-cli jobs](smartling-cli_jobs.md) - Manage translation jobs and monitor their progress. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_jobs_strings.md b/docs/smartling-cli_jobs_strings.md index 22f57c1..a5af990 100644 --- a/docs/smartling-cli_jobs_strings.md +++ b/docs/smartling-cli_jobs_strings.md @@ -68,4 +68,4 @@ job, detach them, or inspect which strings a job currently contains. * [smartling-cli jobs strings list](smartling-cli_jobs_strings_list.md) - List the strings on a translation job. * [smartling-cli jobs strings remove](smartling-cli_jobs_strings_remove.md) - Remove strings from a translation job. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_jobs_strings_add.md b/docs/smartling-cli_jobs_strings_add.md index f875a71..2bfd9eb 100644 --- a/docs/smartling-cli_jobs_strings_add.md +++ b/docs/smartling-cli_jobs_strings_add.md @@ -64,4 +64,4 @@ smartling-cli jobs strings add [flags] * [smartling-cli jobs strings](smartling-cli_jobs_strings.md) - Manage strings on a translation job. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_jobs_strings_list.md b/docs/smartling-cli_jobs_strings_list.md index e8fde59..bc5e245 100644 --- a/docs/smartling-cli_jobs_strings_list.md +++ b/docs/smartling-cli_jobs_strings_list.md @@ -64,4 +64,4 @@ smartling-cli jobs strings list [flags] * [smartling-cli jobs strings](smartling-cli_jobs_strings.md) - Manage strings on a translation job. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_jobs_strings_remove.md b/docs/smartling-cli_jobs_strings_remove.md index 308c7bc..f3c06c2 100644 --- a/docs/smartling-cli_jobs_strings_remove.md +++ b/docs/smartling-cli_jobs_strings_remove.md @@ -63,4 +63,4 @@ smartling-cli jobs strings remove [flags] * [smartling-cli jobs strings](smartling-cli_jobs_strings.md) - Manage strings on a translation job. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_jobs_view.md b/docs/smartling-cli_jobs_view.md index de393fb..1b454a0 100644 --- a/docs/smartling-cli_jobs_view.md +++ b/docs/smartling-cli_jobs_view.md @@ -61,4 +61,4 @@ smartling-cli jobs view [flags] * [smartling-cli jobs](smartling-cli_jobs.md) - Manage translation jobs and monitor their progress. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_mt.md b/docs/smartling-cli_mt.md index 421c441..f5c0ced 100644 --- a/docs/smartling-cli_mt.md +++ b/docs/smartling-cli_mt.md @@ -50,4 +50,4 @@ smartling-cli mt [flags] * [smartling-cli mt detect](smartling-cli_mt_detect.md) - Detect the source language of files using Smartling's File MT API. * [smartling-cli mt translate](smartling-cli_mt_translate.md) - Translate files using Smartling's File Machine Translation API. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_mt_detect.md b/docs/smartling-cli_mt_detect.md index 68602e8..a0e8d53 100644 --- a/docs/smartling-cli_mt_detect.md +++ b/docs/smartling-cli_mt_detect.md @@ -69,4 +69,4 @@ smartling-cli mt detect [flags] * [smartling-cli mt](smartling-cli_mt.md) - File Machine Translations -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_mt_translate.md b/docs/smartling-cli_mt_translate.md index f0dac98..6123f85 100644 --- a/docs/smartling-cli_mt_translate.md +++ b/docs/smartling-cli_mt_translate.md @@ -83,4 +83,4 @@ smartling-cli mt translate [flags] * [smartling-cli mt](smartling-cli_mt.md) - File Machine Translations -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_projects.md b/docs/smartling-cli_projects.md index 481af1a..54ff2b7 100644 --- a/docs/smartling-cli_projects.md +++ b/docs/smartling-cli_projects.md @@ -49,4 +49,4 @@ smartling-cli projects [flags] * [smartling-cli projects list](smartling-cli_projects_list.md) - Lists projects for current account. * [smartling-cli projects locales](smartling-cli_projects_locales.md) - Display list of target locales. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_projects_info.md b/docs/smartling-cli_projects_info.md index 30647f5..4c0a338 100644 --- a/docs/smartling-cli_projects_info.md +++ b/docs/smartling-cli_projects_info.md @@ -72,4 +72,4 @@ smartling-cli projects info [flags] * [smartling-cli projects](smartling-cli_projects.md) - Used to access various projects sub-commands. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_projects_list.md b/docs/smartling-cli_projects_list.md index df77ec7..f7836a5 100644 --- a/docs/smartling-cli_projects_list.md +++ b/docs/smartling-cli_projects_list.md @@ -85,4 +85,4 @@ smartling-cli projects list [flags] * [smartling-cli projects](smartling-cli_projects.md) - Used to access various projects sub-commands. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/docs/smartling-cli_projects_locales.md b/docs/smartling-cli_projects_locales.md index 6af191a..3b42de0 100644 --- a/docs/smartling-cli_projects_locales.md +++ b/docs/smartling-cli_projects_locales.md @@ -101,4 +101,4 @@ smartling-cli projects locales [flags] * [smartling-cli projects](smartling-cli_projects.md) - Used to access various projects sub-commands. -###### Auto generated by spf13/cobra on 4-Jun-2026 +###### Auto generated by spf13/cobra on 5-Jun-2026 diff --git a/services/jobs/strings/run_list.go b/services/jobs/strings/run_list.go index 7e536a3..857beff 100644 --- a/services/jobs/strings/run_list.go +++ b/services/jobs/strings/run_list.go @@ -39,9 +39,12 @@ type ListOutput struct { } func newListOutput(resp api.ListResponse) (ListOutput, error) { - o := ListOutput{TotalCount: resp.TotalCount} - for _, it := range resp.Items { - o.Items = append(o.Items, Item{TargetLocaleID: it.TargetLocaleID, Hashcode: it.Hashcode}) + o := ListOutput{ + Items: make([]Item, len(resp.Items)), + TotalCount: resp.TotalCount, + } + for i, item := range resp.Items { + o.Items[i] = Item{TargetLocaleID: item.TargetLocaleID, Hashcode: item.Hashcode} } var err error if o.JSON, err = json.Marshal(o); err != nil { From cf1cbab149535876427b3eb8957935707531fa1b Mon Sep 17 00:00:00 2001 From: az-smartling Date: Fri, 5 Jun 2026 12:34:36 +0200 Subject: [PATCH 6/6] updated API SDK --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c463281..eafff1a 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.2 require ( dario.cat/mergo v1.0.2 - github.com/Smartling/api-sdk-go v0.0.0-20260604182815-19afb8cad3b8 + github.com/Smartling/api-sdk-go v0.0.0-20260605103240-2d3189bf73bf github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.6 github.com/charmbracelet/lipgloss v1.1.0 diff --git a/go.sum b/go.sum index a410267..62a522e 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/Smartling/api-sdk-go v0.0.0-20260604182815-19afb8cad3b8 h1:wMz2bE6IG84SonPdoKrFEsGIfCTpSKy+yzgUhFpxVP0= -github.com/Smartling/api-sdk-go v0.0.0-20260604182815-19afb8cad3b8/go.mod h1:gmlyFNCAACW83HRMrwncSirybaSPOhtq8eUtf6XXsN8= +github.com/Smartling/api-sdk-go v0.0.0-20260605103240-2d3189bf73bf h1:6sSkzpjdcfnt4v11N5/PSCy9ZFzxrbsCu0Z6O3nI2cg= +github.com/Smartling/api-sdk-go v0.0.0-20260605103240-2d3189bf73bf/go.mod h1:gmlyFNCAACW83HRMrwncSirybaSPOhtq8eUtf6XXsN8= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=