Skip to content
Closed
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
6 changes: 6 additions & 0 deletions .changeset/tidy-numbers-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"lingo.dev": minor
"@lingo.dev/_sdk": minor
---

[Enhancement]: This PR improves the test command options in the CLI by refining its functionality and usability. Additionally, it introduces the AbortController API to the SDK, enhancing request handling and cancellation support.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ node_modules
.pnp
.pnp.js

.idea

# Local env files
.env
.env.local
Expand Down Expand Up @@ -37,3 +39,4 @@ yarn-error.log*
.DS_Store
*.pem
i18n.cache
i18n.cache
4 changes: 2 additions & 2 deletions packages/cli/src/cli/cmd/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export default new Command()
.command("auth")
.description("Authenticate with Lingo.dev API")
.helpOption("-h, --help", "Show help")
.option("--logout", "Delete existing authentication")
.option("--login", "Authenticate with Lingo.dev API")
.option("--logout", "Delete existing authentication and clear your saved API key.")
.option("--login", "Authenticate with Lingo.dev API.")
.action(async (options) => {
try {
let settings = await getSettings(undefined);
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/src/cli/cmd/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ export default new Command()
.command("cleanup")
.description("Remove keys from target files that do not exist in the source file")
.helpOption("-h, --help", "Show help")
.option("--locale <locale>", "Specific locale to cleanup")
.option("--bucket <bucket>", "Specific bucket to cleanup")
.option("--dry-run", "Show what would be removed without making changes")
.option("--verbose", "Show verbose output")
.option("--locale <locale>", "Clean up only the specified target locale")
.option("--bucket <bucket>", " Clean up only the specified bucket type")
.option("--dry-run", "Show what would be removed without actually modifying any files")
.option("--verbose", "Show detailed output including:\n" +
" - List of keys that would be removed.\n" +
" - Processing steps.")
.action(async function (options) {
const ora = Ora();
const results: any = [];
Expand Down
16 changes: 8 additions & 8 deletions packages/cli/src/cli/cmd/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ export default new Command()
.helpOption("-h, --help", "Show help")
.option("--locale <locale>", "Locale to process", (val: string, prev: string[]) => (prev ? [...prev, val] : [val]))
.option("--bucket <bucket>", "Bucket to process", (val: string, prev: string[]) => (prev ? [...prev, val] : [val]))
.option("--key <key>", "Key to process")
.option("--frozen", `Don't update the translations and fail if an update is needed`)
.option("--force", "Ignore lockfile and process all keys")
.option("--verbose", "Show verbose output")
.option("--interactive", "Interactive mode")
.option("--api-key <api-key>", "Explicitly set the API key to use")
.option("--debug", "Debug mode")
.option("--strict", "Stop on first error")
.option("--key <key>", "Key to process.Process only a specific translation key. Useful for debugging or updating a single entry.")
.option("--frozen", `Run in read-only mode - fails if any translations need updating. Useful for CI/CD pipelines to detect missing translations.`)
.option("--force", "Ignore lockfile and process all keys, forcing a full re-translation.Use with caution as this may incur higher API costs.")
.option("--verbose", "Show detailed output including intermediate processing data and API communication details.")
.option("--interactive", "Enable interactive mode for reviewing and editing translations before they are applied.")
.option("--api-key <api-key>", "Explicitly set the API key to use. Override the default API key from settings.")
.option("--debug", "Pause execution at start for debugging purposes. Waits for user confirmation before proceeding.")
.option("--strict", "Stop processing on first error instead of continuing with other locales/buckets.")
.action(async function (options) {
updateGitignore();

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/cli/cmd/lockfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default new Command()
.command("lockfile")
.description("Create a lockfile if it does not exist")
.helpOption("-h, --help", "Show help")
.option("-f, --force", "Force create a lockfile")
.option("-f, --force", "Force create a lockfile.")
.action(async (options) => {
const flags = flagsSchema.parse(options);
const ora = Ora();
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/cli/cmd/show/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { resolveOverridenLocale } from "@lingo.dev/_spec";
export default new Command()
.command("files")
.description("Print out the list of files managed by Lingo.dev")
.option("--source", "Only show source files")
.option("--target", "Only show target files")
.option("--source", "Only show source files.Files containing the original translations.")
.option("--target", "Only show target files.Files containing translated content.")
.helpOption("-h, --help", "Show help")
.action(async (type) => {
const ora = Ora();
Expand Down
107 changes: 107 additions & 0 deletions packages/sdk/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,110 @@ describe("ReplexicaEngine", () => {
});
});
});



describe("LingoDotDevEngine Abort Handling", () => {
describe("Abort Signal Propagation", () => {
it("should throw an error when abort signal is triggered before localization starts", async () => {
const engine = new LingoDotDevEngine({ apiKey: "test" });
const abortController = new AbortController();

// Abort immediately
abortController.abort();

await expect(
engine.localizeText("Test text", {
sourceLocale: "en",
targetLocale: "es"
}, undefined, abortController.signal)
).rejects.toThrow("Operation was aborted");
});

it("should throw an error when abort signal is triggered during HTML localization", async () => {
const engine = new LingoDotDevEngine({ apiKey: "test" });
const abortController = new AbortController();

// Mock _localizeRaw to simulate a long-running operation
const mockLocalizeRaw = vi.spyOn(engine as any, "_localizeRaw");
mockLocalizeRaw.mockImplementation(async () => {
// Simulate an asynchronous operation
await new Promise(resolve => setTimeout(resolve, 100));
// Abort during the operation
abortController.abort();
return {};
});

await expect(
engine.localizeHtml("<html><body>Test</body></html>", {
sourceLocale: "en",
targetLocale: "es"
}, undefined, abortController.signal)
).rejects.toThrow("Operation was aborted");
});

it("should propagate abort signal through nested method calls", async () => {
const engine = new LingoDotDevEngine({ apiKey: "test" });
const abortController = new AbortController();

// Spy on internal methods to ensure abort signal is passed
const spyLocalizeChunk = vi.spyOn(engine as any, "localizeChunk");
const spyCheckAbortSignal = vi.spyOn(engine as any, "checkAbortSignal");

// Abort during object localization
abortController.abort();

await expect(
engine.localizeObject({ text: "Test" }, {
sourceLocale: "en",
targetLocale: "es"
}, undefined, abortController.signal)
).rejects.toThrow("Operation was aborted");

// Verify abort signal was checked in multiple places
expect(spyCheckAbortSignal).toHaveBeenCalledWith(abortController.signal);
});

it("should handle multiple concurrent abort scenarios", async () => {
const engine = new LingoDotDevEngine({ apiKey: "test" });

const abortControllers = [
new AbortController(),
new AbortController(),
new AbortController()
];

const localizationPromises = abortControllers.map((controller, index) => {
// Stagger abort times
setTimeout(() => controller.abort(), index * 50);

return engine.localizeText(`Test ${index}`, {
sourceLocale: "en",
targetLocale: "es"
}, undefined, controller.signal).catch(err => err);
});

const results = await Promise.all(localizationPromises);

// All promises should result in abort errors
results.forEach(result => {
expect(result.message).toBe("Operation was aborted");
});
});

it("should allow localization when no abort signal is provided", async () => {
const engine = new LingoDotDevEngine({ apiKey: "test" });

// Mock _localizeRaw to return a predictable result
const mockLocalizeRaw = vi.spyOn(engine as any, "_localizeRaw");
mockLocalizeRaw.mockResolvedValue({ text: "Localized Text" });

const result = await engine.localizeText("Test text", {
sourceLocale: "en",
targetLocale: "es"
});

expect(result).toBe("Localized Text");
});
});
});
Loading