@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/README.md +773 -0
- package/package.json +77 -0
- package/src/babel/__tests__/extractor.test.ts +224 -0
- package/src/babel/__tests__/plugin.test.ts +289 -0
- package/src/babel/__tests__/reporter.test.ts +314 -0
- package/src/babel/extractor.ts +179 -0
- package/src/babel/index.ts +21 -0
- package/src/babel/plugin.js +545 -0
- package/src/babel/reporter.ts +287 -0
- package/src/babel/types.ts +214 -0
- package/src/components/Trans.tsx +72 -0
- package/src/components/index.ts +2 -0
- package/src/components/types.ts +64 -0
- package/src/config/index.ts +2 -0
- package/src/config/types.ts +10 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/types.ts +113 -0
- package/src/hooks/useLanguage.ts +73 -0
- package/src/hooks/useTranslation.ts +52 -0
- package/src/index.native.ts +2 -0
- package/src/index.ts +22 -0
- package/src/index.web.ts +24 -0
- package/src/provider/TranslateProvider.tsx +132 -0
- package/src/provider/index.ts +6 -0
- package/src/provider/types.ts +119 -0
- package/src/utils/__tests__/namespace.test.ts +211 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/namespace.ts +97 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { generateReport, writeReport, loadTranslations } from '../reporter';
|
|
4
|
+
import type { ExtractedKey, TranslatePluginOptions } from '../types';
|
|
5
|
+
|
|
6
|
+
describe('loadTranslations', () => {
|
|
7
|
+
const tempDir = path.join(__dirname, '.temp-loader-test');
|
|
8
|
+
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
fs.mkdirSync(path.join(tempDir, 'en'), { recursive: true });
|
|
11
|
+
fs.mkdirSync(path.join(tempDir, 'es'), { recursive: true });
|
|
12
|
+
|
|
13
|
+
fs.writeFileSync(
|
|
14
|
+
path.join(tempDir, 'en', 'common.json'),
|
|
15
|
+
JSON.stringify({ greeting: 'Hello' })
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
fs.writeFileSync(
|
|
19
|
+
path.join(tempDir, 'es', 'common.json'),
|
|
20
|
+
JSON.stringify({ greeting: 'Hola' })
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterAll(() => {
|
|
25
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('loads translations from directory structure', () => {
|
|
29
|
+
const translations = loadTranslations(
|
|
30
|
+
[path.join(tempDir, '**/*.json')],
|
|
31
|
+
false
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(translations).toHaveProperty('en');
|
|
35
|
+
expect(translations).toHaveProperty('es');
|
|
36
|
+
expect(translations.en.common).toEqual({ greeting: 'Hello' });
|
|
37
|
+
expect(translations.es.common).toEqual({ greeting: 'Hola' });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('handles non-existent paths gracefully', () => {
|
|
41
|
+
const translations = loadTranslations(['/nonexistent/path/*.json'], false);
|
|
42
|
+
expect(Object.keys(translations)).toHaveLength(0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('generateReport', () => {
|
|
47
|
+
const tempDir = path.join(__dirname, '.temp-report-test');
|
|
48
|
+
|
|
49
|
+
beforeAll(() => {
|
|
50
|
+
fs.mkdirSync(path.join(tempDir, 'en'), { recursive: true });
|
|
51
|
+
fs.mkdirSync(path.join(tempDir, 'es'), { recursive: true });
|
|
52
|
+
fs.mkdirSync(path.join(tempDir, 'fr'), { recursive: true });
|
|
53
|
+
|
|
54
|
+
fs.writeFileSync(
|
|
55
|
+
path.join(tempDir, 'en', 'common.json'),
|
|
56
|
+
JSON.stringify({
|
|
57
|
+
buttons: {
|
|
58
|
+
submit: 'Submit',
|
|
59
|
+
cancel: 'Cancel',
|
|
60
|
+
},
|
|
61
|
+
unused: 'Not used anywhere',
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
fs.writeFileSync(
|
|
66
|
+
path.join(tempDir, 'es', 'common.json'),
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
buttons: {
|
|
69
|
+
submit: 'Enviar',
|
|
70
|
+
// cancel is missing
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
fs.writeFileSync(
|
|
76
|
+
path.join(tempDir, 'fr', 'common.json'),
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
buttons: {
|
|
79
|
+
submit: 'Soumettre',
|
|
80
|
+
cancel: 'Annuler',
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
afterAll(() => {
|
|
87
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const createKeys = (): ExtractedKey[] => [
|
|
91
|
+
{
|
|
92
|
+
key: 'common.buttons.submit',
|
|
93
|
+
namespace: 'common',
|
|
94
|
+
localKey: 'buttons.submit',
|
|
95
|
+
file: 'Form.tsx',
|
|
96
|
+
line: 10,
|
|
97
|
+
column: 5,
|
|
98
|
+
isDynamic: false,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
key: 'common.buttons.cancel',
|
|
102
|
+
namespace: 'common',
|
|
103
|
+
localKey: 'buttons.cancel',
|
|
104
|
+
file: 'Form.tsx',
|
|
105
|
+
line: 11,
|
|
106
|
+
column: 5,
|
|
107
|
+
isDynamic: false,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
key: '<dynamic>',
|
|
111
|
+
namespace: 'common',
|
|
112
|
+
localKey: '<dynamic>',
|
|
113
|
+
file: 'Dynamic.tsx',
|
|
114
|
+
line: 5,
|
|
115
|
+
column: 0,
|
|
116
|
+
isDynamic: true,
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const createOptions = (): TranslatePluginOptions => ({
|
|
121
|
+
translationFiles: [path.join(tempDir, '**/*.json')],
|
|
122
|
+
defaultNamespace: 'common',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('generates report with correct structure', () => {
|
|
126
|
+
const report = generateReport(createKeys(), createOptions());
|
|
127
|
+
|
|
128
|
+
expect(report).toHaveProperty('timestamp');
|
|
129
|
+
expect(report).toHaveProperty('totalKeys');
|
|
130
|
+
expect(report).toHaveProperty('extractedKeys');
|
|
131
|
+
expect(report).toHaveProperty('dynamicKeys');
|
|
132
|
+
expect(report).toHaveProperty('languages');
|
|
133
|
+
expect(report).toHaveProperty('missing');
|
|
134
|
+
expect(report).toHaveProperty('unused');
|
|
135
|
+
expect(report).toHaveProperty('summary');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('counts only static keys in totalKeys', () => {
|
|
139
|
+
const report = generateReport(createKeys(), createOptions());
|
|
140
|
+
|
|
141
|
+
expect(report.totalKeys).toBe(2); // Excludes dynamic key
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('separates static and dynamic keys', () => {
|
|
145
|
+
const report = generateReport(createKeys(), createOptions());
|
|
146
|
+
|
|
147
|
+
expect(report.extractedKeys).toHaveLength(2);
|
|
148
|
+
expect(report.dynamicKeys).toHaveLength(1);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('detects missing translations', () => {
|
|
152
|
+
const report = generateReport(createKeys(), createOptions());
|
|
153
|
+
|
|
154
|
+
// Spanish is missing 'cancel'
|
|
155
|
+
expect(report.missing.es).toHaveLength(1);
|
|
156
|
+
expect(report.missing.es[0].key).toBe('common.buttons.cancel');
|
|
157
|
+
|
|
158
|
+
// English and French have all keys
|
|
159
|
+
expect(report.missing.en).toHaveLength(0);
|
|
160
|
+
expect(report.missing.fr).toHaveLength(0);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('includes usage locations for missing keys', () => {
|
|
164
|
+
const report = generateReport(createKeys(), createOptions());
|
|
165
|
+
|
|
166
|
+
const missingCancel = report.missing.es[0];
|
|
167
|
+
expect(missingCancel.usedIn).toHaveLength(1);
|
|
168
|
+
expect(missingCancel.usedIn[0].file).toBe('Form.tsx');
|
|
169
|
+
expect(missingCancel.usedIn[0].line).toBe(11);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('detects unused translations', () => {
|
|
173
|
+
const report = generateReport(createKeys(), createOptions());
|
|
174
|
+
|
|
175
|
+
// 'unused' key exists in en/common.json but not in code
|
|
176
|
+
expect(report.unused.en).toContain('common.unused');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('calculates coverage percentages correctly', () => {
|
|
180
|
+
const report = generateReport(createKeys(), createOptions());
|
|
181
|
+
|
|
182
|
+
// English: 2/2 = 100%
|
|
183
|
+
expect(report.summary.coveragePercent.en).toBe(100);
|
|
184
|
+
|
|
185
|
+
// Spanish: 1/2 = 50%
|
|
186
|
+
expect(report.summary.coveragePercent.es).toBe(50);
|
|
187
|
+
|
|
188
|
+
// French: 2/2 = 100%
|
|
189
|
+
expect(report.summary.coveragePercent.fr).toBe(100);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('calculates summary totals', () => {
|
|
193
|
+
const report = generateReport(createKeys(), createOptions());
|
|
194
|
+
|
|
195
|
+
expect(report.summary.totalMissing).toBe(1); // Only Spanish missing 1
|
|
196
|
+
expect(report.summary.totalUnused).toBeGreaterThan(0);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('includes timestamp in ISO format', () => {
|
|
200
|
+
const report = generateReport(createKeys(), createOptions());
|
|
201
|
+
|
|
202
|
+
expect(() => new Date(report.timestamp)).not.toThrow();
|
|
203
|
+
expect(report.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('handles empty keys array', () => {
|
|
207
|
+
const report = generateReport([], createOptions());
|
|
208
|
+
|
|
209
|
+
expect(report.totalKeys).toBe(0);
|
|
210
|
+
expect(report.extractedKeys).toHaveLength(0);
|
|
211
|
+
expect(report.summary.coveragePercent.en).toBe(100); // No keys = 100% coverage
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('handles keys with defaultValue', () => {
|
|
215
|
+
const keysWithDefault: ExtractedKey[] = [
|
|
216
|
+
{
|
|
217
|
+
key: 'common.new.feature',
|
|
218
|
+
namespace: 'common',
|
|
219
|
+
localKey: 'new.feature',
|
|
220
|
+
file: 'New.tsx',
|
|
221
|
+
line: 1,
|
|
222
|
+
column: 0,
|
|
223
|
+
isDynamic: false,
|
|
224
|
+
defaultValue: 'New Feature',
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
const report = generateReport(keysWithDefault, createOptions());
|
|
229
|
+
|
|
230
|
+
const missingKey = report.missing.en.find(
|
|
231
|
+
(m) => m.key === 'common.new.feature'
|
|
232
|
+
);
|
|
233
|
+
expect(missingKey?.defaultValue).toBe('New Feature');
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('writeReport', () => {
|
|
238
|
+
const tempDir = path.join(__dirname, '.temp-write-test');
|
|
239
|
+
|
|
240
|
+
afterEach(() => {
|
|
241
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('writes report to file', () => {
|
|
245
|
+
const report = {
|
|
246
|
+
timestamp: new Date().toISOString(),
|
|
247
|
+
totalKeys: 0,
|
|
248
|
+
extractedKeys: [],
|
|
249
|
+
dynamicKeys: [],
|
|
250
|
+
languages: ['en'],
|
|
251
|
+
missing: { en: [] },
|
|
252
|
+
unused: { en: [] },
|
|
253
|
+
summary: {
|
|
254
|
+
totalMissing: 0,
|
|
255
|
+
totalUnused: 0,
|
|
256
|
+
coveragePercent: { en: 100 },
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const outputPath = path.join(tempDir, 'report.json');
|
|
261
|
+
writeReport(report, outputPath);
|
|
262
|
+
|
|
263
|
+
expect(fs.existsSync(outputPath)).toBe(true);
|
|
264
|
+
|
|
265
|
+
const written = JSON.parse(fs.readFileSync(outputPath, 'utf-8'));
|
|
266
|
+
expect(written).toEqual(report);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('creates nested directories if needed', () => {
|
|
270
|
+
const report = {
|
|
271
|
+
timestamp: new Date().toISOString(),
|
|
272
|
+
totalKeys: 0,
|
|
273
|
+
extractedKeys: [],
|
|
274
|
+
dynamicKeys: [],
|
|
275
|
+
languages: [],
|
|
276
|
+
missing: {},
|
|
277
|
+
unused: {},
|
|
278
|
+
summary: {
|
|
279
|
+
totalMissing: 0,
|
|
280
|
+
totalUnused: 0,
|
|
281
|
+
coveragePercent: {},
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const outputPath = path.join(tempDir, 'deep', 'nested', 'report.json');
|
|
286
|
+
writeReport(report, outputPath);
|
|
287
|
+
|
|
288
|
+
expect(fs.existsSync(outputPath)).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('formats JSON with indentation', () => {
|
|
292
|
+
const report = {
|
|
293
|
+
timestamp: new Date().toISOString(),
|
|
294
|
+
totalKeys: 1,
|
|
295
|
+
extractedKeys: [],
|
|
296
|
+
dynamicKeys: [],
|
|
297
|
+
languages: ['en'],
|
|
298
|
+
missing: { en: [] },
|
|
299
|
+
unused: { en: [] },
|
|
300
|
+
summary: {
|
|
301
|
+
totalMissing: 0,
|
|
302
|
+
totalUnused: 0,
|
|
303
|
+
coveragePercent: { en: 100 },
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const outputPath = path.join(tempDir, 'formatted.json');
|
|
308
|
+
writeReport(report, outputPath);
|
|
309
|
+
|
|
310
|
+
const content = fs.readFileSync(outputPath, 'utf-8');
|
|
311
|
+
expect(content).toContain('\n'); // Has newlines
|
|
312
|
+
expect(content).toContain(' '); // Has indentation
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { ExtractedKey } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse a full translation key into namespace and local key parts
|
|
5
|
+
*/
|
|
6
|
+
export function parseKey(
|
|
7
|
+
fullKey: string,
|
|
8
|
+
defaultNamespace: string = 'translation'
|
|
9
|
+
): { namespace: string; localKey: string } {
|
|
10
|
+
// Handle namespace:key format (i18next standard)
|
|
11
|
+
if (fullKey.includes(':')) {
|
|
12
|
+
const [namespace, ...rest] = fullKey.split(':');
|
|
13
|
+
return { namespace, localKey: rest.join(':') };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Handle namespace.key format (first segment is namespace if multiple segments)
|
|
17
|
+
const segments = fullKey.split('.');
|
|
18
|
+
if (segments.length > 1) {
|
|
19
|
+
return { namespace: segments[0], localKey: segments.slice(1).join('.') };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Single key without namespace
|
|
23
|
+
return { namespace: defaultNamespace, localKey: fullKey };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Registry for collecting translation keys across files during build
|
|
28
|
+
*/
|
|
29
|
+
export class KeyRegistry {
|
|
30
|
+
private keys: Map<string, ExtractedKey[]> = new Map();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Add a key to the registry
|
|
34
|
+
*/
|
|
35
|
+
addKey(key: ExtractedKey): void {
|
|
36
|
+
const existing = this.keys.get(key.key) || [];
|
|
37
|
+
existing.push(key);
|
|
38
|
+
this.keys.set(key.key, existing);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get all keys (including duplicates from different files)
|
|
43
|
+
*/
|
|
44
|
+
getAllKeys(): ExtractedKey[] {
|
|
45
|
+
return Array.from(this.keys.values()).flat();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get static keys only (excludes dynamic keys)
|
|
50
|
+
*/
|
|
51
|
+
getStaticKeys(): ExtractedKey[] {
|
|
52
|
+
return this.getAllKeys().filter((k) => !k.isDynamic);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get dynamic keys only
|
|
57
|
+
*/
|
|
58
|
+
getDynamicKeys(): ExtractedKey[] {
|
|
59
|
+
return this.getAllKeys().filter((k) => k.isDynamic);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get unique static key strings
|
|
64
|
+
*/
|
|
65
|
+
getUniqueKeys(): string[] {
|
|
66
|
+
return Array.from(new Set(this.getStaticKeys().map((k) => k.key)));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get keys grouped by namespace
|
|
71
|
+
*/
|
|
72
|
+
getKeysByNamespace(): Record<string, ExtractedKey[]> {
|
|
73
|
+
const result: Record<string, ExtractedKey[]> = {};
|
|
74
|
+
for (const key of this.getAllKeys()) {
|
|
75
|
+
if (!result[key.namespace]) {
|
|
76
|
+
result[key.namespace] = [];
|
|
77
|
+
}
|
|
78
|
+
result[key.namespace].push(key);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if a key exists
|
|
85
|
+
*/
|
|
86
|
+
hasKey(key: string): boolean {
|
|
87
|
+
return this.keys.has(key);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get usage locations for a specific key
|
|
92
|
+
*/
|
|
93
|
+
getKeyUsages(key: string): ExtractedKey[] {
|
|
94
|
+
return this.keys.get(key) || [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the count of unique keys
|
|
99
|
+
*/
|
|
100
|
+
getKeyCount(): number {
|
|
101
|
+
return this.keys.size;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Clear all keys
|
|
106
|
+
*/
|
|
107
|
+
clear(): void {
|
|
108
|
+
this.keys.clear();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Export keys to a plain object for serialization
|
|
113
|
+
*/
|
|
114
|
+
toJSON(): { keys: ExtractedKey[]; uniqueCount: number } {
|
|
115
|
+
return {
|
|
116
|
+
keys: this.getAllKeys(),
|
|
117
|
+
uniqueCount: this.getKeyCount(),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get line number from character index in source code
|
|
124
|
+
*/
|
|
125
|
+
export function getLineNumber(code: string, index: number): number {
|
|
126
|
+
return code.slice(0, index).split('\n').length;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get column number from character index in source code
|
|
131
|
+
*/
|
|
132
|
+
export function getColumnNumber(code: string, index: number): number {
|
|
133
|
+
const lastNewline = code.lastIndexOf('\n', index);
|
|
134
|
+
return index - lastNewline;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Simple regex-based key extraction (fallback for non-Babel usage)
|
|
139
|
+
*/
|
|
140
|
+
export function extractKeysFromSource(
|
|
141
|
+
code: string,
|
|
142
|
+
filename: string,
|
|
143
|
+
defaultNamespace: string = 'translation'
|
|
144
|
+
): ExtractedKey[] {
|
|
145
|
+
const keys: ExtractedKey[] = [];
|
|
146
|
+
|
|
147
|
+
// Match t('key') or t("key")
|
|
148
|
+
const tCallRegex = /\bt\s*\(\s*['"]([^'"]+)['"]/g;
|
|
149
|
+
let match;
|
|
150
|
+
while ((match = tCallRegex.exec(code)) !== null) {
|
|
151
|
+
const { namespace, localKey } = parseKey(match[1], defaultNamespace);
|
|
152
|
+
keys.push({
|
|
153
|
+
key: match[1],
|
|
154
|
+
namespace,
|
|
155
|
+
localKey,
|
|
156
|
+
file: filename,
|
|
157
|
+
line: getLineNumber(code, match.index),
|
|
158
|
+
column: getColumnNumber(code, match.index),
|
|
159
|
+
isDynamic: false,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Match i18nKey="key" (Trans component)
|
|
164
|
+
const transRegex = /i18nKey\s*=\s*['"]([^'"]+)['"]/g;
|
|
165
|
+
while ((match = transRegex.exec(code)) !== null) {
|
|
166
|
+
const { namespace, localKey } = parseKey(match[1], defaultNamespace);
|
|
167
|
+
keys.push({
|
|
168
|
+
key: match[1],
|
|
169
|
+
namespace,
|
|
170
|
+
localKey,
|
|
171
|
+
file: filename,
|
|
172
|
+
line: getLineNumber(code, match.index),
|
|
173
|
+
column: getColumnNumber(code, match.index),
|
|
174
|
+
isDynamic: false,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return keys;
|
|
179
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Export types
|
|
2
|
+
export type {
|
|
3
|
+
TranslatePluginOptions,
|
|
4
|
+
ExtractedKey,
|
|
5
|
+
MissingTranslation,
|
|
6
|
+
CoverageStats,
|
|
7
|
+
TranslationsReport,
|
|
8
|
+
LoadedTranslations,
|
|
9
|
+
} from './types';
|
|
10
|
+
|
|
11
|
+
// Export utilities
|
|
12
|
+
export { KeyRegistry, parseKey, extractKeysFromSource } from './extractor';
|
|
13
|
+
export {
|
|
14
|
+
generateReport,
|
|
15
|
+
writeReport,
|
|
16
|
+
loadTranslations,
|
|
17
|
+
printReportSummary,
|
|
18
|
+
} from './reporter';
|
|
19
|
+
|
|
20
|
+
// Note: The actual Babel plugin is in plugin.js (CommonJS)
|
|
21
|
+
// Import it via: import plugin from '@idealyst/translate/plugin'
|