diff --git a/.changeset/tame-cats-mate.md b/.changeset/tame-cats-mate.md new file mode 100644 index 000000000..be9e73c4a --- /dev/null +++ b/.changeset/tame-cats-mate.md @@ -0,0 +1,5 @@ +--- +"lingo.dev": minor +--- + +Implemented check command to that exits with non-zero code when translations need updating, replacing the lingo.dev i18n --frozen functionality we're deprecating in the future (in favor of run command). diff --git a/packages/cli/src/cli/cmd/check.ts b/packages/cli/src/cli/cmd/check.ts new file mode 100644 index 000000000..6e569b162 --- /dev/null +++ b/packages/cli/src/cli/cmd/check.ts @@ -0,0 +1,156 @@ +import { Command } from "interactive-commander"; +import setup from "./run/setup"; +import plan from "./run/plan"; +import { CmdRunContext, flagsSchema } from "./run/_types"; +import { + renderClear, + renderSpacer, + renderBanner, + renderHero, + pauseIfDebug, +} from "../utils/ui"; +import chalk from "chalk"; +import trackEvent from "../utils/observability"; +import { determineAuthId } from "./run/_utils"; +import { colors } from "../constants"; + +export default new Command() + .command("check") + .description("Check if translations need updating without making changes") + .helpOption("-h, --help", "Show help") + .option( + "--source-locale ", + "Locale to use as source locale. Defaults to i18n.json locale.source", + ) + .option( + "--target-locale ", + "Locale to use as target locale. Defaults to i18n.json locale.targets", + (val: string, prev: string[]) => (prev ? [...prev, val] : [val]), + ) + .option( + "--bucket ", + "Bucket to process", + (val: string, prev: string[]) => (prev ? [...prev, val] : [val]), + ) + .option( + "--file ", + "File to process. Process only files that match this glob pattern in their path. Use quotes around patterns to prevent shell expansion (e.g., --file '**/*.json'). Useful if you have a lot of files and want to focus on a specific one. Specify more files separated by commas or spaces. Accepts glob patterns.", + (val: string, prev: string[]) => (prev ? [...prev, val] : [val]), + ) + .option( + "--key ", + "Key to process. Process only a specific translation key, useful for updating a single entry. Accepts glob patterns.", + (val: string, prev: string[]) => (prev ? [...prev, val] : [val]), + ) + .option( + "--force", + "Ignore lockfile and process all keys, useful for full re-translation", + ) + .option( + "--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( + "--concurrency ", + "Number of concurrent tasks to run", + (val: string) => parseInt(val), + ) + .action(async (args) => { + let authId: string | null = null; + try { + const ctx: CmdRunContext = { + flags: flagsSchema.parse(args), + config: null, + results: new Map(), + tasks: [], + localizer: null, + }; + + await pauseIfDebug(ctx.flags.debug); + await renderClear(); + await renderSpacer(); + await renderBanner(); + await renderHero(); + await renderSpacer(); + + await setup(ctx); + + authId = await determineAuthId(ctx); + + trackEvent(authId, "cmd.check.start", { + config: ctx.config, + flags: ctx.flags, + }); + + await renderSpacer(); + + await plan(ctx); + await renderSpacer(); + + // Display summary of what needs updating + if (ctx.tasks.length > 0) { + console.log(chalk.hex(colors.orange)("[Check Results]")); + console.log( + `${chalk.hex(colors.red)("✗")} Translations need updating: ${chalk.hex( + colors.yellow, + )(ctx.tasks.length.toString())} task(s) found`, + ); + + // Group tasks by target locale for better readability + const tasksByTargetLocale = ctx.tasks.reduce( + (acc, task) => { + if (!acc[task.targetLocale]) { + acc[task.targetLocale] = []; + } + acc[task.targetLocale].push(task); + return acc; + }, + {} as Record, + ); + + Object.entries(tasksByTargetLocale).forEach(([locale, tasks]) => { + console.log( + ` ${chalk.hex(colors.blue)("→")} ${chalk.hex(colors.blue)( + locale, + )}: ${tasks.length} task(s)`, + ); + }); + + console.log( + `\nRun ${chalk.hex(colors.green)( + "lingo.dev run", + )} to update translations.`, + ); + await renderSpacer(); + + trackEvent(authId, "cmd.check.needs_update", { + config: ctx.config, + flags: ctx.flags, + taskCount: ctx.tasks.length, + }); + + process.exit(1); + } else { + console.log(chalk.hex(colors.orange)("[Check Results]")); + console.log( + `${chalk.hex(colors.green)("✓")} All translations are up-to-date`, + ); + await renderSpacer(); + + trackEvent(authId, "cmd.check.up_to_date", { + config: ctx.config, + flags: ctx.flags, + }); + + process.exit(0); + } + } catch (error: any) { + trackEvent(authId || "unknown", "cmd.check.error", {}); + console.error(chalk.red("Error during check:"), error.message); + process.exit(1); + } + }); diff --git a/packages/cli/src/cli/index.ts b/packages/cli/src/cli/index.ts index 86d03ee64..9b49a6372 100644 --- a/packages/cli/src/cli/index.ts +++ b/packages/cli/src/cli/index.ts @@ -20,6 +20,7 @@ import statusCmd from "./cmd/status"; import mayTheFourthCmd from "./cmd/may-the-fourth"; import packageJson from "../../package.json"; import run from "./cmd/run"; +import checkCmd from "./cmd/check"; import purgeCmd from "./cmd/purge"; export default new InteractiveCommand() @@ -58,6 +59,7 @@ Star the the repo :) https://github.com/LingoDotDev/lingo.dev .addCommand(statusCmd) .addCommand(mayTheFourthCmd, { hidden: true }) .addCommand(run) + .addCommand(checkCmd) .addCommand(purgeCmd) .exitOverride((err) => { // Exit with code 0 when help or version is displayed