i18nizer 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import { Args, Command, Flags } from "@oclif/core";
2
2
  import chalk from "chalk";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import ora from "ora";
5
6
  import { generateTranslations } from "../core/ai/client.js";
@@ -10,6 +11,7 @@ import { parseFile } from "../core/ast/parse-file.js";
10
11
  import { replaceTempKeysWithT } from "../core/ast/replace-text-with-text.js";
11
12
  import { TranslationCache } from "../core/cache/translation-cache.js";
12
13
  import { Deduplicator } from "../core/deduplication/deduplicator.js";
14
+ import { generateAggregator } from "../core/i18n/generate-aggregator.js";
13
15
  import { parseAiJson } from "../core/i18n/parse-ai-json.js";
14
16
  import { saveSourceFile } from "../core/i18n/sace-source-file.js";
15
17
  import { writeLocaleFiles } from "../core/i18n/write-files.js";
@@ -149,6 +151,10 @@ export default class Extract extends Command {
149
151
  saveSourceFile(sourceFile);
150
152
  // Save cache
151
153
  cache.save();
154
+ // Generate aggregator (extract uses home directory for standalone mode)
155
+ const homeDir = os.homedir();
156
+ const standaloneMessagesDir = path.join(homeDir, ".i18nizer", "messages");
157
+ generateAggregator(standaloneMessagesDir);
152
158
  this.log(chalk.green("✨ Component rewritten with t() calls"));
153
159
  this.log(chalk.green(`🌍 JSON files generated using ${provider}`));
154
160
  }
@@ -3,6 +3,7 @@ import chalk from "chalk";
3
3
  import inquirer from "inquirer";
4
4
  import ora from "ora";
5
5
  import { detectFramework, detectI18nLibrary, generateConfig, getMessagesDir, getProjectDir, isProjectInitialized, normalizeI18nLibrary, writeConfig, } from "../core/config/config-manager.js";
6
+ import { ensureI18nizerDirInGitignore } from "../core/config/gitignore-manager.js";
6
7
  export default class Start extends Command {
7
8
  static description = "🚀 Initialize i18nizer in your project with framework presets";
8
9
  static flags = {
@@ -121,6 +122,15 @@ export default class Start extends Command {
121
122
  spinner.start("Setting up messages directory...");
122
123
  const messagesDir = getMessagesDir(cwd, config);
123
124
  spinner.succeed(`✅ Created ${chalk.cyan(config.messages.path + "/")} directory`);
125
+ // Add .i18nizer/ to .gitignore
126
+ spinner.start("Adding .i18nizer/ to .gitignore...");
127
+ const addedToGitignore = ensureI18nizerDirInGitignore(cwd);
128
+ if (addedToGitignore) {
129
+ spinner.succeed(`✅ Added ${chalk.cyan(".i18nizer/")} to .gitignore`);
130
+ }
131
+ else {
132
+ spinner.info(`â„šī¸ ${chalk.cyan(".i18nizer/")} already in .gitignore`);
133
+ }
124
134
  // Summary
125
135
  this.log("");
126
136
  this.log(chalk.green("🎉 i18nizer initialized successfully!"));
@@ -11,6 +11,7 @@ import { replaceTempKeysWithT } from "../core/ast/replace-text-with-text.js";
11
11
  import { TranslationCache } from "../core/cache/translation-cache.js";
12
12
  import { detectFramework, generateConfig, getMessagesDir, getProjectDir, isProjectInitialized, loadConfig, } from "../core/config/config-manager.js";
13
13
  import { Deduplicator } from "../core/deduplication/deduplicator.js";
14
+ import { generateAggregator } from "../core/i18n/generate-aggregator.js";
14
15
  import { parseAiJson } from "../core/i18n/parse-ai-json.js";
15
16
  import { saveSourceFile } from "../core/i18n/sace-source-file.js";
16
17
  import { writeLocaleFiles } from "../core/i18n/write-files.js";
@@ -76,7 +77,7 @@ export default class Translate extends Command {
76
77
  // Override config with flags if provided
77
78
  const locales = flags.locales
78
79
  ? flags.locales.split(",")
79
- : [config.messages.defaultLocale, "es"]; // Default fallback
80
+ : config.messages.locales || [config.messages.defaultLocale, "es"]; // Use config locales or default fallback
80
81
  let provider = "huggingface";
81
82
  if (flags.provider) {
82
83
  const p = flags.provider.toLowerCase();
@@ -243,6 +244,11 @@ export default class Translate extends Command {
243
244
  // Save cache
244
245
  if (!flags["dry-run"]) {
245
246
  cache.save();
247
+ // Generate aggregator if project is initialized
248
+ if (isInitialized) {
249
+ const messagesDir = getMessagesDir(cwd, config);
250
+ generateAggregator(messagesDir);
251
+ }
246
252
  }
247
253
  // Get statistics from deduplicator
248
254
  const stats = deduplicator.getStats();
@@ -0,0 +1,37 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const I18NIZER_DIR = ".i18nizer/";
4
+ /**
5
+ * Ensures that .i18nizer/ folder is added to .gitignore
6
+ */
7
+ export function ensureI18nizerDirInGitignore(cwd) {
8
+ const gitignorePath = path.join(cwd, ".gitignore");
9
+ const i18nizerDirPath = path.join(cwd, ".i18nizer");
10
+ // Check if .i18nizer directory exists
11
+ if (!fs.existsSync(i18nizerDirPath)) {
12
+ return false;
13
+ }
14
+ // Create .gitignore if it doesn't exist
15
+ if (!fs.existsSync(gitignorePath)) {
16
+ fs.writeFileSync(gitignorePath, "", "utf8");
17
+ }
18
+ // Read current .gitignore content
19
+ const gitignoreContent = fs.readFileSync(gitignorePath, "utf8");
20
+ const lines = gitignoreContent.split("\n");
21
+ // Check if .i18nizer/ is already ignored (exact match or pattern match)
22
+ const isAlreadyIgnored = lines.some((line) => line.trim() === I18NIZER_DIR ||
23
+ line.trim() === `/${I18NIZER_DIR}` ||
24
+ line.trim() === ".i18nizer" ||
25
+ line.trim() === "/.i18nizer" ||
26
+ line.trim() === "**/.i18nizer/" ||
27
+ line.trim() === "**/.i18nizer");
28
+ if (isAlreadyIgnored) {
29
+ return false;
30
+ }
31
+ // Add .i18nizer/ to .gitignore
32
+ const newContent = gitignoreContent.trim() === ""
33
+ ? `${I18NIZER_DIR}\n`
34
+ : `${gitignoreContent}${gitignoreContent.endsWith("\n") ? "" : "\n"}${I18NIZER_DIR}\n`;
35
+ fs.writeFileSync(gitignorePath, newContent, "utf8");
36
+ return true;
37
+ }
@@ -0,0 +1,86 @@
1
+ import chalk from "chalk";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ /**
5
+ * Generates the aggregator TypeScript file that imports all translation JSON files
6
+ * and exports them as a single messages object.
7
+ */
8
+ export function generateAggregator(messagesDir, outputDir) {
9
+ const baseMessagesDir = path.resolve(messagesDir);
10
+ if (!fs.existsSync(baseMessagesDir)) {
11
+ console.log(chalk.yellow("âš ī¸ Messages directory does not exist, skipping aggregator generation"));
12
+ return;
13
+ }
14
+ // Scan for locales and namespaces
15
+ const locales = fs
16
+ .readdirSync(baseMessagesDir, { withFileTypes: true })
17
+ .filter((dirent) => dirent.isDirectory())
18
+ .map((dirent) => dirent.name)
19
+ .sort();
20
+ if (locales.length === 0) {
21
+ console.log(chalk.yellow("âš ī¸ No locale directories found, skipping aggregator generation"));
22
+ return;
23
+ }
24
+ // Collect all JSON files organized by locale
25
+ const filesByLocale = new Map();
26
+ for (const locale of locales) {
27
+ const localeDir = path.join(baseMessagesDir, locale);
28
+ const files = fs
29
+ .readdirSync(localeDir)
30
+ .filter((file) => file.endsWith(".json"))
31
+ .sort();
32
+ if (files.length > 0) {
33
+ filesByLocale.set(locale, files);
34
+ }
35
+ }
36
+ if (filesByLocale.size === 0) {
37
+ console.log(chalk.yellow("âš ī¸ No JSON files found, skipping aggregator generation"));
38
+ return;
39
+ }
40
+ // Generate imports and exports
41
+ const imports = [];
42
+ const exportsByLocale = new Map();
43
+ for (const [locale, files] of filesByLocale) {
44
+ const localeExports = [];
45
+ for (const file of files) {
46
+ const namespace = path.basename(file, ".json");
47
+ const importName = `${namespace}_${locale}`;
48
+ const relativePath = `../messages/${locale}/${file}`;
49
+ imports.push(`import ${importName} from "${relativePath}";`);
50
+ localeExports.push(` ...${importName},`);
51
+ }
52
+ exportsByLocale.set(locale, localeExports);
53
+ }
54
+ // Build the output content
55
+ const lines = [
56
+ "/**",
57
+ " * Auto-generated aggregator file.",
58
+ " * DO NOT EDIT MANUALLY - This file is regenerated by i18nizer.",
59
+ " */",
60
+ "",
61
+ ...imports,
62
+ "",
63
+ "export const messages = {",
64
+ ];
65
+ for (const [locale, exports] of exportsByLocale) {
66
+ lines.push(` ${locale}: {`);
67
+ lines.push(...exports);
68
+ lines.push(" },");
69
+ }
70
+ lines.push("} as const;");
71
+ lines.push("");
72
+ // Write the file
73
+ const i18nDir = outputDir ?? path.join(path.dirname(baseMessagesDir), "i18n");
74
+ fs.mkdirSync(i18nDir, { recursive: true });
75
+ const outputPath = path.join(i18nDir, "messages.generated.ts");
76
+ fs.writeFileSync(outputPath, lines.join("\n"), "utf8");
77
+ console.log(chalk.green(`✨ Aggregator generated: ${outputPath}`));
78
+ // Log statistics
79
+ const namespaces = new Set();
80
+ for (const files of filesByLocale.values()) {
81
+ for (const file of files) {
82
+ namespaces.add(path.basename(file, ".json"));
83
+ }
84
+ }
85
+ console.log(chalk.cyan(`đŸ“Ļ Processed ${locales.length} locales, ${namespaces.size} namespaces`));
86
+ }
@@ -32,6 +32,7 @@ export const DEFAULT_CONFIG = {
32
32
  messages: {
33
33
  defaultLocale: "en",
34
34
  format: "json",
35
+ locales: ["en", "es"],
35
36
  path: "messages",
36
37
  },
37
38
  };
@@ -307,5 +307,5 @@
307
307
  ]
308
308
  }
309
309
  },
310
- "version": "0.5.1"
310
+ "version": "0.6.0"
311
311
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "i18nizer",
3
3
  "description": "CLI to extract texts from JSX/TSX and generate i18n JSON with AI translations",
4
- "version": "0.5.1",
4
+ "version": "0.6.0",
5
5
  "author": "Yoannis Sanchez Soto",
6
6
  "bin": "./bin/run.js",
7
7
  "bugs": "https://github.com/yossTheDev/i18nizer/issues",