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
|
-
//
|
|
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 =
|
|
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
|
|
46
|
-
const namespace = path.basename(
|
|
47
|
-
|
|
48
|
-
const
|
|
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
|
|
82
|
-
namespaces.add(path.basename(
|
|
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, `${
|
|
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}`));
|
package/oclif.manifest.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|