diff --git a/.changeset/wicked-baboons-bake.md b/.changeset/wicked-baboons-bake.md new file mode 100644 index 000000000..576f8d42e --- /dev/null +++ b/.changeset/wicked-baboons-bake.md @@ -0,0 +1,5 @@ +--- +"lingo.dev": patch +--- + +Fix parsing of comma-separated locale inputs with spaces and quotes during init diff --git a/packages/cli/src/cli/cmd/init.ts b/packages/cli/src/cli/cmd/init.ts index 6ce9c8924..1ecc09422 100644 --- a/packages/cli/src/cli/cmd/init.ts +++ b/packages/cli/src/cli/cmd/init.ts @@ -30,11 +30,25 @@ const throwHelpError = (option: string, value: string) => { if (value === "help") { openUrl("/go/call"); } + + // Better error message for invalid locale or bucket format inputs, with instructions to get help for supported formats throw new Error( - `Invalid ${option}: ${value}\n\nDo you need support for ${value} ${option}? Type "help" and we will.`, + `Invalid ${option}: "${value}".\n\n` + + `Accepted formats:\n` + + ` es,fr\n` + + ` es fr\n` + + ` 'es', 'fr'\n\n` + + `If you need support for "${value}" ${option}, type "help" and we will.`, ); }; +// Parses comma or space separated list of values, removing extra whitespace and quotes +export const parseListInput = (value: string): string[] => + value + .split(/[,\s]+/) + .map((v) => v.replace(/^['"]|['"]$/g, "")) + .filter(Boolean); + export default new InteractiveCommand() .command("init") .description("Create i18n.json configuration file for a new project") @@ -67,10 +81,8 @@ export default new InteractiveCommand() "-t --targets ", "Target languages to translate to. Accepts locale codes like 'es', 'fr', 'de-AT' separated by commas or spaces. Defaults to 'es'", ) - .argParser((value) => { - const values = ( - value.includes(",") ? value.split(",") : value.split(" ") - ) as LocaleCode[]; + .argParser((value: string) => { + const values = parseListInput(value) as LocaleCode[]; values.forEach((value) => { try { resolveLocaleCode(value); @@ -102,9 +114,7 @@ export default new InteractiveCommand() ) .argParser((value) => { if (!value || value.length === 0) return []; - const values = value.includes(",") - ? value.split(",") - : value.split(" "); + const values = parseListInput(value); for (const p of values) { try { @@ -118,7 +128,6 @@ export default new InteractiveCommand() } } return values; - }) .prompt(undefined) // make non-interactive .default([]), @@ -191,9 +200,7 @@ export default new InteractiveCommand() const customPaths = await input({ message: "Enter paths to use", }); - selectedPatterns = customPaths.includes(",") - ? customPaths.split(",") - : customPaths.split(" "); + selectedPatterns = parseListInput(customPaths); } newConfig.buckets = { diff --git a/packages/cli/src/cli/cmd/parseListInput.spec.ts b/packages/cli/src/cli/cmd/parseListInput.spec.ts new file mode 100644 index 000000000..f29983d59 --- /dev/null +++ b/packages/cli/src/cli/cmd/parseListInput.spec.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from "vitest"; +import { parseListInput } from "./init"; + +describe("parseListInput", () => { + it("parses comma-separated values", () => { + expect(parseListInput("es,fr")).toEqual(["es", "fr"]); + }); + + it("parses comma-separated values with whitespace", () => { + expect(parseListInput("es, fr")).toEqual(["es", "fr"]); + }); + + it("parses space-separated values", () => { + expect(parseListInput("es fr")).toEqual(["es", "fr"]); + }); + + it("parses single-quoted values", () => { + expect(parseListInput("'es', 'fr'")).toEqual(["es", "fr"]); + }); + + it("parses double-quoted values", () => { + expect(parseListInput('"es", "fr"')).toEqual(["es", "fr"]); + }); + + it("returns empty array for empty input", () => { + expect(parseListInput("")).toEqual([]); + }); + + it("returns empty array for whitespace-only input", () => { + expect(parseListInput(" ")).toEqual([]); + }); + + it("filters duplicate separators", () => { + expect(parseListInput("es,,fr")).toEqual(["es", "fr"]); + }); + + it("parses outer-quoted comma-separated lists", () => { + expect(parseListInput("'es,fr'")).toEqual(["es", "fr"]); + }); +});