i18nizer 0.6.1 → 0.6.2

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.
@@ -0,0 +1,37 @@
1
+ import { Command } from "@oclif/core";
2
+ import chalk from "chalk";
3
+ import { getMessagesDir, isProjectInitialized, loadConfig, } from "../core/config/config-manager.js";
4
+ import { generateAggregator } from "../core/i18n/generate-aggregator.js";
5
+ export default class Regenerate extends Command {
6
+ static description = "♻️ Regenerate the messages.generated.ts aggregator file from JSON files";
7
+ static examples = [
8
+ "<%= config.bin %> <%= command.id %>",
9
+ ];
10
+ async run() {
11
+ const cwd = process.cwd();
12
+ // Check if project is initialized
13
+ if (!isProjectInitialized(cwd)) {
14
+ this.error(chalk.red("❌ Project is not initialized.") +
15
+ "\n" +
16
+ chalk.yellow("💡 Run") +
17
+ " " +
18
+ chalk.bold("i18nizer start") +
19
+ " " +
20
+ chalk.yellow("to initialize the project first."));
21
+ }
22
+ // Load config
23
+ const config = loadConfig(cwd);
24
+ if (!config) {
25
+ this.error(chalk.red("❌ Could not load project configuration."));
26
+ }
27
+ this.log(chalk.cyan("📋 Regenerating aggregator..."));
28
+ // Get messages directory
29
+ const messagesDir = getMessagesDir(cwd, config);
30
+ // Generate aggregator
31
+ generateAggregator(messagesDir);
32
+ this.log("");
33
+ this.log(chalk.green("✅ Aggregator regenerated successfully!"));
34
+ this.log(chalk.gray(" All translation JSON files have been imported."));
35
+ this.log(chalk.gray(" File: ") + chalk.cyan("i18n/messages.generated.ts"));
36
+ }
37
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Utility functions for handling filename conversions for i18nizer.
3
+ *
4
+ * JSON files use lowercase-hyphen format (e.g., "notification-item.json")
5
+ * TypeScript identifiers use PascalCase (e.g., "NotificationItem_en")
6
+ */
7
+ /**
8
+ * Convert a component name to a valid JSON filename (lowercase with hyphens).
9
+ *
10
+ * @param componentName - The component name (e.g., "NotificationItem", "CollapsibleText")
11
+ * @returns Lowercase hyphenated filename without extension (e.g., "notification-item", "collapsible-text")
12
+ *
13
+ * @example
14
+ * componentNameToFilename("NotificationItem") // "notification-item"
15
+ * componentNameToFilename("CollapsibleText") // "collapsible-text"
16
+ * componentNameToFilename("DeleteModel") // "delete-model"
17
+ */
18
+ export function componentNameToFilename(componentName) {
19
+ // Convert PascalCase/camelCase to lowercase-hyphen format
20
+ return componentName
21
+ // Insert hyphen before uppercase letters (except first)
22
+ .replaceAll(/([A-Z])/g, (match, letter, index) => index === 0 ? letter.toLowerCase() : `-${letter.toLowerCase()}`)
23
+ // Remove leading hyphen if any
24
+ .replace(/^-/, "");
25
+ }
26
+ /**
27
+ * Convert a filename to a valid TypeScript identifier (PascalCase).
28
+ *
29
+ * @param filename - The filename without extension (e.g., "notification-item", "collapsible-text")
30
+ * @returns PascalCase identifier (e.g., "NotificationItem", "CollapsibleText")
31
+ *
32
+ * @example
33
+ * filenameToIdentifier("notification-item") // "NotificationItem"
34
+ * filenameToIdentifier("collapsible-text") // "CollapsibleText"
35
+ * filenameToIdentifier("delete-model") // "DeleteModel"
36
+ */
37
+ export function filenameToIdentifier(filename) {
38
+ // Split by hyphens and convert to PascalCase
39
+ return filename
40
+ .split("-")
41
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
42
+ .join("");
43
+ }
@@ -1,6 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import { filenameToIdentifier } from "./filename-utils.js";
4
5
  /**
5
6
  * Generates the aggregator TypeScript file that imports all translation JSON files
6
7
  * and exports them as a single messages object.
@@ -21,14 +22,31 @@ export function generateAggregator(messagesDir, outputDir) {
21
22
  console.log(chalk.yellow("⚠️ No locale directories found, skipping aggregator generation"));
22
23
  return;
23
24
  }
24
- // Collect all JSON files organized by locale
25
+ // Recursively collect all JSON files organized by locale
25
26
  const filesByLocale = new Map();
27
+ /**
28
+ * Recursively find all JSON files in a directory
29
+ */
30
+ function findJsonFiles(dir, baseDir) {
31
+ const results = [];
32
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
33
+ for (const entry of entries) {
34
+ const fullPath = path.join(dir, entry.name);
35
+ if (entry.isDirectory()) {
36
+ // Recursively scan subdirectories
37
+ results.push(...findJsonFiles(fullPath, baseDir));
38
+ }
39
+ else if (entry.isFile() && entry.name.endsWith(".json")) {
40
+ // Get relative path from the base locale directory
41
+ const relativePath = path.relative(baseDir, fullPath);
42
+ results.push({ filename: entry.name, relativePath });
43
+ }
44
+ }
45
+ return results;
46
+ }
26
47
  for (const locale of locales) {
27
48
  const localeDir = path.join(baseMessagesDir, locale);
28
- const files = fs
29
- .readdirSync(localeDir)
30
- .filter((file) => file.endsWith(".json"))
31
- .sort();
49
+ const files = findJsonFiles(localeDir, localeDir).sort((a, b) => a.relativePath.localeCompare(b.relativePath));
32
50
  if (files.length > 0) {
33
51
  filesByLocale.set(locale, files);
34
52
  }
@@ -42,10 +60,21 @@ export function generateAggregator(messagesDir, outputDir) {
42
60
  const exportsByLocale = new Map();
43
61
  for (const [locale, files] of filesByLocale) {
44
62
  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}`;
63
+ for (const fileInfo of files) {
64
+ const namespace = path.basename(fileInfo.filename, ".json");
65
+ // Convert filename to valid TypeScript identifier (kebab-case to PascalCase)
66
+ const identifier = filenameToIdentifier(namespace);
67
+ // Create unique import name including subdirectory path to avoid conflicts
68
+ // Replace path separators with underscores and sanitize
69
+ const pathPrefix = path.dirname(fileInfo.relativePath)
70
+ .split(path.sep)
71
+ .filter((part) => part !== ".")
72
+ .map((part) => filenameToIdentifier(part))
73
+ .join("");
74
+ const importName = pathPrefix
75
+ ? `${pathPrefix}_${identifier}_${locale}`
76
+ : `${identifier}_${locale}`;
77
+ const relativePath = `../messages/${locale}/${fileInfo.relativePath.split(path.sep).join("/")}`;
49
78
  imports.push(`import ${importName} from "${relativePath}";`);
50
79
  localeExports.push(` ...${importName},`);
51
80
  }
@@ -63,12 +92,9 @@ export function generateAggregator(messagesDir, outputDir) {
63
92
  "export const messages = {",
64
93
  ];
65
94
  for (const [locale, exports] of exportsByLocale) {
66
- lines.push(` ${locale}: {`);
67
- lines.push(...exports);
68
- lines.push(" },");
95
+ lines.push(` ${locale}: {`, ...exports, " },");
69
96
  }
70
- lines.push("} as const;");
71
- lines.push("");
97
+ lines.push("} as const;", "");
72
98
  // Write the file
73
99
  const i18nDir = outputDir ?? path.join(path.dirname(baseMessagesDir), "i18n");
74
100
  fs.mkdirSync(i18nDir, { recursive: true });
@@ -78,8 +104,8 @@ export function generateAggregator(messagesDir, outputDir) {
78
104
  // Log statistics
79
105
  const namespaces = new Set();
80
106
  for (const files of filesByLocale.values()) {
81
- for (const file of files) {
82
- namespaces.add(path.basename(file, ".json"));
107
+ for (const fileInfo of files) {
108
+ namespaces.add(path.basename(fileInfo.filename, ".json"));
83
109
  }
84
110
  }
85
111
  console.log(chalk.cyan(`📦 Processed ${locales.length} locales, ${namespaces.size} namespaces`));
@@ -2,9 +2,12 @@ import chalk from "chalk";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import { componentNameToFilename } from "./filename-utils.js";
5
6
  const DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".i18nizer", "messages");
6
7
  export function writeLocaleFiles(namespace, data, locales, outputDir) {
7
8
  const baseDir = outputDir ?? DEFAULT_CONFIG_DIR;
9
+ // Convert namespace to lowercase-hyphen format for filename
10
+ const filename = componentNameToFilename(namespace);
8
11
  for (const locale of locales) {
9
12
  const content = {};
10
13
  content[namespace] = {};
@@ -15,7 +18,7 @@ export function writeLocaleFiles(namespace, data, locales, outputDir) {
15
18
  }
16
19
  const dir = path.join(baseDir, locale);
17
20
  fs.mkdirSync(dir, { recursive: true });
18
- const filePath = path.join(dir, `${namespace}.json`);
21
+ const filePath = path.join(dir, `${filename}.json`);
19
22
  // Use 2-space indentation for clean, readable JSON
20
23
  fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + "\n");
21
24
  console.log(chalk.green(`💾 Locale file saved: ${filePath}`));
@@ -102,6 +102,29 @@
102
102
  "keys.js"
103
103
  ]
104
104
  },
105
+ "regenerate": {
106
+ "aliases": [],
107
+ "args": {},
108
+ "description": "♻️ Regenerate the messages.generated.ts aggregator file from JSON files",
109
+ "examples": [
110
+ "<%= config.bin %> <%= command.id %>"
111
+ ],
112
+ "flags": {},
113
+ "hasDynamicHelp": false,
114
+ "hiddenAliases": [],
115
+ "id": "regenerate",
116
+ "pluginAlias": "i18nizer",
117
+ "pluginName": "i18nizer",
118
+ "pluginType": "core",
119
+ "strict": true,
120
+ "enableJsonFlag": false,
121
+ "isESM": true,
122
+ "relativePath": [
123
+ "dist",
124
+ "commands",
125
+ "regenerate.js"
126
+ ]
127
+ },
105
128
  "start": {
106
129
  "aliases": [],
107
130
  "args": {},
@@ -307,5 +330,5 @@
307
330
  ]
308
331
  }
309
332
  },
310
- "version": "0.6.1"
333
+ "version": "0.6.2"
311
334
  }
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.6.1",
4
+ "version": "0.6.2",
5
5
  "author": "Yoannis Sanchez Soto",
6
6
  "bin": "./bin/run.js",
7
7
  "bugs": "https://github.com/yossTheDev/i18nizer/issues",