i18nizer 0.2.0 → 0.3.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.
package/README.md
CHANGED
|
@@ -140,24 +140,55 @@ export function Login() {
|
|
|
140
140
|
- Always generates **English camelCase keys**
|
|
141
141
|
- Supports **any number of locales**
|
|
142
142
|
- Isolated TypeScript parsing (no project tsconfig required)
|
|
143
|
-
- Friendly logs
|
|
143
|
+
- Friendly logs and errors
|
|
144
|
+
|
|
145
|
+
### Supported Extraction Cases
|
|
146
|
+
|
|
147
|
+
- **JSX text children**: `<div>Hello</div>`
|
|
148
|
+
- **JSX attributes**: `placeholder`, `title`, `alt`, `aria-label`, `aria-placeholder`, `label`, `text`, `tooltip`, `helperText`
|
|
149
|
+
- **String literals**: `placeholder="Enter name"`
|
|
150
|
+
- **Curly-braced strings**: `placeholder={"Enter name"}`
|
|
151
|
+
- **Template literals**: `` placeholder={`Enter name`} ``
|
|
152
|
+
- **Template literals with placeholders**: `` <p>{`Hello ${name}`}</p> ``
|
|
153
|
+
- **Ternary operators**: `placeholder={condition ? "Text A" : "Text B"}`
|
|
154
|
+
- **Logical AND**: `{condition && "Visible text"}`
|
|
155
|
+
- **Logical OR**: `{condition || "Fallback text"}`
|
|
156
|
+
- **Nested expressions**: Complex combinations of the above
|
|
157
|
+
|
|
158
|
+
### Filtering & Quality
|
|
159
|
+
|
|
160
|
+
- **Skips non-translatable content**: Single symbols (`*`, `|`), punctuation-only strings (`...`), whitespace
|
|
161
|
+
- **Deterministic key generation**: Same input always produces the same key
|
|
162
|
+
- **Stable JSON output**: Alphabetically sorted keys, consistent formatting
|
|
163
|
+
- **2-space indentation**: Clean and diff-friendly JSON files
|
|
144
164
|
|
|
145
165
|
---
|
|
146
166
|
|
|
147
167
|
## 🔮 Roadmap
|
|
148
168
|
|
|
149
|
-
-
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
-
|
|
153
|
-
-
|
|
169
|
+
- [ ] Cross-file string deduplication
|
|
170
|
+
- [ ] Key reuse mechanism
|
|
171
|
+
- [ ] Configurable output directory
|
|
172
|
+
- [ ] Framework support (Vue, Svelte)
|
|
173
|
+
- [ ] i18n library presets (`next-intl`, `react-i18next`)
|
|
174
|
+
- [ ] Watch mode
|
|
175
|
+
- [ ] Non-AI fallback mode
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## ⚠️ Current Limitations
|
|
180
|
+
|
|
181
|
+
- Does not yet deduplicate identical strings across files
|
|
182
|
+
- AI-generated keys may vary between runs (deterministic fallback available)
|
|
183
|
+
- Only supports React JSX/TSX (no Vue, Svelte yet)
|
|
184
|
+
- Does not handle runtime-only string generation
|
|
154
185
|
|
|
155
186
|
---
|
|
156
187
|
|
|
157
188
|
## ⚠️ Notes
|
|
158
189
|
|
|
159
190
|
- API keys are **never committed**
|
|
160
|
-
- JSON files are stored
|
|
191
|
+
- JSON files are stored in `/{HOME}/.i18nizer/`
|
|
161
192
|
- Designed for incremental adoption
|
|
162
193
|
|
|
163
194
|
---
|
|
@@ -4,7 +4,17 @@ import { isTranslatableString } from "./is-translatable.js";
|
|
|
4
4
|
let tempIdCounter = 0;
|
|
5
5
|
const allowedFunctions = new Set(["alert", "confirm", "prompt"]);
|
|
6
6
|
const allowedMemberFunctions = new Set(["toast.error", "toast.info", "toast.success", "toast.warn"]);
|
|
7
|
-
const allowedProps = new Set([
|
|
7
|
+
const allowedProps = new Set([
|
|
8
|
+
"alt",
|
|
9
|
+
"aria-label",
|
|
10
|
+
"aria-placeholder",
|
|
11
|
+
"helperText",
|
|
12
|
+
"label",
|
|
13
|
+
"placeholder",
|
|
14
|
+
"text",
|
|
15
|
+
"title",
|
|
16
|
+
"tooltip",
|
|
17
|
+
]);
|
|
8
18
|
// --- Helpers ---
|
|
9
19
|
function processTemplateLiteral(node) {
|
|
10
20
|
if (Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
/* eslint-disable complexity */
|
|
2
2
|
import { Node } from "ts-morph";
|
|
3
3
|
// Allowed JSX props to replace text
|
|
4
|
-
const allowedProps = new Set([
|
|
4
|
+
const allowedProps = new Set([
|
|
5
|
+
"alt",
|
|
6
|
+
"aria-label",
|
|
7
|
+
"aria-placeholder",
|
|
8
|
+
"helperText",
|
|
9
|
+
"label",
|
|
10
|
+
"placeholder",
|
|
11
|
+
"text",
|
|
12
|
+
"title",
|
|
13
|
+
"tooltip",
|
|
14
|
+
]);
|
|
5
15
|
// Allowed functions for simple calls
|
|
6
16
|
const allowedFunctions = new Set(["alert", "confirm", "prompt"]);
|
|
7
17
|
// Allowed member functions (e.g., toast.error)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a deterministic, human-readable translation key from text.
|
|
3
|
+
* This is used as a fallback when AI-generated keys are not available.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Convert a string to camelCase
|
|
7
|
+
*/
|
|
8
|
+
function toCamelCase(str) {
|
|
9
|
+
return str
|
|
10
|
+
.replaceAll(/[^\w\s]/g, " ") // Replace special chars (note: \w includes underscores, so this won't replace them)
|
|
11
|
+
.replaceAll("_", " ") // Explicitly replace underscores with spaces
|
|
12
|
+
.split(/\s+/) // Split by whitespace
|
|
13
|
+
.filter(Boolean) // Remove empty strings
|
|
14
|
+
.map((word, index) => {
|
|
15
|
+
const lower = word.toLowerCase();
|
|
16
|
+
return index === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
17
|
+
})
|
|
18
|
+
.join("");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Generate a deterministic key from text.
|
|
22
|
+
*
|
|
23
|
+
* Rules:
|
|
24
|
+
* - Remove special characters
|
|
25
|
+
* - Convert to camelCase
|
|
26
|
+
* - Limit to first 4-5 words for readability
|
|
27
|
+
* - Ensure minimum length
|
|
28
|
+
*
|
|
29
|
+
* @param text - The text to generate a key from
|
|
30
|
+
* @returns A camelCase key
|
|
31
|
+
*/
|
|
32
|
+
export function generateKey(text) {
|
|
33
|
+
// Remove template literal placeholders like {name}
|
|
34
|
+
const cleanedText = text.replaceAll(/\{[^}]+\}/g, "");
|
|
35
|
+
// Replace apostrophes followed by 's' with just 's' (e.g., "User's" -> "Users")
|
|
36
|
+
const normalizedText = cleanedText.replaceAll(/'s\b/g, "s");
|
|
37
|
+
// Split into words and take first 4-5 significant words
|
|
38
|
+
const words = normalizedText
|
|
39
|
+
.replaceAll(/[^\w\s]/g, " ") // Replace special chars (note: \w includes underscores, so this won't replace them)
|
|
40
|
+
.replaceAll("_", " ") // Explicitly replace underscores with spaces
|
|
41
|
+
.split(/\s+/)
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.slice(0, 5);
|
|
44
|
+
if (words.length === 0) {
|
|
45
|
+
return "text";
|
|
46
|
+
}
|
|
47
|
+
const key = toCamelCase(words.join(" "));
|
|
48
|
+
// Ensure minimum key length
|
|
49
|
+
if (key.length < 3) {
|
|
50
|
+
return "text";
|
|
51
|
+
}
|
|
52
|
+
return key;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Generate a unique key by appending a counter if needed.
|
|
56
|
+
*
|
|
57
|
+
* @param baseKey - The base key
|
|
58
|
+
* @param existingKeys - Set of already used keys
|
|
59
|
+
* @returns A unique key
|
|
60
|
+
*/
|
|
61
|
+
export function generateUniqueKey(baseKey, existingKeys) {
|
|
62
|
+
if (!existingKeys.has(baseKey)) {
|
|
63
|
+
return baseKey;
|
|
64
|
+
}
|
|
65
|
+
let counter = 2;
|
|
66
|
+
let uniqueKey = `${baseKey}${counter}`;
|
|
67
|
+
while (existingKeys.has(uniqueKey)) {
|
|
68
|
+
counter++;
|
|
69
|
+
uniqueKey = `${baseKey}${counter}`;
|
|
70
|
+
}
|
|
71
|
+
return uniqueKey;
|
|
72
|
+
}
|
|
@@ -7,13 +7,16 @@ export function writeLocaleFiles(namespace, data, locales) {
|
|
|
7
7
|
for (const locale of locales) {
|
|
8
8
|
const content = {};
|
|
9
9
|
content[namespace] = {};
|
|
10
|
-
|
|
10
|
+
// Sort keys for stable output
|
|
11
|
+
const sortedKeys = Object.keys(data[namespace]).sort();
|
|
12
|
+
for (const key of sortedKeys) {
|
|
11
13
|
content[namespace][key] = data[namespace][key][locale];
|
|
12
14
|
}
|
|
13
15
|
const dir = path.join(CONFIG_DIR, locale);
|
|
14
16
|
fs.mkdirSync(dir, { recursive: true });
|
|
15
17
|
const filePath = path.join(dir, `${namespace}.json`);
|
|
16
|
-
|
|
18
|
+
// Use 2-space indentation for clean, readable JSON
|
|
19
|
+
fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + "\n");
|
|
17
20
|
console.log(chalk.green(`💾 Locale file saved: ${filePath}`));
|
|
18
21
|
}
|
|
19
22
|
}
|
package/oclif.manifest.json
CHANGED
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.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"author": "Yoannis Sanchez Soto",
|
|
6
6
|
"bin": "./bin/run.js",
|
|
7
7
|
"bugs": "https://github.com/yossTheDev/i18nizer/issues",
|