Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions .changeset/thirty-pots-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@lingo.dev/compiler": minor
"@lingo.dev/_compiler": minor
"@lingo.dev/_spec": minor
"lingo.dev": minor
"@lingo.dev/_sdk": minor
---

Migrate SDK and CLI to unified API endpoints. All requests now use `api.lingo.dev` with `X-API-Key` auth. Added `engineId` config option (auto-migrated from `vNext`)
58 changes: 16 additions & 42 deletions packages/cli/src/cli/cmd/ci/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import path from "path";
import { Command } from "interactive-commander";
import createOra from "ora";
import { getSettings } from "../../utils/settings";
Expand All @@ -7,7 +6,6 @@ import { IIntegrationFlow } from "./flows/_base";
import { PullRequestFlow } from "./flows/pull-request";
import { InBranchFlow } from "./flows/in-branch";
import { getPlatformKit } from "./platforms";
import { getConfig } from "../../utils/config";

interface CIOptions {
parallel?: boolean;
Expand Down Expand Up @@ -72,53 +70,29 @@ export default new Command()
parseBooleanArg,
)
.action(async (options: CIOptions) => {
const configDir = options.workingDirectory
? path.resolve(process.cwd(), options.workingDirectory)
: process.cwd();
const originalCwd = process.cwd();
let config;
try {
process.chdir(configDir);
config = getConfig(false);
} finally {
process.chdir(originalCwd);
}

const isVNext = !!config?.vNext;

const settings = getSettings(options.apiKey);

if (isVNext) {
if (!settings.auth.vnext?.apiKey) {
console.error(
"No LINGO_API_KEY provided. vNext requires LINGO_API_KEY environment variable.",
);
return;
}
} else {
if (!settings.auth.apiKey) {
console.error("No API key provided");
return;
}
if (!settings.auth.apiKey) {
console.error(
"No API key provided. Set LINGO_API_KEY environment variable or use --api-key flag.",
);
return;
}
Comment on lines +75 to +80
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.

⚠️ Potential issue | 🔴 Critical

Auth failures should fail CI with non-zero exit code.

At Line 93 and Line 104, early return exits successfully. CI can pass even when authentication is missing/invalid.

🛠️ Proposed fix
     if (!settings.auth.apiKey) {
       console.error(
         "No API key provided. Set LINGODOTDEV_API_KEY environment variable or use --api-key flag.",
       );
+      process.exitCode = 1;
       return;
     }

@@
     const auth = await authenticator.whoami();
     if (!auth) {
       console.error("Not authenticated");
+      process.exitCode = 1;
       return;
     }

Also applies to: 101-105

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/cli/cmd/ci/index.ts` around lines 89 - 94, The auth failure
currently just logs and returns (see the check for settings.auth.apiKey and the
other early returns), which exits with code 0; change these early-return
branches to terminate with a non-zero exit code (e.g., call process.exit(1) or
throw an error) so CI detects failure—update the blocks that log via
console.error and then return to instead log the error and immediately call
process.exit(1) (or throw) to fail the process.


const authenticator = createAuthenticator({
apiUrl: settings.auth.apiUrl,
apiKey: settings.auth.apiKey,
});
const authenticator = createAuthenticator({
apiUrl: settings.auth.apiUrl,
apiKey: settings.auth.apiKey,
});

const auth = await authenticator.whoami();
if (!auth) {
console.error("Not authenticated");
return;
}
const auth = await authenticator.whoami();
if (!auth) {
console.error("Not authenticated");
return;
}

const env = {
...(settings.auth.apiKey && {
LINGODOTDEV_API_KEY: settings.auth.apiKey,
}),
...(settings.auth.vnext?.apiKey && {
LINGO_API_KEY: settings.auth.vnext.apiKey,
LINGO_API_KEY: settings.auth.apiKey,
}),
LINGODOTDEV_PULL_REQUEST: options.pullRequest?.toString() || "false",
...(options.commitMessage && {
Expand Down Expand Up @@ -162,7 +136,7 @@ export default new Command()
}

const hasChanges = await flow.run({
parallel: isVNext || options.parallel,
parallel: options.parallel,
});
if (!hasChanges) {
return;
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/cli/cmd/ci/platforms/_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export abstract class PlatformKit<

get config() {
const env = Z.object({
LINGO_API_KEY: Z.string().optional(),
LINGODOTDEV_API_KEY: Z.string().optional(),
LINGODOTDEV_PULL_REQUEST: Z.preprocess(
(val) => val === "true" || val === true,
Expand All @@ -68,7 +69,7 @@ export abstract class PlatformKit<
}).parse(process.env);

return {
replexicaApiKey: env.LINGODOTDEV_API_KEY || "",
replexicaApiKey: env.LINGO_API_KEY || env.LINGODOTDEV_API_KEY || "",
isPullRequestMode: env.LINGODOTDEV_PULL_REQUEST,
commitMessage: env.LINGODOTDEV_COMMIT_MESSAGE || defaultMessage,
pullRequestTitle: env.LINGODOTDEV_PULL_REQUEST_TITLE || defaultMessage,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/cli/cmd/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ export default new Command()
let processPayload = createProcessor(i18nConfig!.provider, {
apiKey: settings.auth.apiKey,
apiUrl: settings.auth.apiUrl,
engineId: i18nConfig!.engineId,
});
processPayload = withExponentialBackoff(
processPayload,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/cli/cmd/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Press Enter to open the browser for authentication.

---

Having issues? Put LINGODOTDEV_API_KEY in your .env file instead.
Having issues? Put LINGO_API_KEY in your .env file instead.
`.trim() + "\n",
);

Expand Down
21 changes: 8 additions & 13 deletions packages/cli/src/cli/cmd/run/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,18 @@ export default async function setup(input: CmdRunContext) {
{
title: "Selecting localization provider",
task: async (ctx, task) => {
const isPseudo = ctx.flags.pseudo || ctx.config?.dev?.usePseudotranslator;
const isPseudo =
ctx.flags.pseudo || ctx.config?.dev?.usePseudotranslator;
const provider = isPseudo ? "pseudo" : ctx.config?.provider;
const vNext = ctx.config?.vNext;
ctx.localizer = createLocalizer(provider, ctx.flags.apiKey, vNext);
const engineId = ctx.config?.engineId;
ctx.localizer = createLocalizer(provider, engineId, ctx.flags.apiKey);
if (!ctx.localizer) {
throw new Error(
"Could not create localization provider. Please check your i18n.json configuration.",
);
}
task.title =
ctx.localizer.id === "Lingo.dev" ||
ctx.localizer.id === "Lingo.dev vNext"
ctx.localizer.id === "Lingo.dev"
? `Using ${chalk.hex(colors.green)(ctx.localizer.id)} provider`
: ctx.localizer.id === "pseudo"
? `Using ${chalk.hex(colors.blue)("pseudo")} mode for testing`
Expand All @@ -71,8 +71,7 @@ export default async function setup(input: CmdRunContext) {
{
title: "Checking authentication",
enabled: (ctx) =>
(ctx.localizer?.id === "Lingo.dev" ||
ctx.localizer?.id === "Lingo.dev vNext") &&
ctx.localizer?.id === "Lingo.dev" &&
!ctx.flags.pseudo &&
!ctx.config?.dev?.usePseudotranslator,
task: async (ctx, task) => {
Expand All @@ -87,9 +86,7 @@ export default async function setup(input: CmdRunContext) {
},
{
title: "Validating configuration",
enabled: (ctx) =>
ctx.localizer?.id !== "Lingo.dev" &&
ctx.localizer?.id !== "Lingo.dev vNext",
enabled: (ctx) => ctx.localizer?.id !== "Lingo.dev",
task: async (ctx, task) => {
const validationStatus = await ctx.localizer!.validateSettings!();
if (!validationStatus.valid) {
Comment on lines +89 to 92
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.

⚠️ Potential issue | 🔴 Critical

Pseudo mode can crash in configuration validation.

At Line 89, the task is enabled for "pseudo", but pseudo localizer does not implement validateSettings, so Line 91 can throw at runtime.

🧩 Proposed fix
       {
         title: "Validating configuration",
-        enabled: (ctx) => ctx.localizer?.id !== "Lingo.dev",
+        enabled: (ctx) =>
+          ctx.localizer?.id !== "Lingo.dev" &&
+          ctx.localizer?.id !== "pseudo" &&
+          !!ctx.localizer?.validateSettings,
         task: async (ctx, task) => {
           const validationStatus = await ctx.localizer!.validateSettings!();
           if (!validationStatus.valid) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
enabled: (ctx) => ctx.localizer?.id !== "Lingo.dev",
task: async (ctx, task) => {
const validationStatus = await ctx.localizer!.validateSettings!();
if (!validationStatus.valid) {
enabled: (ctx) =>
ctx.localizer?.id !== "Lingo.dev" &&
ctx.localizer?.id !== "pseudo" &&
!!ctx.localizer?.validateSettings,
task: async (ctx, task) => {
const validationStatus = await ctx.localizer!.validateSettings!();
if (!validationStatus.valid) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/cli/cmd/run/setup.ts` around lines 89 - 92, The current
task's enabled predicate and execution call assume
ctx.localizer.validateSettings exists, which causes a runtime crash for the
"pseudo" localizer; in the async task (task: async (ctx, task) => { ... }) guard
the call to ctx.localizer!.validateSettings by checking that ctx.localizer is
present and typeof ctx.localizer.validateSettings === "function" before invoking
it, and treat missing validateSettings as valid (or handle appropriately) so the
validationStatus access and .valid check cannot throw; update both the
enabled/decision logic and the call site to use this predicate around
validateSettings to avoid calling an undefined function.

Expand All @@ -103,9 +100,7 @@ export default async function setup(input: CmdRunContext) {
{
title: "Initializing localization provider",
async task(ctx, task) {
const isLingoDotDev =
ctx.localizer!.id === "Lingo.dev" ||
ctx.localizer!.id === "Lingo.dev vNext";
const isLingoDotDev = ctx.localizer!.id === "Lingo.dev";
const isPseudo = ctx.localizer!.id === "pseudo";

const subTasks = isLingoDotDev
Expand Down
6 changes: 1 addition & 5 deletions packages/cli/src/cli/localizer/_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@ export type LocalizerProgressFn = (
) => void;

export interface ILocalizer {
id:
| "Lingo.dev"
| "Lingo.dev vNext"
| "pseudo"
| NonNullable<I18nConfig["provider"]>["id"];
id: "Lingo.dev" | "pseudo" | NonNullable<I18nConfig["provider"]>["id"];
checkAuth: () => Promise<{
authenticated: boolean;
username?: string;
Expand Down
10 changes: 2 additions & 8 deletions packages/cli/src/cli/localizer/index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
import { I18nConfig } from "@lingo.dev/_spec";

import createLingoDotDevLocalizer from "./lingodotdev";
import createLingoDotDevVNextLocalizer from "./lingodotdev-vnext";
import createExplicitLocalizer from "./explicit";
import createPseudoLocalizer from "./pseudo";
import { ILocalizer } from "./_types";

export default function createLocalizer(
provider: I18nConfig["provider"] | "pseudo" | null | undefined,
engineId?: string,
apiKey?: string,
vNext?: string,
): ILocalizer {
if (provider === "pseudo") {
return createPseudoLocalizer();
}

// Check if vNext is configured
if (vNext) {
return createLingoDotDevVNextLocalizer(vNext);
}

if (!provider) {
return createLingoDotDevLocalizer(apiKey);
return createLingoDotDevLocalizer(apiKey, engineId);
} else {
return createExplicitLocalizer(provider);
}
Expand Down
81 changes: 0 additions & 81 deletions packages/cli/src/cli/localizer/lingodotdev-vnext.ts

This file was deleted.

18 changes: 12 additions & 6 deletions packages/cli/src/cli/localizer/lingodotdev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { getSettings } from "../utils/settings";

export default function createLingoDotDevLocalizer(
explicitApiKey?: string,
engineId?: string,
): ILocalizer {
const { auth } = getSettings(explicitApiKey);
const settings = getSettings(explicitApiKey);

if (!auth) {
if (!settings.auth.apiKey) {
throw new Error(
dedent`
You're trying to use ${chalk.hex(colors.green)(
Expand All @@ -20,14 +21,17 @@ export default function createLingoDotDevLocalizer(
To fix this issue:
1. Run ${chalk.dim("lingo.dev login")} to authenticate, or
2. Use the ${chalk.dim("--api-key")} flag to provide an API key.
3. Set ${chalk.dim("LINGODOTDEV_API_KEY")} environment variable.
3. Set ${chalk.dim("LINGO_API_KEY")} environment variable.
`,
);
}

const triggerType = process.env.CI ? "ci" : "cli";

const engine = new LingoDotDevEngine({
apiKey: auth.apiKey,
apiUrl: auth.apiUrl,
apiKey: settings.auth.apiKey,
apiUrl: settings.auth.apiUrl,
...(engineId && { engineId }),
});

return {
Expand All @@ -48,7 +52,7 @@ export default function createLingoDotDevLocalizer(
localize: async (input: LocalizerData, onProgress) => {
// Nothing to translate – return the input as-is.
if (!Object.keys(input.processableData).length) {
return input;
return input.processableData;
}

const processedData = await engine.localizeObject(
Expand All @@ -61,6 +65,8 @@ export default function createLingoDotDevLocalizer(
[input.targetLocale]: input.targetData,
},
hints: input.hints,
filePath: input.filePath,
triggerType,
},
onProgress,
);
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/cli/processor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { createOllama } from "ollama-ai-provider-v2";

export default function createProcessor(
provider: I18nConfig["provider"],
params: { apiKey?: string; apiUrl: string },
params: { apiKey?: string; apiUrl: string; engineId?: string },
): LocalizerFn {
if (!provider) {
const result = createLingoLocalizer(params);
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/cli/processor/lingo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { LocalizerInput, LocalizerProgressFn } from "./_base";
export function createLingoLocalizer(params: {
apiKey?: string;
apiUrl: string;
engineId?: string;
}) {
return async (input: LocalizerInput, onProgress: LocalizerProgressFn) => {
if (!Object.keys(input.processableData).length) {
Expand All @@ -13,6 +14,7 @@ export function createLingoLocalizer(params: {
const lingo = new LingoDotDevEngine({
apiKey: params.apiKey,
apiUrl: params.apiUrl,
...(params.engineId && { engineId: params.engineId }),
});

const result = await lingo.localizeObject(
Expand Down
Loading