diff --git a/.changeset/thirty-pots-judge.md b/.changeset/thirty-pots-judge.md new file mode 100644 index 000000000..d23a52e6c --- /dev/null +++ b/.changeset/thirty-pots-judge.md @@ -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`) diff --git a/packages/cli/src/cli/cmd/ci/index.ts b/packages/cli/src/cli/cmd/ci/index.ts index 59ecc2317..887981c09 100644 --- a/packages/cli/src/cli/cmd/ci/index.ts +++ b/packages/cli/src/cli/cmd/ci/index.ts @@ -1,4 +1,3 @@ -import path from "path"; import { Command } from "interactive-commander"; import createOra from "ora"; import { getSettings } from "../../utils/settings"; @@ -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; @@ -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; + } - 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 && { @@ -162,7 +136,7 @@ export default new Command() } const hasChanges = await flow.run({ - parallel: isVNext || options.parallel, + parallel: options.parallel, }); if (!hasChanges) { return; diff --git a/packages/cli/src/cli/cmd/ci/platforms/_base.ts b/packages/cli/src/cli/cmd/ci/platforms/_base.ts index bafe56414..6c8e7947f 100644 --- a/packages/cli/src/cli/cmd/ci/platforms/_base.ts +++ b/packages/cli/src/cli/cmd/ci/platforms/_base.ts @@ -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, @@ -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, diff --git a/packages/cli/src/cli/cmd/i18n.ts b/packages/cli/src/cli/cmd/i18n.ts index 0669567e0..1214f7b66 100644 --- a/packages/cli/src/cli/cmd/i18n.ts +++ b/packages/cli/src/cli/cmd/i18n.ts @@ -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, diff --git a/packages/cli/src/cli/cmd/login.ts b/packages/cli/src/cli/cmd/login.ts index beffb7b98..7356b09af 100644 --- a/packages/cli/src/cli/cmd/login.ts +++ b/packages/cli/src/cli/cmd/login.ts @@ -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", ); diff --git a/packages/cli/src/cli/cmd/run/setup.ts b/packages/cli/src/cli/cmd/run/setup.ts index 0f3480b41..4277bd6b9 100644 --- a/packages/cli/src/cli/cmd/run/setup.ts +++ b/packages/cli/src/cli/cmd/run/setup.ts @@ -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` @@ -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) => { @@ -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) { @@ -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 diff --git a/packages/cli/src/cli/localizer/_types.ts b/packages/cli/src/cli/localizer/_types.ts index 7213cc948..8cfe8f431 100644 --- a/packages/cli/src/cli/localizer/_types.ts +++ b/packages/cli/src/cli/localizer/_types.ts @@ -17,11 +17,7 @@ export type LocalizerProgressFn = ( ) => void; export interface ILocalizer { - id: - | "Lingo.dev" - | "Lingo.dev vNext" - | "pseudo" - | NonNullable["id"]; + id: "Lingo.dev" | "pseudo" | NonNullable["id"]; checkAuth: () => Promise<{ authenticated: boolean; username?: string; diff --git a/packages/cli/src/cli/localizer/index.ts b/packages/cli/src/cli/localizer/index.ts index 122bdf63b..6d20b192b 100644 --- a/packages/cli/src/cli/localizer/index.ts +++ b/packages/cli/src/cli/localizer/index.ts @@ -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); } diff --git a/packages/cli/src/cli/localizer/lingodotdev-vnext.ts b/packages/cli/src/cli/localizer/lingodotdev-vnext.ts deleted file mode 100644 index 07fde149f..000000000 --- a/packages/cli/src/cli/localizer/lingodotdev-vnext.ts +++ /dev/null @@ -1,81 +0,0 @@ -import dedent from "dedent"; -import { ILocalizer, LocalizerData } from "./_types"; -import chalk from "chalk"; -import { colors } from "../constants"; -import { LingoDotDevEngine } from "@lingo.dev/_sdk"; -import { getSettings } from "../utils/settings"; - -export default function createLingoDotDevVNextLocalizer( - processId: string, -): ILocalizer { - const settings = getSettings(undefined); - - // Use LINGO_API_KEY from environment or settings - const apiKey = process.env.LINGO_API_KEY || settings.auth.vnext?.apiKey; - - if (!apiKey) { - throw new Error( - dedent` - You're trying to use ${chalk.hex(colors.green)( - "Lingo.dev vNext", - )} provider, however, no API key is configured. - - To fix this issue: - 1. Set ${chalk.dim("LINGO_API_KEY")} environment variable, or - 2. Add the key to your ${chalk.dim("~/.lingodotdevrc")} file under ${chalk.dim("[auth.vnext]")} section. - `, - ); - } - - // Use LINGO_API_URL from environment or default to api.lingo.dev - const apiUrl = process.env.LINGO_API_URL || "https://api.lingo.dev"; - - const triggerType = process.env.CI ? "ci" : "cli"; - - const engine = new LingoDotDevEngine({ - apiKey, - apiUrl, - engineId: processId, - }); - - return { - id: "Lingo.dev vNext", - checkAuth: async () => { - try { - const response = await engine.whoami(); - return { - authenticated: !!response, - username: response?.email, - }; - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - return { authenticated: false, error: errorMessage }; - } - }, - localize: async (input: LocalizerData, onProgress) => { - // Nothing to translate – return the input as-is. - if (!Object.keys(input.processableData).length) { - return input.processableData; - } - - const processedData = await engine.localizeObject( - input.processableData, - { - sourceLocale: input.sourceLocale, - targetLocale: input.targetLocale, - reference: { - [input.sourceLocale]: input.sourceData, - [input.targetLocale]: input.targetData, - }, - hints: input.hints, - filePath: input.filePath, - triggerType, - }, - onProgress, - ); - - return processedData; - }, - }; -} diff --git a/packages/cli/src/cli/localizer/lingodotdev.ts b/packages/cli/src/cli/localizer/lingodotdev.ts index 6e2834486..d3723728c 100644 --- a/packages/cli/src/cli/localizer/lingodotdev.ts +++ b/packages/cli/src/cli/localizer/lingodotdev.ts @@ -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)( @@ -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 { @@ -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( @@ -61,6 +65,8 @@ export default function createLingoDotDevLocalizer( [input.targetLocale]: input.targetData, }, hints: input.hints, + filePath: input.filePath, + triggerType, }, onProgress, ); diff --git a/packages/cli/src/cli/processor/index.ts b/packages/cli/src/cli/processor/index.ts index 1a92fe2f0..6b7db441a 100644 --- a/packages/cli/src/cli/processor/index.ts +++ b/packages/cli/src/cli/processor/index.ts @@ -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); diff --git a/packages/cli/src/cli/processor/lingo.ts b/packages/cli/src/cli/processor/lingo.ts index 41a8dc873..d078acffe 100644 --- a/packages/cli/src/cli/processor/lingo.ts +++ b/packages/cli/src/cli/processor/lingo.ts @@ -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) { @@ -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( diff --git a/packages/cli/src/cli/utils/auth.ts b/packages/cli/src/cli/utils/auth.ts index 8c1407504..034df1a55 100644 --- a/packages/cli/src/cli/utils/auth.ts +++ b/packages/cli/src/cli/utils/auth.ts @@ -18,10 +18,10 @@ export function createAuthenticator(params: AuthenticatorParams) { return { async whoami(): Promise { try { - const res = await fetch(`${params.apiUrl}/whoami`, { - method: "POST", + const res = await fetch(`${params.apiUrl}/users/me`, { + method: "GET", headers: { - Authorization: `Bearer ${params.apiKey}`, + "X-API-Key": params.apiKey, "Content-Type": "application/json", }, }); diff --git a/packages/cli/src/cli/utils/init-ci-cd.ts b/packages/cli/src/cli/utils/init-ci-cd.ts index 358f9b4ff..783567e47 100644 --- a/packages/cli/src/cli/utils/init-ci-cd.ts +++ b/packages/cli/src/cli/utils/init-ci-cd.ts @@ -105,7 +105,7 @@ jobs: - uses: actions/checkout@v4 - uses: lingodotdev/lingo.dev@main with: - api-key: \${{ secrets.LINGODOTDEV_API_KEY }} + api-key: \${{ secrets.LINGO_API_KEY }} `, }, spinner, diff --git a/packages/cli/src/cli/utils/settings.ts b/packages/cli/src/cli/utils/settings.ts index bdaf3c79c..91f895356 100644 --- a/packages/cli/src/cli/utils/settings.ts +++ b/packages/cli/src/cli/utils/settings.ts @@ -20,10 +20,13 @@ export function getSettings(explicitApiKey: string | undefined): CliSettings { auth: { apiKey: explicitApiKey || + env.LINGO_API_KEY || env.LINGODOTDEV_API_KEY || systemFile.auth?.apiKey || + systemFile.auth?.vnext?.apiKey || defaults.auth.apiKey, apiUrl: + env.LINGO_API_URL || env.LINGODOTDEV_API_URL || systemFile.auth?.apiUrl || defaults.auth.apiUrl, @@ -31,11 +34,6 @@ export function getSettings(explicitApiKey: string | undefined): CliSettings { env.LINGODOTDEV_WEB_URL || systemFile.auth?.webUrl || defaults.auth.webUrl, - vnext: { - apiKey: - env.LINGO_API_KEY || - systemFile.auth?.vnext?.apiKey, - }, }, llm: { openaiApiKey: env.OPENAI_API_KEY || systemFile.llm?.openaiApiKey, @@ -72,9 +70,6 @@ const SettingsSchema = Z.object({ apiKey: Z.string(), apiUrl: Z.string(), webUrl: Z.string(), - vnext: Z.object({ - apiKey: Z.string().optional(), - }).optional(), }), llm: Z.object({ openaiApiKey: Z.string().optional(), @@ -96,7 +91,7 @@ function _loadDefaults(): CliSettings { return { auth: { apiKey: "", - apiUrl: "https://engine.lingo.dev", + apiUrl: "https://api.lingo.dev", webUrl: "https://lingo.dev", }, llm: {}, @@ -105,10 +100,11 @@ function _loadDefaults(): CliSettings { function _loadEnv() { return Z.looseObject({ + LINGO_API_KEY: Z.string().optional(), + LINGO_API_URL: Z.string().optional(), LINGODOTDEV_API_KEY: Z.string().optional(), LINGODOTDEV_API_URL: Z.string().optional(), LINGODOTDEV_WEB_URL: Z.string().optional(), - LINGO_API_KEY: Z.string().optional(), OPENAI_API_KEY: Z.string().optional(), ANTHROPIC_API_KEY: Z.string().optional(), GROQ_API_KEY: Z.string().optional(), @@ -161,14 +157,40 @@ function _getSettingsFilePath(): string { function _legacyEnvVarWarning() { const env = _loadEnv(); - if (env.REPLEXICA_API_KEY && !env.LINGODOTDEV_API_KEY) { + if (env.REPLEXICA_API_KEY && !env.LINGO_API_KEY && !env.LINGODOTDEV_API_KEY) { console.warn( "\x1b[33m%s\x1b[0m", ` ⚠️ WARNING: REPLEXICA_API_KEY env var is deprecated ⚠️ =========================================================== -Please use LINGODOTDEV_API_KEY instead. +Please use LINGO_API_KEY instead. +=========================================================== +`, + ); + } + + if (env.LINGODOTDEV_API_KEY && !env.LINGO_API_KEY) { + console.warn( + "\x1b[33m%s\x1b[0m", + ` +⚠️ WARNING: LINGODOTDEV_API_KEY env var is deprecated ⚠️ +=========================================================== + +Please use LINGO_API_KEY instead. +=========================================================== +`, + ); + } + + if (env.LINGODOTDEV_API_URL && !env.LINGO_API_URL) { + console.warn( + "\x1b[33m%s\x1b[0m", + ` +⚠️ WARNING: LINGODOTDEV_API_URL env var is deprecated ⚠️ +=========================================================== + +Please use LINGO_API_URL instead. =========================================================== `, ); @@ -221,10 +243,10 @@ function _envVarsInfo() { `ℹ️ Using MISTRAL_API_KEY env var instead of key from user config`, ); } - if (env.LINGODOTDEV_API_URL) { + if (env.LINGO_API_URL || env.LINGODOTDEV_API_URL) { console.info( "\x1b[36m%s\x1b[0m", - `ℹ️ Using LINGODOTDEV_API_URL: ${env.LINGODOTDEV_API_URL}`, + `ℹ️ Using custom API URL: ${env.LINGO_API_URL || env.LINGODOTDEV_API_URL}`, ); } if (env.LINGODOTDEV_WEB_URL) { diff --git a/packages/compiler/src/utils/observability.ts b/packages/compiler/src/utils/observability.ts index dc06e3ad8..778f3df80 100644 --- a/packages/compiler/src/utils/observability.ts +++ b/packages/compiler/src/utils/observability.ts @@ -100,18 +100,18 @@ async function tryGetEmail(): Promise { const apiUrl = process.env.LINGODOTDEV_API_URL || rc?.auth?.apiUrl || - "https://engine.lingo.dev"; + "https://api.lingo.dev"; if (!apiKey) { return null; } try { - const res = await fetch(`${apiUrl}/whoami`, { - method: "POST", + const res = await fetch(`${apiUrl}/users/me`, { + method: "GET", headers: { - Authorization: `Bearer ${apiKey}`, - ContentType: "application/json", + "X-API-Key": apiKey, + "Content-Type": "application/json", }, }); if (res.ok) { diff --git a/packages/new-compiler/src/utils/observability.ts b/packages/new-compiler/src/utils/observability.ts index 1dcc3e0e2..dcd781841 100644 --- a/packages/new-compiler/src/utils/observability.ts +++ b/packages/new-compiler/src/utils/observability.ts @@ -100,18 +100,18 @@ async function tryGetEmail(): Promise { const apiUrl = process.env.LINGODOTDEV_API_URL || rc?.auth?.apiUrl || - "https://engine.lingo.dev"; + "https://api.lingo.dev"; if (!apiKey) { return null; } try { - const res = await fetch(`${apiUrl}/whoami`, { - method: "POST", + const res = await fetch(`${apiUrl}/users/me`, { + method: "GET", headers: { - Authorization: `Bearer ${apiKey}`, - ContentType: "application/json", + "X-API-Key": apiKey, + "Content-Type": "application/json", }, }); if (res.ok) { diff --git a/packages/sdk/src/abort-controller.specs.ts b/packages/sdk/src/abort-controller.specs.ts index 54b475f02..6232cd171 100644 --- a/packages/sdk/src/abort-controller.specs.ts +++ b/packages/sdk/src/abort-controller.specs.ts @@ -32,7 +32,7 @@ describe("AbortController Support", () => { ); expect(global.fetch).toHaveBeenCalledWith( - "https://test.api.com/i18n", + "https://test.api.com/process/localize", expect.objectContaining({ signal: controller.signal, }), @@ -71,7 +71,7 @@ describe("AbortController Support", () => { ); expect(global.fetch).toHaveBeenCalledWith( - "https://test.api.com/i18n", + "https://test.api.com/process/localize", expect.objectContaining({ signal: controller.signal, }), @@ -125,7 +125,7 @@ describe("AbortController Support", () => { ); expect(global.fetch).toHaveBeenCalledWith( - "https://test.api.com/i18n", + "https://test.api.com/process/localize", expect.objectContaining({ signal: controller.signal, }), @@ -150,7 +150,7 @@ describe("AbortController Support", () => { ); expect(global.fetch).toHaveBeenCalledWith( - "https://test.api.com/i18n", + "https://test.api.com/process/localize", expect.objectContaining({ signal: controller.signal, }), @@ -178,7 +178,7 @@ describe("AbortController Support", () => { expect(global.fetch).toHaveBeenCalledTimes(2); expect(global.fetch).toHaveBeenCalledWith( - "https://test.api.com/i18n", + "https://test.api.com/process/localize", expect.objectContaining({ signal: controller.signal, }), @@ -198,7 +198,7 @@ describe("AbortController Support", () => { await engine.recognizeLocale("Hello world", controller.signal); expect(global.fetch).toHaveBeenCalledWith( - "https://test.api.com/recognize", + "https://test.api.com/process/recognize", expect.objectContaining({ signal: controller.signal, }), @@ -220,7 +220,7 @@ describe("AbortController Support", () => { await engine.whoami(controller.signal); expect(global.fetch).toHaveBeenCalledWith( - "https://test.api.com/whoami", + "https://test.api.com/users/me", expect.objectContaining({ signal: controller.signal, }), diff --git a/packages/sdk/src/index.spec.ts b/packages/sdk/src/index.spec.ts index d11d38e2e..2df657ca0 100644 --- a/packages/sdk/src/index.spec.ts +++ b/packages/sdk/src/index.spec.ts @@ -4,7 +4,7 @@ vi.mock("./utils/observability"); import { LingoDotDevEngine } from "./index"; -describe("ReplexicaEngine", () => { +describe("LingoDotDevEngine", () => { it("should pass", () => { expect(1).toBe(1); }); @@ -39,7 +39,10 @@ describe("ReplexicaEngine", () => { `.trim(); // Mock the internal localization method - const engine = new LingoDotDevEngine({ apiKey: "test" }); + const engine = new LingoDotDevEngine({ + apiKey: "test", + engineId: "eng_test", + }); const mockLocalizeRaw = vi.spyOn(engine as any, "_localizeRaw"); mockLocalizeRaw.mockImplementation(async (content: any) => { // Simulate translation by adding 'ES:' prefix to all strings @@ -97,7 +100,10 @@ describe("ReplexicaEngine", () => { describe("localizeStringArray", () => { it("should localize an array of strings and maintain order", async () => { - const engine = new LingoDotDevEngine({ apiKey: "test" }); + const engine = new LingoDotDevEngine({ + apiKey: "test", + engineId: "eng_test", + }); const mockLocalizeRaw = vi.spyOn(engine as any, "_localizeRaw"); mockLocalizeRaw.mockImplementation(async (obj: any) => { // Simulate translation by adding 'ES:' prefix to all string values @@ -132,7 +138,10 @@ describe("ReplexicaEngine", () => { }); it("should handle empty array", async () => { - const engine = new LingoDotDevEngine({ apiKey: "test" }); + const engine = new LingoDotDevEngine({ + apiKey: "test", + engineId: "eng_test", + }); const mockLocalizeRaw = vi.spyOn(engine as any, "_localizeRaw"); mockLocalizeRaw.mockImplementation(async () => ({})); @@ -155,7 +164,10 @@ describe("ReplexicaEngine", () => { describe("localizeChat", () => { it("should flatten chat texts and preserve speaker names", async () => { - const engine = new LingoDotDevEngine({ apiKey: "test" }); + const engine = new LingoDotDevEngine({ + apiKey: "test", + engineId: "eng_test", + }); const mockLocalizeRaw = vi.spyOn(engine as any, "_localizeRaw"); mockLocalizeRaw.mockImplementation(async (obj: any) => { return Object.fromEntries( @@ -192,7 +204,7 @@ describe("ReplexicaEngine", () => { }); }); - describe("LingoDotDevEngine with engineId (vNext)", () => { + describe("with engineId", () => { let mockFetch: ReturnType; beforeEach(() => { @@ -200,29 +212,7 @@ describe("ReplexicaEngine", () => { global.fetch = mockFetch as any; }); - it("should use vNext endpoint and X-API-Key header", async () => { - mockFetch.mockResolvedValue({ - ok: true, - json: async () => ({ data: { greeting: "Hola" } }), - }); - - const engine = new LingoDotDevEngine({ - apiKey: "test-vnext-key", - engineId: "eng_123", - }); - - await engine.localizeObject( - { greeting: "Hello" }, - { sourceLocale: "en", targetLocale: "es" }, - ); - - const [url, options] = mockFetch.mock.calls[0]; - expect(url).toMatch(/\/process\/eng_123\/localize$/); - expect(options.headers["X-API-Key"]).toBe("test-vnext-key"); - expect(options.headers["Authorization"]).toBeUndefined(); - }); - - it("should send flat locale fields (not nested)", async () => { + it("should include engineId in request body", async () => { mockFetch.mockResolvedValue({ ok: true, json: async () => ({ data: { greeting: "Hola" } }), @@ -238,10 +228,11 @@ describe("ReplexicaEngine", () => { { sourceLocale: "en", targetLocale: "es" }, ); - const body = JSON.parse(mockFetch.mock.calls[0][1].body); - expect(body.sourceLocale).toBe("en"); - expect(body.targetLocale).toBe("es"); - expect(body.locale).toBeUndefined(); + const [url, options] = mockFetch.mock.calls[0]; + expect(url).toMatch(/\/process\/localize$/); + expect(options.headers["X-API-Key"]).toBe("test-key"); + const body = JSON.parse(options.body); + expect(body.engineId).toBe("eng_123"); }); it("should include sessionId and triggerType in request body", async () => { @@ -313,8 +304,37 @@ describe("ReplexicaEngine", () => { const body = JSON.parse(mockFetch.mock.calls[0][1].body); expect(body.metadata).toEqual({ filePath: "src/messages.json" }); }); + }); + + describe("without engineId", () => { + let mockFetch: ReturnType; + + beforeEach(() => { + mockFetch = vi.fn(); + global.fetch = mockFetch as any; + }); - it("should use /process/recognize endpoint for recognizeLocale", async () => { + it("should use /process/localize endpoint", async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: async () => ({ data: { greeting: "Hola" } }), + }); + + const engine = new LingoDotDevEngine({ + apiKey: "test-key", + }); + + await engine.localizeObject( + { greeting: "Hello" }, + { sourceLocale: "en", targetLocale: "es" }, + ); + + const [url, options] = mockFetch.mock.calls[0]; + expect(url).toBe("https://api.lingo.dev/process/localize"); + expect(options.headers["X-API-Key"]).toBe("test-key"); + }); + + it("should use /process/recognize endpoint", async () => { mockFetch.mockResolvedValue({ ok: true, json: async () => ({ locale: "fr" }), @@ -322,18 +342,17 @@ describe("ReplexicaEngine", () => { const engine = new LingoDotDevEngine({ apiKey: "test-key", - engineId: "eng_123", }); const result = await engine.recognizeLocale("Bonjour le monde"); const [url, options] = mockFetch.mock.calls[0]; - expect(url).toMatch(/\/process\/recognize$/); + expect(url).toBe("https://api.lingo.dev/process/recognize"); expect(options.headers["X-API-Key"]).toBe("test-key"); expect(result).toBe("fr"); }); - it("whoami should call /users/me with GET and X-API-Key header", async () => { + it("should use /users/me with GET and X-API-Key header", async () => { mockFetch.mockResolvedValue({ ok: true, json: async () => ({ id: "usr_abc", email: "user@example.com" }), @@ -341,23 +360,42 @@ describe("ReplexicaEngine", () => { const engine = new LingoDotDevEngine({ apiKey: "test-key", - engineId: "eng_123", }); const result = await engine.whoami(); const [url, options] = mockFetch.mock.calls[0]; - expect(url).toMatch(/\/users\/me$/); + expect(url).toBe("https://api.lingo.dev/users/me"); expect(options.method).toBe("GET"); expect(options.headers["X-API-Key"]).toBe("test-key"); - expect(options.headers["Authorization"]).toBeUndefined(); expect(result).toEqual({ id: "usr_abc", email: "user@example.com" }); }); + + it("should not include engineId in request body", async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: async () => ({ data: { greeting: "Hola" } }), + }); + + const engine = new LingoDotDevEngine({ + apiKey: "test-key", + }); + + await engine.localizeObject( + { greeting: "Hello" }, + { sourceLocale: "en", targetLocale: "es" }, + ); + + const body = JSON.parse(mockFetch.mock.calls[0][1].body); + expect(body.sourceLocale).toBe("en"); + expect(body.targetLocale).toBe("es"); + expect(body.sessionId).toBeDefined(); + expect(body.engineId).toBeUndefined(); + }); }); describe("hints support", () => { it("should send hints to the backend API", async () => { - // Mock global fetch const mockFetch = vi.fn(); global.fetch = mockFetch as any; @@ -374,6 +412,7 @@ describe("ReplexicaEngine", () => { const engine = new LingoDotDevEngine({ apiKey: "test-api-key", apiUrl: "https://test.api.url", + engineId: "eng_test", }); const hints = { @@ -393,22 +432,19 @@ describe("ReplexicaEngine", () => { }, ); - // Verify fetch was called with correct parameters expect(mockFetch).toHaveBeenCalledTimes(1); const fetchCall = mockFetch.mock.calls[0]; - expect(fetchCall[0]).toBe("https://test.api.url/i18n"); + expect(fetchCall[0]).toBe("https://test.api.url/process/localize"); - // Parse the request body to verify hints are included const requestBody = JSON.parse(fetchCall[1].body); + expect(requestBody.engineId).toBe("eng_test"); expect(requestBody.hints).toEqual(hints); expect(requestBody.data).toEqual({ "brand-name": "Optimum", "team-label": "NHL Team", }); - expect(requestBody.locale).toEqual({ - source: "en", - target: "es", - }); + expect(requestBody.sourceLocale).toBe("en"); + expect(requestBody.targetLocale).toBe("es"); }); it("should handle localizeObject without hints", async () => { @@ -427,6 +463,7 @@ describe("ReplexicaEngine", () => { const engine = new LingoDotDevEngine({ apiKey: "test-api-key", apiUrl: "https://test.api.url", + engineId: "eng_test", }); await engine.localizeObject( diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 329030067..24139cf7f 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -6,7 +6,7 @@ import { TRACKING_EVENTS } from "./utils/tracking-events"; const engineParamsSchema = Z.object({ apiKey: Z.string(), - apiUrl: Z.string().url().default("https://engine.lingo.dev"), + apiUrl: Z.string().url().default("https://api.lingo.dev"), batchSize: Z.number().int().gt(0).lte(250).default(25), idealBatchItemSize: Z.number().int().gt(0).lte(2500).default(250), engineId: Z.string().optional(), @@ -36,20 +36,11 @@ export class LingoDotDevEngine { private readonly sessionId = createId(); - private get isVNext(): boolean { - return !!this.config.engineId; - } - private get headers(): Record { - return this.isVNext - ? { - "Content-Type": "application/json; charset=utf-8", - "X-API-Key": this.config.apiKey, - } - : { - "Content-Type": "application/json; charset=utf-8", - Authorization: `Bearer ${this.config.apiKey}`, - }; + return { + "Content-Type": "application/json; charset=utf-8", + "X-API-Key": this.config.apiKey, + }; } /** @@ -57,11 +48,7 @@ export class LingoDotDevEngine { * @param config - Configuration options for the Engine */ constructor(config: Partial>) { - const parsed = engineParamsSchema.parse(config); - if (!config.apiUrl && parsed.engineId) { - parsed.apiUrl = "https://api.lingo.dev"; - } - this.config = parsed; + this.config = engineParamsSchema.parse(config); } /** @@ -125,7 +112,7 @@ export class LingoDotDevEngine { * @param workflowId - Workflow ID for tracking * @param fast - Whether to use fast mode * @param filePath - Optional file path for metadata - * @param triggerType - Optional trigger type for vNext requests + * @param triggerType - Optional trigger type * @param signal - Optional AbortSignal to cancel the operation * @returns Localized chunk */ @@ -143,32 +130,20 @@ export class LingoDotDevEngine { triggerType?: "cli" | "ci", signal?: AbortSignal, ): Promise> { - const url = this.isVNext - ? `${this.config.apiUrl}/process/${this.config.engineId}/localize` - : `${this.config.apiUrl}/i18n`; - - const body = this.isVNext - ? { - params: { fast }, - sourceLocale, - targetLocale, - data: payload.data, - reference: payload.reference, - hints: payload.hints, - sessionId: this.sessionId, - triggerType, - metadata: filePath ? { filePath } : undefined, - } - : { - params: { workflowId, fast }, - locale: { - source: sourceLocale, - target: targetLocale, - }, - data: payload.data, - reference: payload.reference, - hints: payload.hints, - }; + const url = `${this.config.apiUrl}/process/localize`; + + const body = { + params: { fast }, + sourceLocale, + targetLocale, + data: payload.data, + reference: payload.reference, + hints: payload.hints, + sessionId: this.sessionId, + triggerType, + metadata: filePath ? { filePath } : undefined, + ...(this.config.engineId && { engineId: this.config.engineId }), + }; const res = await fetch(url, { method: "POST", @@ -739,9 +714,7 @@ export class LingoDotDevEngine { trackProps, ); try { - const url = this.isVNext - ? `${this.config.apiUrl}/process/recognize` - : `${this.config.apiUrl}/recognize`; + const url = `${this.config.apiUrl}/process/recognize`; const response = await fetch(url, { method: "POST", @@ -784,13 +757,11 @@ export class LingoDotDevEngine { async whoami( signal?: AbortSignal, ): Promise<{ email: string; id: string } | null> { - const url = this.isVNext - ? `${this.config.apiUrl}/users/me` - : `${this.config.apiUrl}/whoami`; + const url = `${this.config.apiUrl}/users/me`; try { const res = await fetch(url, { - method: this.isVNext ? "GET" : "POST", + method: "GET", headers: this.headers, signal, }); diff --git a/packages/sdk/src/utils/observability.spec.ts b/packages/sdk/src/utils/observability.spec.ts index fde8f108c..507fa29a6 100644 --- a/packages/sdk/src/utils/observability.spec.ts +++ b/packages/sdk/src/utils/observability.spec.ts @@ -110,7 +110,7 @@ describe("trackEvent", () => { // whoami fetch should only be called once due to caching const whoamiCalls = mockFetch.mock.calls.filter( - (call) => typeof call[0] === "string" && call[0].includes("/whoami"), + (call) => typeof call[0] === "string" && call[0].includes("/users/me"), ); expect(whoamiCalls).toHaveLength(1); expect(capture).toHaveBeenCalledTimes(2); @@ -130,7 +130,7 @@ describe("trackEvent", () => { await new Promise((r) => setTimeout(r, 200)); const whoamiCalls = mockFetch.mock.calls.filter( - (call) => typeof call[0] === "string" && call[0].includes("/whoami"), + (call) => typeof call[0] === "string" && call[0].includes("/users/me"), ); expect(whoamiCalls).toHaveLength(2); }); diff --git a/packages/sdk/src/utils/observability.ts b/packages/sdk/src/utils/observability.ts index af9eba907..09eb4667e 100644 --- a/packages/sdk/src/utils/observability.ts +++ b/packages/sdk/src/utils/observability.ts @@ -73,10 +73,10 @@ async function getDistinctId( if (cached) return cached; try { - const res = await fetch(`${apiUrl}/whoami`, { - method: "POST", + const res = await fetch(`${apiUrl}/users/me`, { + method: "GET", headers: { - Authorization: `Bearer ${apiKey}`, + "X-API-Key": apiKey, "Content-Type": "application/json", }, }); diff --git a/packages/spec/src/config.ts b/packages/spec/src/config.ts index b7ad04152..db7e10323 100644 --- a/packages/spec/src/config.ts +++ b/packages/spec/src/config.ts @@ -602,8 +602,32 @@ export const configV1_14Definition = extendConfigDefinition( }, ); +// v1.14 -> v1.15 +// Changes: Add "engineId" field, deprecate "vNext" +export const configV1_15Definition = extendConfigDefinition( + configV1_14Definition, + { + createSchema: (baseSchema) => + baseSchema.extend({ + engineId: Z.string().optional(), + }), + createDefaultValue: (baseDefaultValue) => ({ + ...baseDefaultValue, + version: "1.15", + }), + createUpgrader: (oldConfig) => { + const { vNext, ...rest } = oldConfig as any; + return { + ...rest, + version: "1.15", + ...(vNext && !rest.engineId ? { engineId: vNext } : {}), + }; + }, + }, +); + // exports -export const LATEST_CONFIG_DEFINITION = configV1_14Definition; +export const LATEST_CONFIG_DEFINITION = configV1_15Definition; export type I18nConfig = Z.infer<(typeof LATEST_CONFIG_DEFINITION)["schema"]>;