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, and errors
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
- - Configurable output directory
150
- - Framework support (Vue, Svelte)
151
- - i18n library presets (`next-intl`, `react-i18next`)
152
- - Watch mode
153
- - Non-AI fallback mode
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 per project in `.i18nizer/`
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(["alt", "aria-label", "placeholder", "title"]);
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(["alt", "aria-label", "placeholder", "title"]);
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
- for (const key of Object.keys(data[namespace])) {
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
- fs.writeFileSync(filePath, JSON.stringify(content, null, 2));
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
  }
@@ -161,5 +161,5 @@
161
161
  ]
162
162
  }
163
163
  },
164
- "version": "0.2.0"
164
+ "version": "0.3.0"
165
165
  }
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.2.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",