Skip to content

feat: add run --estimate for pre-run cost estimates(cli,sdk)#2111

Open
cherkanovart wants to merge 1 commit into
mainfrom
eng-1067-run-estimate-flag
Open

feat: add run --estimate for pre-run cost estimates(cli,sdk)#2111
cherkanovart wants to merge 1 commit into
mainfrom
eng-1067-run-estimate-flag

Conversation

@cherkanovart

@cherkanovart cherkanovart commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Adds a pre-run cost estimate to the sync CLI, mirroring the platform CLI's push --estimate (ENG-1067).

What was done

  • SDK: new LingoDotDevEngine.estimate(items) method — POSTs per-target-locale character counts to the new /process/estimate endpoint and returns the approximate cost with a per-locale breakdown. Items are zod-validated and locale codes normalized to canonical BCP 47 client-side.
  • CLI: new lingo.dev run --estimate flag. Computes the same change delta as a regular run (checksums → delta → processable data, same --force/--key/--file/--bucket semantics), counts translatable characters (leaf string values only), fetches the estimate, prints a per-locale breakdown, and exits. Nothing is translated, written, or billed; lockfile and target files stay untouched.
  • Delta-scoping logic (computeProcessableData) and bucket-loader construction extracted from execute.ts into shared _utils.ts helpers so estimate and execute can never disagree on what is "pending".
  • --estimate is rejected when combined with --watch or --frozen, and errors with a clear message for providers without server-side pricing (pseudo, BYOK).

Notes for review/testing

  • Blocked on platform PR lingodotdev/v1#713 — the /process/estimate endpoint is not deployed yet. Code here is inert until then (the flag fails with a 404 from the API).
  • After chore: bump package versions #713 deploys, e2e test: project with pending changes → lingo.dev run --estimate → per-locale breakdown matches push --estimate semantics; re-run without changes → "$0.00 — nothing needs translation"; --estimate --watch → clear error.
  • Character-count semantics match the server contract: sum of translatable leaf string lengths, keys/markup excluded.

Tests

  • SDK: 5 new cases (request shape, locale normalization, empty/negative rejection, server error surfacing) — 30/30 pass.
  • CLI: estimate.spec.ts covering countTranslatableChars and computeProcessableData (delta scoping, --force, key patterns) — 6/6 pass.
  • Full turbo build typecheck test for both packages green.

Summary by CodeRabbit

  • New Features

    • Added --estimate flag to the run command to display estimated translation costs without processing
    • Added estimate() method to the SDK for cost estimation
  • Tests

    • Added test coverage for estimation functionality and data processing validation

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a new lingo.dev run --estimate feature that computes pending translation character counts per locale, requests a cost estimate from the backend /process/estimate endpoint, displays per-locale USD cost breakdowns, and exits without performing translations. It also extracts shared data-processing utilities and extends the SDK with estimate support.

Changes

Estimate Feature

Layer / File(s) Summary
Type contracts and schemas
.changeset/run-estimate-flag.md, packages/cli/src/cli/cmd/run/_types.ts, packages/cli/src/cli/localizer/_types.ts, packages/sdk/src/index.ts
New CostEstimate type with overall totals and per-locale breakdown; estimate boolean flag added to run CLI schema; ILocalizer interface extended with optional estimate method; release notes document new --estimate mode and SDK method.
Shared CLI data processing utilities
packages/cli/src/cli/cmd/run/_utils.ts, packages/cli/src/cli/cmd/run/estimate.spec.ts
Extracted createLoaderForTask (bucket loader builder) and computeProcessableData (delta/force/pattern-based entry filtering) as reusable functions with comprehensive test coverage for both.
SDK estimate endpoint client
packages/sdk/src/index.ts, packages/sdk/src/index.spec.ts
LingoDotDevEngine.estimate validates inputs, normalizes locale codes, POSTs to /process/estimate, and returns CostEstimate; tests verify request format, locale normalization, client-side validation, and server error handling.
CLI localizer provider integration
packages/cli/src/cli/localizer/lingodotdev.ts
createLingoDotDevLocalizer now returns a localizer with an estimate method that delegates to the SDK engine.
Estimate command implementation
packages/cli/src/cli/cmd/run/estimate.ts
countTranslatableChars sums string leaf values; estimate default export orchestrates a Listr workflow that computes per-task deltas, counts translatable characters per target locale, fetches cost estimates, formats USD amounts, and displays per-locale and total costs.
CLI run command wiring
packages/cli/src/cli/cmd/run/index.ts
Imports estimate command, adds --estimate flag, validates it is not combined with --watch/--frozen, and provides an early-exit path that calls estimate(ctx) and skips normal translation execution.
Execute command refactoring
packages/cli/src/cli/cmd/run/execute.ts
Imports and uses shared createLoaderForTask and computeProcessableData utilities instead of in-file implementations, reducing code duplication.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • AndreyHirsa

🐰 A new flag to peek before the cost,
Estimates dance through /process/estimate with care,
Shared utils refactored, no precious bytes lost—
Translation wisdom, with pricing laid bare! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description is comprehensive, covering what was done (SDK/CLI changes), testing approach, and notes for review; it aligns with the template's key sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title directly and clearly summarizes the main change: adding a run --estimate feature for pre-run cost estimates to both CLI and SDK.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch eng-1067-run-estimate-flag

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (2)
packages/cli/src/cli/cmd/run/estimate.ts (1)

10-22: 💤 Low value

Consider clarifying "leaf" terminology or verifying flat data assumption.

The comment describes counting "leaf string-value lengths," but the implementation only checks top-level values in the processableData object. If processableData can contain nested objects, this function would miss strings in nested structures.

However, in typical i18n workflows, translation data is flattened into dot-notation keys (e.g., {"auth.login.title": "Login"}), so top-level iteration is likely correct.

Recommendation: Either:

  1. Update the comment to clarify that data is expected to be flat (e.g., "the sum of string values — processableData keys are already flattened"), or
  2. Add a test case with a nested object to document the expected behavior (should return 0 for nested values)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/cli/cmd/run/estimate.ts` around lines 10 - 22, The comment
for countTranslatableChars claims it sums "leaf string-value lengths" but the
implementation only inspects top-level values, so either clarify the assumption
or make behavior explicit: update the comment to state that processableData is
expected to be flattened (e.g., "processableData keys are already flattened;
only top-level string values are counted"), or if nested objects may appear,
modify tests to assert current behavior (add a test with a nested object
expecting nested strings are ignored) or change the implementation to
recursively traverse nested objects; reference the countTranslatableChars
function when applying the change.
packages/sdk/src/index.ts (1)

904-930: ⚡ Quick win

Consider adding retry logic and event tracking for consistency.

The estimate() method uses plain fetch instead of fetchWithRetry, which means transient 5xx errors won't be retried. Since cost estimates are user-facing operations that users request before committing to expensive translations, retrying on transient failures would improve reliability.

Additionally, estimate() doesn't track events (ESTIMATE_START/SUCCESS/ERROR), unlike other public methods (localizeObject, localizeText, etc.). Adding tracking would improve observability and usage analytics.

♻️ Suggested improvements
  1. Replace fetch with fetchWithRetry for resilience:
-    const res = await fetch(url, {
+    const res = await this.fetchWithRetry(
+      url,
+      {
        method: "POST",
        headers: this.headers,
        body: JSON.stringify({ items: parsedItems }),
        signal,
-    });
+      },
+      signal,
+    );
  1. Add event tracking for observability:
  async estimate(
    items: { targetLocale: string; sourceChars: number }[],
    signal?: AbortSignal,
  ): Promise<CostEstimate> {
+   const trackProps = { method: "estimate" };
+   trackEvent(
+     this.config.apiKey,
+     this.config.apiUrl,
+     TRACKING_EVENTS.ESTIMATE_START,
+     trackProps,
+   );
+   try {
      const parsedItems = estimateItemsSchema.parse(items);
      const url = `${this.config.apiUrl}/process/estimate`;

      const res = await this.fetchWithRetry(
        url,
        {
          method: "POST",
          headers: this.headers,
          body: JSON.stringify({ items: parsedItems }),
          signal,
        },
        signal,
      );

      await LingoDotDevEngine.throwOnHttpError(res, "Error estimating cost");

-     return res.json();
+     const result = await res.json();
+     trackEvent(
+       this.config.apiKey,
+       this.config.apiUrl,
+       TRACKING_EVENTS.ESTIMATE_SUCCESS,
+       trackProps,
+     );
+     return result;
+   } catch (error) {
+     trackEvent(
+       this.config.apiKey,
+       this.config.apiUrl,
+       TRACKING_EVENTS.ESTIMATE_ERROR,
+       {
+         ...trackProps,
+         errorMessage: error instanceof Error ? error.message : String(error),
+       },
+     );
+     throw error;
+   }
  }

Note: You'll need to add ESTIMATE_START, ESTIMATE_SUCCESS, and ESTIMATE_ERROR to TRACKING_EVENTS in utils/tracking-events.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sdk/src/index.ts` around lines 904 - 930, The estimate() method
should be made resilient and observable: replace the plain fetch call with
fetchWithRetry (preserving method, headers, body and signal) and keep the
existing LingoDotDevEngine.throwOnHttpError(res, ...) behavior; surround the
operation with Tracking.track calls (emit ESTIMATE_START before the request,
ESTIMATE_SUCCESS on successful JSON return, and ESTIMATE_ERROR in the catch
path) and ensure the ESTIMATE_START/ESTIMATE_SUCCESS/ESTIMATE_ERROR constants
are added to TRACKING_EVENTS so the events are recognized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/cli/src/cli/cmd/run/estimate.ts`:
- Around line 10-22: The comment for countTranslatableChars claims it sums "leaf
string-value lengths" but the implementation only inspects top-level values, so
either clarify the assumption or make behavior explicit: update the comment to
state that processableData is expected to be flattened (e.g., "processableData
keys are already flattened; only top-level string values are counted"), or if
nested objects may appear, modify tests to assert current behavior (add a test
with a nested object expecting nested strings are ignored) or change the
implementation to recursively traverse nested objects; reference the
countTranslatableChars function when applying the change.

In `@packages/sdk/src/index.ts`:
- Around line 904-930: The estimate() method should be made resilient and
observable: replace the plain fetch call with fetchWithRetry (preserving method,
headers, body and signal) and keep the existing
LingoDotDevEngine.throwOnHttpError(res, ...) behavior; surround the operation
with Tracking.track calls (emit ESTIMATE_START before the request,
ESTIMATE_SUCCESS on successful JSON return, and ESTIMATE_ERROR in the catch
path) and ensure the ESTIMATE_START/ESTIMATE_SUCCESS/ESTIMATE_ERROR constants
are added to TRACKING_EVENTS so the events are recognized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a4d5682-3422-4047-aa5c-5ecece52ed10

📥 Commits

Reviewing files that changed from the base of the PR and between 20ca833 and 20e9855.

📒 Files selected for processing (11)
  • .changeset/run-estimate-flag.md
  • packages/cli/src/cli/cmd/run/_types.ts
  • packages/cli/src/cli/cmd/run/_utils.ts
  • packages/cli/src/cli/cmd/run/estimate.spec.ts
  • packages/cli/src/cli/cmd/run/estimate.ts
  • packages/cli/src/cli/cmd/run/execute.ts
  • packages/cli/src/cli/cmd/run/index.ts
  • packages/cli/src/cli/localizer/_types.ts
  • packages/cli/src/cli/localizer/lingodotdev.ts
  • packages/sdk/src/index.spec.ts
  • packages/sdk/src/index.ts

@cherkanovart cherkanovart changed the title feat(cli,sdk): add run --estimate for pre-run cost estimates feat: add run --estimate for pre-run cost estimates(cli,sdk) Jun 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants