diff --git a/apps/web/src/components/commands.tsx b/apps/web/src/components/commands.tsx index 551386f..93f961a 100644 --- a/apps/web/src/components/commands.tsx +++ b/apps/web/src/components/commands.tsx @@ -103,7 +103,7 @@ export function Commands() { step >= 2 ? "opacity-100" : "opacity-0", )} > - │ es,pt,fr + │ es, pt, fr │ ○ JSON (.json) - = 4 ? "opacity-100" : "opacity-0", - )} - > - │ ○ YAML (.yaml) - - = 4 ? "opacity-100" : "opacity-0", - )} - > - │ ○ Markdown (.md) - │ ○ GPT-3.5 Turbo - = 5 ? "opacity-100" : "opacity-0", - )} - > - │ - - - = 6 ? "opacity-100" : "opacity-0", - )} - > - ◆ Which OpenAI model should be used for translations? - - = 6 ? "opacity-100" : "opacity-0", - )} - > - │ GPT-4 (Default) - = 7 ? "opacity-100" : "opacity-0", + "transition-opacity duration-100 -ml-[1.5px]", + step >= 6 ? "opacity-100" : "opacity-0", )} > └ Configuration file and language files created successfully! diff --git a/examples/i18next/locales/en.json b/examples/i18next/locales/en.json index cfbcd21..34b25ba 100644 --- a/examples/i18next/locales/en.json +++ b/examples/i18next/locales/en.json @@ -5,6 +5,5 @@ }, "interpolated": "Have a nice day, {{name}}!", "pluralKey_one": "This is a nice example.", - "pluralKey_other": "This are nice examples.", - "missing_translation": "This should work" + "pluralKey_other": "This are nice examples." } diff --git a/examples/i18next/locales/sv.json b/examples/i18next/locales/sv.json index 5e4259e..c878c82 100644 --- a/examples/i18next/locales/sv.json +++ b/examples/i18next/locales/sv.json @@ -5,6 +5,5 @@ }, "interpolated": "Ha en trevlig dag, {{name}}!", "pluralKey_one": "Det här är ett fint exempel.", - "pluralKey_other": "Det här är fina exempel.", - "missing_translation": "Detta bör fungera" + "pluralKey_other": "Det här är fina exempel." } \ No newline at end of file diff --git a/examples/next-international/locales/en.ts b/examples/next-international/locales/en.ts index 76ac4f9..a58ae73 100644 --- a/examples/next-international/locales/en.ts +++ b/examples/next-international/locales/en.ts @@ -10,5 +10,4 @@ export default { "scope.more.stars#other": "{count} stars on GitHub", "missing.translation.in.fr": "This should work", "cows#one": "A cow", - "cows#other": "{count} cows", } as const; diff --git a/examples/next-international/locales/fr.ts b/examples/next-international/locales/fr.ts index aecd861..f2deaaa 100644 --- a/examples/next-international/locales/fr.ts +++ b/examples/next-international/locales/fr.ts @@ -9,7 +9,5 @@ export default { "scope.more.stars#one": "1 étoile sur GitHub", "scope.more.stars#other": "{count} étoiles sur GitHub", "missing.translation.in.fr": "Cela devrait fonctionner", - "cows#one": "Une vache", - "cows#other": "{count} vaches", - "hello.world2": "Bonjour le monde !" + "cows#one": "Une vache" } as const; diff --git a/packages/cli/src/commands/clean.ts b/packages/cli/src/commands/clean.ts new file mode 100644 index 0000000..90addc0 --- /dev/null +++ b/packages/cli/src/commands/clean.ts @@ -0,0 +1,99 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { intro, outro, spinner } from "@clack/prompts"; +import chalk from "chalk"; +import { getConfig } from "../utils.js"; + +export async function clean() { + intro("Cleaning unused translations..."); + + try { + const config = await getConfig(); + const s = spinner(); + s.start("Removing unused translation keys..."); + + const { source, targets } = config.locale; + let totalKeysRemoved = 0; + + // Process each file format and pattern + for (const [format, { include }] of Object.entries(config.files)) { + for (const pattern of include) { + const sourcePath = pattern.replace("[locale]", source); + + // Read source file to get reference keys + const sourceContent = await fs.readFile( + path.join(process.cwd(), sourcePath), + "utf-8", + ); + + const sourceKeys = + format === "ts" + ? Object.keys( + Function( + `return ${sourceContent.replace(/export default |as const;/g, "")}`, + )(), + ) + : Object.keys(JSON.parse(sourceContent)); + + // Clean each target locale file + for (const locale of targets) { + const targetPath = pattern.replace("[locale]", locale); + + try { + const targetContent = await fs.readFile( + path.join(process.cwd(), targetPath), + "utf-8", + ); + + const targetObj = + format === "ts" + ? Function( + `return ${targetContent.replace(/export default |as const;/g, "")}`, + )() + : JSON.parse(targetContent); + + const targetKeys = Object.keys(targetObj); + const keysToRemove = targetKeys.filter( + (key) => !sourceKeys.includes(key), + ); + totalKeysRemoved += keysToRemove.length; + + // Remove keys not in source + const cleanedObj = Object.fromEntries( + Object.entries(targetObj).filter(([key]) => + sourceKeys.includes(key), + ), + ); + + // Format and write cleaned content + const finalContent = + format === "ts" + ? `export default ${JSON.stringify(cleanedObj, null, 2)} as const;\n` + : JSON.stringify(cleanedObj, null, 2); + + await fs.writeFile( + path.join(process.cwd(), targetPath), + finalContent, + "utf-8", + ); + } catch (error) { + console.error(chalk.red(`Error cleaning ${targetPath}:`), error); + } + } + } + } + + s.stop("Cleaning completed"); + outro( + totalKeysRemoved > 0 + ? chalk.green( + `Successfully removed ${totalKeysRemoved} unused translation keys!`, + ) + : chalk.green("No unused translations found!"), + ); + } catch (error) { + outro(chalk.red("Failed to clean translations")); + console.error(error); + process.exit(1); + } +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index f0274f2..0f53a8c 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -6,6 +6,7 @@ dotenv.config(); import { select } from "@clack/prompts"; import chalk from "chalk"; import dedent from "dedent"; +import { clean } from "./commands/clean.js"; import { diff } from "./commands/diff.js"; import { init } from "./commands/init.js"; import { instructions } from "./commands/instructions.js"; @@ -41,6 +42,8 @@ const command = { value: "translate", label: "Translate to target languages" }, { value: "instructions", label: "Add custom translation instructions" }, { value: "diff", label: "Check for changes in source locale file" }, + { value: "clean", label: "Clean unused translations" }, + { value: "available", label: "Available commands" }, ], })); @@ -54,14 +57,17 @@ if (command === "init") { instructions(); } else if (command === "diff") { diff(); -} else { - console.log(chalk.bold("\nAvailable commands:")); +} else if (command === "clean") { + clean(); +} else if (command === "available") { console.log(dedent` ${chalk.cyan("init")} Initialize a new Languine configuration ${chalk.cyan("translate")} Translate to all target locales ${chalk.cyan("translate")} ${chalk.gray("")} Translate to a specific locale ${chalk.cyan("instructions")} Add custom translation instructions - + ${chalk.cyan("diff")} Check for changes in source locale file + ${chalk.cyan("clean")} Clean unused translations + ${chalk.cyan("available")} Show available commands Run ${chalk.cyan("languine ")} to execute a command `); } diff --git a/packages/cli/src/prompt.ts b/packages/cli/src/prompt.ts index 7d9c49e..e2265a8 100644 --- a/packages/cli/src/prompt.ts +++ b/packages/cli/src/prompt.ts @@ -12,4 +12,5 @@ Translation Requirements: - Respect existing whitespace and newline patterns - Keep all technical identifiers unchanged - Translate only user-facing strings +- Never add space before a ! or ? `;