@idealyst/translate 1.2.3

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/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@idealyst/translate",
3
+ "version": "1.2.3",
4
+ "description": "Cross-platform internationalization for Idealyst Framework",
5
+ "readme": "README.md",
6
+ "main": "src/index.ts",
7
+ "module": "src/index.ts",
8
+ "types": "src/index.ts",
9
+ "react-native": "src/index.native.ts",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/IdealystIO/idealyst-framework.git",
13
+ "directory": "packages/translate"
14
+ },
15
+ "author": "Your Name <your.email@example.com>",
16
+ "license": "MIT",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "react-native": "./src/index.native.ts",
23
+ "import": "./src/index.ts",
24
+ "require": "./src/index.ts",
25
+ "types": "./src/index.ts"
26
+ },
27
+ "./plugin": {
28
+ "import": "./src/babel/plugin.js",
29
+ "require": "./src/babel/plugin.js"
30
+ },
31
+ "./config": {
32
+ "import": "./src/config/index.ts",
33
+ "require": "./src/config/index.ts",
34
+ "types": "./src/config/index.ts"
35
+ }
36
+ },
37
+ "scripts": {
38
+ "prepublishOnly": "echo 'Publishing TypeScript source directly'",
39
+ "publish:npm": "npm publish"
40
+ },
41
+ "peerDependencies": {
42
+ "i18next": ">=23.0.0",
43
+ "react": ">=16.8.0",
44
+ "react-i18next": ">=13.0.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "react-native": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "devDependencies": {
52
+ "@babel/core": "^7.28.5",
53
+ "@babel/preset-typescript": "^7.28.5",
54
+ "@babel/types": "^7.28.5",
55
+ "@types/babel__core": "^7.20.0",
56
+ "@types/glob": "^8.0.0",
57
+ "@types/react": "^19.1.0",
58
+ "glob": "^10.0.0",
59
+ "i18next": ">=23.0.0",
60
+ "react-i18next": ">=13.0.0",
61
+ "typescript": "^5.0.0"
62
+ },
63
+ "files": [
64
+ "src",
65
+ "README.md",
66
+ "CLAUDE.md"
67
+ ],
68
+ "keywords": [
69
+ "i18n",
70
+ "internationalization",
71
+ "translate",
72
+ "react",
73
+ "react-native",
74
+ "cross-platform",
75
+ "babel-plugin"
76
+ ]
77
+ }
@@ -0,0 +1,224 @@
1
+ import { KeyRegistry, parseKey, extractKeysFromSource } from '../extractor';
2
+
3
+ describe('parseKey', () => {
4
+ it('parses namespace:key format', () => {
5
+ const result = parseKey('common:buttons.submit');
6
+ expect(result).toEqual({
7
+ namespace: 'common',
8
+ localKey: 'buttons.submit',
9
+ });
10
+ });
11
+
12
+ it('parses namespace.key format (first segment as namespace)', () => {
13
+ const result = parseKey('common.buttons.submit');
14
+ expect(result).toEqual({
15
+ namespace: 'common',
16
+ localKey: 'buttons.submit',
17
+ });
18
+ });
19
+
20
+ it('uses default namespace for single key', () => {
21
+ const result = parseKey('submit', 'translation');
22
+ expect(result).toEqual({
23
+ namespace: 'translation',
24
+ localKey: 'submit',
25
+ });
26
+ });
27
+
28
+ it('handles deeply nested keys', () => {
29
+ const result = parseKey('common.forms.validation.errors.required');
30
+ expect(result).toEqual({
31
+ namespace: 'common',
32
+ localKey: 'forms.validation.errors.required',
33
+ });
34
+ });
35
+
36
+ it('handles colon in local key', () => {
37
+ const result = parseKey('common:time:format');
38
+ expect(result).toEqual({
39
+ namespace: 'common',
40
+ localKey: 'time:format',
41
+ });
42
+ });
43
+ });
44
+
45
+ describe('KeyRegistry', () => {
46
+ let registry: KeyRegistry;
47
+
48
+ beforeEach(() => {
49
+ registry = new KeyRegistry();
50
+ });
51
+
52
+ it('adds and retrieves keys', () => {
53
+ registry.addKey({
54
+ key: 'common.test',
55
+ namespace: 'common',
56
+ localKey: 'test',
57
+ file: 'test.tsx',
58
+ line: 1,
59
+ column: 0,
60
+ isDynamic: false,
61
+ });
62
+
63
+ expect(registry.getKeyCount()).toBe(1);
64
+ expect(registry.hasKey('common.test')).toBe(true);
65
+ });
66
+
67
+ it('tracks multiple usages of the same key', () => {
68
+ const baseKey = {
69
+ key: 'common.test',
70
+ namespace: 'common',
71
+ localKey: 'test',
72
+ isDynamic: false,
73
+ };
74
+
75
+ registry.addKey({ ...baseKey, file: 'file1.tsx', line: 1, column: 0 });
76
+ registry.addKey({ ...baseKey, file: 'file2.tsx', line: 5, column: 0 });
77
+
78
+ const usages = registry.getKeyUsages('common.test');
79
+ expect(usages).toHaveLength(2);
80
+ expect(usages[0].file).toBe('file1.tsx');
81
+ expect(usages[1].file).toBe('file2.tsx');
82
+ });
83
+
84
+ it('separates static and dynamic keys', () => {
85
+ registry.addKey({
86
+ key: 'common.static',
87
+ namespace: 'common',
88
+ localKey: 'static',
89
+ file: 'test.tsx',
90
+ line: 1,
91
+ column: 0,
92
+ isDynamic: false,
93
+ });
94
+
95
+ registry.addKey({
96
+ key: '<dynamic>',
97
+ namespace: 'common',
98
+ localKey: '<dynamic>',
99
+ file: 'test.tsx',
100
+ line: 2,
101
+ column: 0,
102
+ isDynamic: true,
103
+ });
104
+
105
+ expect(registry.getStaticKeys()).toHaveLength(1);
106
+ expect(registry.getDynamicKeys()).toHaveLength(1);
107
+ });
108
+
109
+ it('returns unique keys', () => {
110
+ const baseKey = {
111
+ key: 'common.test',
112
+ namespace: 'common',
113
+ localKey: 'test',
114
+ isDynamic: false,
115
+ };
116
+
117
+ registry.addKey({ ...baseKey, file: 'file1.tsx', line: 1, column: 0 });
118
+ registry.addKey({ ...baseKey, file: 'file2.tsx', line: 5, column: 0 });
119
+
120
+ const uniqueKeys = registry.getUniqueKeys();
121
+ expect(uniqueKeys).toHaveLength(1);
122
+ expect(uniqueKeys[0]).toBe('common.test');
123
+ });
124
+
125
+ it('groups keys by namespace', () => {
126
+ registry.addKey({
127
+ key: 'common.test1',
128
+ namespace: 'common',
129
+ localKey: 'test1',
130
+ file: 'test.tsx',
131
+ line: 1,
132
+ column: 0,
133
+ isDynamic: false,
134
+ });
135
+
136
+ registry.addKey({
137
+ key: 'auth.login',
138
+ namespace: 'auth',
139
+ localKey: 'login',
140
+ file: 'test.tsx',
141
+ line: 2,
142
+ column: 0,
143
+ isDynamic: false,
144
+ });
145
+
146
+ const byNamespace = registry.getKeysByNamespace();
147
+ expect(Object.keys(byNamespace)).toEqual(['common', 'auth']);
148
+ expect(byNamespace.common).toHaveLength(1);
149
+ expect(byNamespace.auth).toHaveLength(1);
150
+ });
151
+
152
+ it('clears all keys', () => {
153
+ registry.addKey({
154
+ key: 'common.test',
155
+ namespace: 'common',
156
+ localKey: 'test',
157
+ file: 'test.tsx',
158
+ line: 1,
159
+ column: 0,
160
+ isDynamic: false,
161
+ });
162
+
163
+ registry.clear();
164
+ expect(registry.getKeyCount()).toBe(0);
165
+ });
166
+ });
167
+
168
+ describe('extractKeysFromSource', () => {
169
+ it('extracts t() calls with single quotes', () => {
170
+ const code = `const label = t('common.buttons.submit');`;
171
+ const keys = extractKeysFromSource(code, 'test.tsx');
172
+
173
+ expect(keys).toHaveLength(1);
174
+ expect(keys[0].key).toBe('common.buttons.submit');
175
+ expect(keys[0].namespace).toBe('common');
176
+ expect(keys[0].localKey).toBe('buttons.submit');
177
+ });
178
+
179
+ it('extracts t() calls with double quotes', () => {
180
+ const code = `const label = t("common.buttons.submit");`;
181
+ const keys = extractKeysFromSource(code, 'test.tsx');
182
+
183
+ expect(keys).toHaveLength(1);
184
+ expect(keys[0].key).toBe('common.buttons.submit');
185
+ });
186
+
187
+ it('extracts multiple t() calls', () => {
188
+ const code = `
189
+ const submit = t('common.buttons.submit');
190
+ const cancel = t('common.buttons.cancel');
191
+ `;
192
+ const keys = extractKeysFromSource(code, 'test.tsx');
193
+
194
+ expect(keys).toHaveLength(2);
195
+ expect(keys[0].key).toBe('common.buttons.submit');
196
+ expect(keys[1].key).toBe('common.buttons.cancel');
197
+ });
198
+
199
+ it('extracts Trans component i18nKey', () => {
200
+ const code = `<Trans i18nKey="common.richText" />`;
201
+ const keys = extractKeysFromSource(code, 'test.tsx');
202
+
203
+ expect(keys).toHaveLength(1);
204
+ expect(keys[0].key).toBe('common.richText');
205
+ });
206
+
207
+ it('extracts both t() and Trans keys', () => {
208
+ const code = `
209
+ const label = t('common.title');
210
+ return <Trans i18nKey="common.description" />;
211
+ `;
212
+ const keys = extractKeysFromSource(code, 'test.tsx');
213
+
214
+ expect(keys).toHaveLength(2);
215
+ });
216
+
217
+ it('includes line numbers', () => {
218
+ const code = `const label = t('common.test');`;
219
+ const keys = extractKeysFromSource(code, 'test.tsx');
220
+
221
+ expect(keys[0].line).toBe(1);
222
+ expect(keys[0].file).toBe('test.tsx');
223
+ });
224
+ });
@@ -0,0 +1,289 @@
1
+ import * as babel from '@babel/core';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+
5
+ // Import the plugin
6
+ const plugin = require('../plugin');
7
+
8
+ // Reset the global registry before each test
9
+ beforeEach(() => {
10
+ plugin.globalRegistry.clear();
11
+ });
12
+
13
+ function transform(code: string, options = {}, filename = 'test.tsx') {
14
+ const result = babel.transformSync(code, {
15
+ filename,
16
+ presets: ['@babel/preset-typescript'],
17
+ plugins: [[plugin, options]],
18
+ babelrc: false,
19
+ configFile: false,
20
+ });
21
+ return result;
22
+ }
23
+
24
+ function transformWithReact(code: string, options = {}, filename = 'test.tsx') {
25
+ const result = babel.transformSync(code, {
26
+ filename,
27
+ presets: [
28
+ ['@babel/preset-react', { runtime: 'automatic' }],
29
+ '@babel/preset-typescript',
30
+ ],
31
+ plugins: [[plugin, options]],
32
+ babelrc: false,
33
+ configFile: false,
34
+ });
35
+ return result;
36
+ }
37
+
38
+ describe('Babel Plugin - Key Extraction', () => {
39
+ describe('t() function calls', () => {
40
+ it('extracts simple t() calls', () => {
41
+ const code = `const label = t('common.hello');`;
42
+
43
+ transform(code);
44
+ const keys = plugin.globalRegistry.getStaticKeys();
45
+
46
+ expect(keys).toHaveLength(1);
47
+ expect(keys[0].key).toBe('common.hello');
48
+ expect(keys[0].namespace).toBe('common');
49
+ expect(keys[0].localKey).toBe('hello');
50
+ });
51
+
52
+ it('extracts t() with namespace:key format', () => {
53
+ const code = `const label = t('auth:login.title');`;
54
+
55
+ transform(code, { defaultNamespace: 'common' });
56
+ const keys = plugin.globalRegistry.getStaticKeys();
57
+
58
+ expect(keys).toHaveLength(1);
59
+ expect(keys[0].key).toBe('auth:login.title');
60
+ expect(keys[0].namespace).toBe('auth');
61
+ expect(keys[0].localKey).toBe('login.title');
62
+ });
63
+
64
+ it('extracts t() with interpolation options', () => {
65
+ const code = `const greeting = t('common.greeting', { name: 'World' });`;
66
+
67
+ transform(code);
68
+ const keys = plugin.globalRegistry.getStaticKeys();
69
+
70
+ expect(keys).toHaveLength(1);
71
+ expect(keys[0].key).toBe('common.greeting');
72
+ });
73
+
74
+ it('extracts defaultValue from options', () => {
75
+ const code = `const label = t('common.new', { defaultValue: 'New Feature' });`;
76
+
77
+ transform(code);
78
+ const keys = plugin.globalRegistry.getStaticKeys();
79
+
80
+ expect(keys).toHaveLength(1);
81
+ expect(keys[0].defaultValue).toBe('New Feature');
82
+ });
83
+
84
+ it('extracts i18n.t() calls', () => {
85
+ const code = `const label = i18n.t('common.hello');`;
86
+
87
+ transform(code);
88
+ const keys = plugin.globalRegistry.getStaticKeys();
89
+
90
+ expect(keys).toHaveLength(1);
91
+ expect(keys[0].key).toBe('common.hello');
92
+ });
93
+
94
+ it('marks dynamic keys as isDynamic', () => {
95
+ const code = `
96
+ const key = 'common.' + someVar;
97
+ const label = t(key);
98
+ `;
99
+
100
+ transform(code);
101
+ const dynamicKeys = plugin.globalRegistry.getDynamicKeys();
102
+
103
+ expect(dynamicKeys).toHaveLength(1);
104
+ expect(dynamicKeys[0].isDynamic).toBe(true);
105
+ expect(dynamicKeys[0].key).toBe('<dynamic>');
106
+ });
107
+
108
+ it('marks template literals with expressions as dynamic', () => {
109
+ const code = 'const label = t(`common.${type}.title`);';
110
+
111
+ transform(code);
112
+ const dynamicKeys = plugin.globalRegistry.getDynamicKeys();
113
+
114
+ expect(dynamicKeys).toHaveLength(1);
115
+ expect(dynamicKeys[0].isDynamic).toBe(true);
116
+ });
117
+
118
+ it('treats template literals without expressions as static', () => {
119
+ const code = 'const label = t(`common.hello`);';
120
+
121
+ transform(code);
122
+ const staticKeys = plugin.globalRegistry.getStaticKeys();
123
+
124
+ expect(staticKeys).toHaveLength(1);
125
+ expect(staticKeys[0].key).toBe('common.hello');
126
+ expect(staticKeys[0].isDynamic).toBe(false);
127
+ });
128
+ });
129
+
130
+ describe('<Trans> component', () => {
131
+ it('extracts i18nKey from Trans component', () => {
132
+ const code = `<Trans i18nKey="common.richText" />`;
133
+
134
+ transformWithReact(code);
135
+ const keys = plugin.globalRegistry.getStaticKeys();
136
+
137
+ expect(keys).toHaveLength(1);
138
+ expect(keys[0].key).toBe('common.richText');
139
+ });
140
+
141
+ it('extracts i18nKey with namespace prefix', () => {
142
+ const code = `<Trans i18nKey="auth.terms" />`;
143
+
144
+ transformWithReact(code);
145
+ const keys = plugin.globalRegistry.getStaticKeys();
146
+
147
+ expect(keys).toHaveLength(1);
148
+ expect(keys[0].namespace).toBe('auth');
149
+ expect(keys[0].localKey).toBe('terms');
150
+ });
151
+
152
+ it('extracts i18nKey from JSX expression', () => {
153
+ const code = `<Trans i18nKey={"common.richText"} />`;
154
+
155
+ transformWithReact(code);
156
+ const keys = plugin.globalRegistry.getStaticKeys();
157
+
158
+ expect(keys).toHaveLength(1);
159
+ expect(keys[0].key).toBe('common.richText');
160
+ });
161
+
162
+ it('marks dynamic i18nKey as isDynamic', () => {
163
+ const code = `<Trans i18nKey={dynamicKey} />`;
164
+
165
+ transformWithReact(code);
166
+ const dynamicKeys = plugin.globalRegistry.getDynamicKeys();
167
+
168
+ expect(dynamicKeys).toHaveLength(1);
169
+ expect(dynamicKeys[0].isDynamic).toBe(true);
170
+ });
171
+ });
172
+
173
+ describe('multiple files', () => {
174
+ it('aggregates keys from multiple transforms', () => {
175
+ const code1 = `const a = t('common.key1');`;
176
+ const code2 = `const b = t('common.key2');`;
177
+
178
+ transform(code1, {}, 'file1.tsx');
179
+ transform(code2, {}, 'file2.tsx');
180
+
181
+ const keys = plugin.globalRegistry.getUniqueKeys();
182
+ expect(keys).toHaveLength(2);
183
+ expect(keys).toContain('common.key1');
184
+ expect(keys).toContain('common.key2');
185
+ });
186
+
187
+ it('tracks same key used in multiple files', () => {
188
+ transform(`const a = t('common.shared');`, {}, 'file1.tsx');
189
+ transform(`const b = t('common.shared');`, {}, 'file2.tsx');
190
+
191
+ const usages = plugin.globalRegistry.getKeyUsages('common.shared');
192
+ expect(usages).toHaveLength(2);
193
+ expect(usages[0].file).toContain('file1.tsx');
194
+ expect(usages[1].file).toContain('file2.tsx');
195
+ });
196
+ });
197
+
198
+ describe('source locations', () => {
199
+ it('captures line and column numbers', () => {
200
+ const code = `const label = t('common.test');`;
201
+
202
+ transform(code);
203
+ const keys = plugin.globalRegistry.getStaticKeys();
204
+
205
+ expect(keys).toHaveLength(1);
206
+ expect(keys[0].line).toBeGreaterThan(0);
207
+ expect(keys[0].column).toBeGreaterThanOrEqual(0);
208
+ });
209
+ });
210
+
211
+ describe('default namespace', () => {
212
+ it('uses configured default namespace', () => {
213
+ const code = `const label = t('buttonLabel');`;
214
+
215
+ transform(code, { defaultNamespace: 'ui' });
216
+ const keys = plugin.globalRegistry.getStaticKeys();
217
+
218
+ expect(keys).toHaveLength(1);
219
+ expect(keys[0].namespace).toBe('ui');
220
+ expect(keys[0].localKey).toBe('buttonLabel');
221
+ });
222
+
223
+ it('uses translation as default when not configured', () => {
224
+ const code = `const label = t('buttonLabel');`;
225
+
226
+ transform(code, {});
227
+ const keys = plugin.globalRegistry.getStaticKeys();
228
+
229
+ expect(keys).toHaveLength(1);
230
+ expect(keys[0].namespace).toBe('translation');
231
+ });
232
+ });
233
+ });
234
+
235
+ describe('Babel Plugin - Report Generation (via generateReport)', () => {
236
+ // Note: Full report generation through the plugin is tested in reporter.test.ts
237
+ // These tests verify the plugin correctly passes keys to the report generator
238
+
239
+ it('passes extracted keys for report generation', () => {
240
+ plugin.globalRegistry.clear();
241
+
242
+ const code1 = `const a = t('common.hello');`;
243
+ const code2 = `const b = t('common.world');`;
244
+
245
+ transform(code1, {}, 'file1.tsx');
246
+ transform(code2, {}, 'file2.tsx');
247
+
248
+ // Verify keys are in registry
249
+ const keys = plugin.globalRegistry.getStaticKeys();
250
+ expect(keys).toHaveLength(2);
251
+
252
+ // Generate report using the plugin's exported function
253
+ const report = plugin.generateReport(keys, {
254
+ translationFiles: [],
255
+ languages: ['en', 'es'],
256
+ defaultNamespace: 'common',
257
+ });
258
+
259
+ expect(report.totalKeys).toBe(2);
260
+ expect(report.languages).toContain('en');
261
+ expect(report.languages).toContain('es');
262
+ });
263
+
264
+ it('includes dynamic keys in report', () => {
265
+ plugin.globalRegistry.clear();
266
+
267
+ const code = `
268
+ const key = dynamicVar;
269
+ const label = t(key);
270
+ `;
271
+
272
+ transform(code);
273
+
274
+ const report = plugin.generateReport(plugin.globalRegistry.getAllKeys(), {
275
+ translationFiles: [],
276
+ languages: ['en'],
277
+ defaultNamespace: 'common',
278
+ });
279
+
280
+ expect(report.dynamicKeys).toHaveLength(1);
281
+ expect(report.dynamicKeys[0].isDynamic).toBe(true);
282
+ });
283
+
284
+ it('exports generateReport and writeReport functions', () => {
285
+ expect(typeof plugin.generateReport).toBe('function');
286
+ expect(typeof plugin.writeReport).toBe('function');
287
+ expect(typeof plugin.parseKey).toBe('function');
288
+ });
289
+ });