Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

This is the Smartling CLI tool - a command-line interface for managing translation files through the Smartling platform. The CLI provides commands for file operations (push, pull, list, status, rename, delete, import), project management, and machine translation services.
This is the Smartling CLI tool - a command-line interface for managing translation files through the Smartling platform. The CLI provides commands for file operations (push, pull, list, status, rename, delete, import), project management, machine translation services, and job progress tracking.

## Architecture

Expand All @@ -14,8 +14,9 @@ The codebase follows a layered architecture pattern:
- Each command has its own subdirectory with command definition and tests
- Service initializers are used to inject dependencies into commands
- **services/**: Business logic layer containing service implementations
- Each service corresponds to a command group (files, projects, init, mt)
- Each service corresponds to a command group (files, projects, init, mt, jobs)
- Services interact with the Smartling API SDK
- **services/helpers/**: Shared utilities for config management, error handling, progress rendering, format compilation, and thread pooling
- **output/**: Rendering and formatting layer for CLI output
- **main.go**: Entry point that wires together all commands and their dependencies

Expand All @@ -32,16 +33,20 @@ go build # Build for current platform

### Testing
```bash
make test_unit # Run unit tests
make test_integration # Run integration tests (requires binary in tests/cmd/bin/)
go test ./cmd/... # Run specific unit tests
make test_unit # Run all unit tests
make test_integration # Run all integration tests (requires binary in tests/cmd/bin/)
go test ./cmd/... # Run all unit tests in cmd/
go test ./cmd/files/push/ # Run tests for a specific command
go test ./tests/cmd/files/push/... # Run specific integration test
go test -v -run TestSpecificFunction ./cmd/... # Run specific test function
```

### Code Quality
```bash
make lint # Run revive linter
make lint # Run golangci-lint and revive linter
make tidy # Clean up go.mod
make mockery # Generate mocks using mockery
make mockery # Generate mocks using mockery (config: .mockery.yml)
make docs # Generate command documentation
```

### Package Building
Expand Down Expand Up @@ -82,4 +87,16 @@ The CLI uses YAML configuration files (smartling.yml) that can be placed in the
4. Output formatting is handled in output/ package
5. Configuration is managed through config helpers

The service initializer pattern allows for clean dependency injection and makes the codebase highly testable.
### Service Initializer Pattern

Each command group (files, projects, init, mt, jobs) follows the same dependency injection pattern:

1. **Command Group** (e.g., `cmd/files/cmd_files.go`): Defines the `SrvInitializer` interface and factory function
2. **Service Initializer** (e.g., `cmd/files/cmd_files.go`): Implements the initializer that wires up SDK clients and configuration
3. **Service** (e.g., `services/files/service.go`): Defines the Service interface with business logic methods
4. **Command Implementation** (e.g., `cmd/files/push/cmd_push.go`): Uses the initializer to get service instance and executes operations

This pattern enables:
- Easy mocking of services in command tests
- Centralized client and configuration setup
- Clear separation between CLI interface and business logic
4 changes: 3 additions & 1 deletion Makefile
Comment thread
dimitrystd marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ build: darwin windows.exe linux

get:
go get
go mod vendor

clean:
rm -rf bin pkg
Expand Down Expand Up @@ -59,7 +60,7 @@ _pkg-init:
$(shell git rev-list --count HEAD).$(shell git rev-parse --short HEAD))

%:
GOOS=$(basename $@) go build -o bin/smartling.$@
GOOS=$(basename $@) go build -mod=mod -o bin/smartling.$@

docs:
go run ./main.go docs
Expand Down Expand Up @@ -87,6 +88,7 @@ test_unit:
test_integration:
go test ./tests/cmd/files/push/...
go test ./tests/cmd/files/pull/...
go test ./tests/cmd/jobs/progress/...
go test ./tests/cmd/files/list/...
go test ./tests/cmd/files/status/...
go test ./tests/cmd/files/rename/...
Expand Down
File renamed without changes.
39 changes: 39 additions & 0 deletions cmd/helpers/resolve/output_params.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package resolve

import (
"github.com/Smartling/smartling-cli/output"
clierror "github.com/Smartling/smartling-cli/services/helpers/cli_error"

"github.com/spf13/cobra"
)

// OutputParams resolve OutputParams for subcommands
func OutputParams(cmd *cobra.Command, fileConfigMTFileFormat *string) (output.Params, error) {
const outputTemplateFlag = "format"
format, err := cmd.Parent().PersistentFlags().GetString("output")
if err != nil {
return output.Params{}, clierror.UIError{
Operation: "get output",
Err: err,
Description: "unable to get output param",
}
}
template := FallbackString(cmd.Flags().Lookup(outputTemplateFlag), StringParam{
FlagName: outputTemplateFlag,
Config: fileConfigMTFileFormat,
})

mode, err := cmd.Parent().PersistentFlags().GetString("output-mode")
if err != nil {
return output.Params{}, clierror.UIError{
Operation: "get output mode",
Err: err,
Description: "unable to get output mode param",
}
}
return output.Params{
Mode: mode,
Format: format,
Template: template,
}, nil
}
4 changes: 1 addition & 3 deletions cmd/init/cmd_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ import (
"github.com/spf13/cobra"
)

var (
dryRun bool
)
var dryRun bool

// NewInitCmd creates a new command to initialize the Smartling CLI.
func NewInitCmd(srvInitializer SrvInitializer) *cobra.Command {
Expand Down
70 changes: 70 additions & 0 deletions cmd/jobs/cmd_jobs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package jobs

import (
"fmt"
"slices"
"strings"

"github.com/spf13/cobra"
)

const (
outputFormatFlag = "output"
)

var (
outputFormat string
allowedOutputs = []string{
"json",
"simple",
}
joinedAllowedOutputs = strings.Join(allowedOutputs, ", ")
)

// NewJobsCmd returns new jobs command
func NewJobsCmd() *cobra.Command {
jobsCmd := &cobra.Command{
Use: "jobs",
Short: "Manage translation jobs and monitor their progress.",
Long: `Translation jobs are the fundamental unit of work in Smartling TMS that organize
content for translation and track it through the translation workflow.

The jobs command group provides tools to interact with translation jobs, including
monitoring translation progress, viewing job details, and managing job workflows.

Each job contains one or more files targeted for translation into specific locales,
with defined due dates and workflow steps. Jobs help coordinate translation work between
content owners, project managers, and translators.

Available options:
--output string Output format: ` + joinedAllowedOutputs + ` (default "simple")
- simple: Human-readable format optimized for terminal display
- json: Raw API response for programmatic processing and automation`,
Example: `
# View job progress in human-readable format

smartling-cli jobs progress "Website Q1 2026"

# Get detailed progress data for automation

smartling-cli jobs progress aabbccdd --output json

`,
PreRunE: func(cmd *cobra.Command, args []string) error {
if !slices.Contains(allowedOutputs, outputFormat) {
return fmt.Errorf("invalid output: %s (allowed: %s)", outputFormat, joinedAllowedOutputs)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 && cmd.Flags().NFlag() == 0 {
return cmd.Help()
}
return nil
},
}

jobsCmd.PersistentFlags().StringVar(&outputFormat, outputFormatFlag, "simple", "Output format: "+joinedAllowedOutputs)

return jobsCmd
}
136 changes: 136 additions & 0 deletions cmd/jobs/progress/cmd_progress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package progress

import (
"errors"
"fmt"

rootcmd "github.com/Smartling/smartling-cli/cmd"
"github.com/Smartling/smartling-cli/cmd/helpers/resolve"
jobscmd "github.com/Smartling/smartling-cli/cmd/jobs"
"github.com/Smartling/smartling-cli/output"
clierror "github.com/Smartling/smartling-cli/services/helpers/cli_error"
"github.com/Smartling/smartling-cli/services/helpers/help"
srv "github.com/Smartling/smartling-cli/services/jobs"

"github.com/spf13/cobra"
)

// NewProgressCmd returns new progress command
func NewProgressCmd(initializer jobscmd.SrvInitializer) *cobra.Command {
progressCmd := &cobra.Command{
Use: "progress <translationJobUid|translationJobName>",
Short: "Track translation progress for a specific job.",
Long: `smartling-cli jobs progress <translationJobUid|translationJobName> [--output json]

Retrieves real-time translation progress metrics for a specific translation job.
This command is essential for monitoring active translations, estimating completion times,
and tracking workflow progress across multiple locales.

Progress information includes total word counts, completion percentages, and detailed
per-locale breakdowns showing how content moves through each translation workflow step
(awaiting authorization, in translation, completed, etc.).

The command accepts either:
• Translation Job UID: 12-character alphanumeric identifier (e.g., aabbccdd1122)
• Translation Job Name: Human-readable name assigned when creating the job

If multiple jobs share the same name, the most recent active job (not Canceled or Closed)
will be selected.

Output Formats:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@az-smartling I have updated the documentation for two new commands. Please review the new texts, but I want to draw your attention to these output formats. Of course, this text was generated by LLM, but I'm not sure if we should hard-code it into this command. Perhaps we should make this text a shared const and reuse it in all new commands. What are your thoughts on it?

Copy link
Copy Markdown
Contributor Author

@az-smartling az-smartling Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be good to standardize output related variable for all commands. And move them to root command.
We have
--output-format (simple, json etc)
--output-template ({{.File}}, {{.Language}} ...)
--output-mode (dynamic, static) - only for mt command

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My original comment was about two predefined output styles\templates: simple and json. If we can generalize the code and keep it in the root command, then it is great.
But I didn't get the output-mode (dynamic, static). What is it? And why mt requires a dedicated implementation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some commands we have table output.

--output string Output format: table, json, simple (default "simple")

We should discuss this.


--output simple (default)
Displays key progress metrics in human-readable format:
- Total word count across all locales
- Overall completion percentage
Best for: Quick status checks, manual monitoring, terminal viewing

--output json
Returns the complete API response as JSON, including:
- Per-locale progress breakdowns
- Workflow step details (authorized, awaiting, completed, etc.)
- String and word counts at each workflow stage
- Target locale descriptions
Best for: Automation scripts, CI/CD pipelines, custom reporting tools

Use Cases:
• Monitor active translation projects to estimate delivery times
• Track progress before authorizing next workflow steps
• Build automated alerts when translations reach completion thresholds
• Generate custom progress reports for stakeholders
• Integrate with CI/CD pipelines to gate deployments on translation completion

Project Configuration:
Project ID must be configured in smartling.yml or specified via --project flag.
Account ID can be configured in smartling.yml or specified via --account flag.

Authentication is required via user_id and secret in smartling.yml or environment variables.

Available options:` + help.AuthenticationOptions,
Example: `
# Check progress using job name

smartling-cli jobs progress "Website Q1 2026"

# Check progress using job UID

smartling-cli jobs progress aabbccdd1122

# Get detailed JSON output for automation

smartling-cli jobs progress "Mobile App Release" --output json

# Use with specific project

smartling-cli jobs progress aabbccdd1122 --project 9876543210

# Parse JSON output in scripts (example: check if job is 100% complete)

PROGRESS=$(smartling-cli jobs progress my-job --output json | jq '.percentComplete')
if [ "$PROGRESS" -eq 100 ]; then
echo "Translation complete!"
fi

# Monitor progress for CI/CD gate

smartling-cli jobs progress "Release v2.0" --output json | \
jq -e '.percentComplete >= 95' && echo "Ready for deployment"

`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if len(args) != 1 {
return clierror.UIError{
Operation: "check args",
Err: errors.New("wrong argument quantity"),
Description: fmt.Sprintf("expected one argument, got: %d", len(args)),
}
}
idOrName := args[0]

cnf, err := rootcmd.Config()
if err != nil {
return err
}

accountUID, err := resolve.FallbackAccount(cmd.Root().PersistentFlags().Lookup("account"), cnf.AccountID)
if err != nil {
return err
}

params := srv.ProgressParams{
AccountUID: accountUID,
ProjectUID: cnf.ProjectID,
JobIDOrName: idOrName,
}
format, err := cmd.Parent().PersistentFlags().GetString("output")
if err != nil {
return err
}
outputParams := output.Params{Format: format}
return run(ctx, initializer, params, outputParams)
},
}

return progressCmd
}
42 changes: 42 additions & 0 deletions cmd/jobs/progress/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package progress

import (
"context"

jobscmd "github.com/Smartling/smartling-cli/cmd/jobs"
"github.com/Smartling/smartling-cli/output"
"github.com/Smartling/smartling-cli/output/jobs"
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"
)

func run(ctx context.Context,
initializer jobscmd.SrvInitializer,
params srv.ProgressParams,
outputParams output.Params,
) error {
rlog.Debugf("running progress with params: %v", params)
jobSrv, err := initializer.InitJobSrv()
if err != nil {
return clierror.UIError{
Operation: "init",
Err: err,
Description: "unable to initialize Jobs service",
}
}

progressOutput, err := jobSrv.RunProgress(ctx, params)
if err != nil {
return err
}

if progressOutput.TranslationJobUID == "" {
rlog.Infof("no jobs found for given translationJobUid or translationJobName: %s", params.JobIDOrName)
return nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the exit code that will be returned in this case? I believe it should be non-zero.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to return error

}

outputFormat := jobs.GetOutputFormat(outputParams.Format)
outputFormat.FormatAndRender(progressOutput)
return nil
}
Loading