vite-plugin-drupal-t 1.0.2 → 1.0.4

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Plugin } from 'rollup';
1
+ import type { Plugin } from 'vite';
2
2
  export interface ExtractDrupalTOptions {
3
3
  include?: string | string[];
4
4
  exclude?: string | string[];
package/dist/index.js CHANGED
@@ -4,6 +4,20 @@ export default function extractDrupalT(options = {}) {
4
4
  const translations = new Set();
5
5
  return {
6
6
  name: 'extract-drupal-t',
7
+ config() {
8
+ return {
9
+ build: {
10
+ rollupOptions: {
11
+ external: ['Drupal'],
12
+ output: {
13
+ globals: {
14
+ Drupal: 'Drupal',
15
+ },
16
+ },
17
+ },
18
+ },
19
+ };
20
+ },
7
21
  transform(code, id) {
8
22
  if (!filter(id))
9
23
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-drupal-t",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "A Vite plugin that automatically extracts Drupal.t() and Drupal.formatPlural() translation calls for seamless internationalization",
5
5
  "author": "Märt Rang <rang501@gmail.com>",
6
6
  "license": "MIT",
@@ -16,6 +16,7 @@
16
16
  "types": "dist/index.d.ts",
17
17
  "files": [
18
18
  "dist",
19
+ "src",
19
20
  "README.md",
20
21
  "LICENSE"
21
22
  ],
@@ -39,7 +40,8 @@
39
40
  "formatPlural"
40
41
  ],
41
42
  "dependencies": {
42
- "@rollup/pluginutils": "^4.2.1"
43
+ "@rollup/pluginutils": "^4.2.1",
44
+ "ts-for-drupal-core": "^0.0.7"
43
45
  },
44
46
  "devDependencies": {
45
47
  "@types/estree": "^1.0.7",
@@ -0,0 +1,444 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import extractDrupalT from './index';
3
+ import type { Plugin } from 'rollup';
4
+
5
+ describe('extractDrupalT', () => {
6
+ let plugin: Plugin;
7
+
8
+ beforeEach(() => {
9
+ plugin = extractDrupalT() as Plugin;
10
+ });
11
+
12
+ describe('Drupal.t() extraction', () => {
13
+ it('should extract simple Drupal.t() calls', () => {
14
+ const code = `const message = Drupal.t('Hello World');`;
15
+ const id = 'test.js';
16
+
17
+ callTransform(plugin, code, id);
18
+ const output = generateOutput(plugin);
19
+
20
+ expect(output).toContain(`// Drupal.t('Hello World')`);
21
+ });
22
+
23
+ it('should extract Drupal.t() with double quotes', () => {
24
+ const code = `const message = Drupal.t("Hello World");`;
25
+ const id = 'test.js';
26
+
27
+ callTransform(plugin, code, id);
28
+ const output = generateOutput(plugin);
29
+
30
+ expect(output).toContain(`// Drupal.t("Hello World")`);
31
+ });
32
+
33
+ it('should extract Drupal.t() with backticks', () => {
34
+ const code = 'const message = Drupal.t(`Hello World`);';
35
+ const id = 'test.js';
36
+
37
+ callTransform(plugin, code, id);
38
+ const output = generateOutput(plugin);
39
+
40
+ expect(output).toContain('// Drupal.t(`Hello World`)');
41
+ });
42
+
43
+ it('should extract (Drupal).t() calls', () => {
44
+ const code = `const message = (Drupal).t('Hello World');`;
45
+ const id = 'test.js';
46
+
47
+ callTransform(plugin, code, id);
48
+ const output = generateOutput(plugin);
49
+
50
+ expect(output).toContain(`// Drupal.t('Hello World')`);
51
+ });
52
+
53
+ it('should extract Drupal.t() with context parameter', () => {
54
+ const code = `const message = Drupal.t('Hello World', {}, {context: 'greeting'});`;
55
+ const id = 'test.js';
56
+
57
+ callTransform(plugin, code, id);
58
+ const output = generateOutput(plugin);
59
+
60
+ expect(output).toContain(`// Drupal.t('Hello World', {}, {context: 'greeting'})`);
61
+ });
62
+
63
+ it('should extract Drupal.t() with placeholder variables', () => {
64
+ const code = `const message = Drupal.t('Hello @name', {'@name': userName});`;
65
+ const id = 'test.js';
66
+
67
+ callTransform(plugin, code, id);
68
+ const output = generateOutput(plugin);
69
+
70
+ expect(output).toContain(`// Drupal.t('Hello @name', {'@name': userName})`);
71
+ });
72
+
73
+ it('should extract multiple Drupal.t() calls', () => {
74
+ const code = `
75
+ const msg1 = Drupal.t('First message');
76
+ const msg2 = Drupal.t('Second message');
77
+ const msg3 = Drupal.t('Third message');
78
+ `;
79
+ const id = 'test.js';
80
+
81
+ callTransform(plugin, code, id);
82
+ const output = generateOutput(plugin);
83
+
84
+ expect(output).toContain(`// Drupal.t('First message')`);
85
+ expect(output).toContain(`// Drupal.t('Second message')`);
86
+ expect(output).toContain(`// Drupal.t('Third message')`);
87
+ });
88
+
89
+ it('should deduplicate identical Drupal.t() calls', () => {
90
+ const code = `
91
+ const msg1 = Drupal.t('Same message');
92
+ const msg2 = Drupal.t('Same message');
93
+ `;
94
+ const id = 'test.js';
95
+
96
+ callTransform(plugin, code, id);
97
+ const output = generateOutput(plugin);
98
+
99
+ const matches = output.match(/Drupal\.t\('Same message'\)/g);
100
+ expect(matches).toHaveLength(1);
101
+ });
102
+
103
+ it('should extract Drupal.t() with nested quotes', () => {
104
+ const code = `const message = Drupal.t("It's a test");`;
105
+ const id = 'test.js';
106
+
107
+ callTransform(plugin, code, id);
108
+ const output = generateOutput(plugin);
109
+
110
+ expect(output).toContain(`// Drupal.t("It's a test")`);
111
+ });
112
+
113
+ it('should extract Drupal.t() with special characters in string', () => {
114
+ const code = `const message = Drupal.t('Message with @placeholder and %markup');`;
115
+ const id = 'test.js';
116
+
117
+ callTransform(plugin, code, id);
118
+ const output = generateOutput(plugin);
119
+
120
+ expect(output).toContain(`// Drupal.t('Message with @placeholder and %markup')`);
121
+ });
122
+
123
+ it('should extract Drupal.t() with parentheses in string', () => {
124
+ const code = `const message = Drupal.t('This is a message (with parentheses)');`;
125
+ const id = 'test.js';
126
+
127
+ callTransform(plugin, code, id);
128
+ const output = generateOutput(plugin);
129
+
130
+ expect(output).toContain(`// Drupal.t('This is a message (with parentheses)')`);
131
+ });
132
+
133
+ it('should extract Drupal.t() with nested parentheses in string', () => {
134
+ const code = `const message = Drupal.t('Message (with (nested) parentheses)');`;
135
+ const id = 'test.js';
136
+
137
+ callTransform(plugin, code, id);
138
+ const output = generateOutput(plugin);
139
+
140
+ expect(output).toContain(`// Drupal.t('Message (with (nested) parentheses)')`);
141
+ });
142
+
143
+ it('should extract Drupal.t() with parentheses and parameters', () => {
144
+ const code = `const message = Drupal.t('Total cost (@count items)', {'@count': count});`;
145
+ const id = 'test.js';
146
+
147
+ callTransform(plugin, code, id);
148
+ const output = generateOutput(plugin);
149
+
150
+ expect(output).toContain(`// Drupal.t('Total cost (@count items)', {'@count': count})`);
151
+ });
152
+ });
153
+
154
+ describe('Drupal.t() multiline strings', () => {
155
+ it('should extract multiline template literals', () => {
156
+ const code = `const message = Drupal.t(\`This is a
157
+ multiline string\`);`;
158
+ const id = 'test.js';
159
+
160
+ callTransform(plugin, code, id);
161
+ const output = generateOutput(plugin);
162
+
163
+ expect(output).toContain('// Drupal.t(`This is a');
164
+ expect(output).toContain('multiline string`)');
165
+ });
166
+
167
+ it('should extract single-line template literals', () => {
168
+ const code = 'const message = Drupal.t(`Single line template literal`);';
169
+ const id = 'test.js';
170
+
171
+ callTransform(plugin, code, id);
172
+ const output = generateOutput(plugin);
173
+
174
+ expect(output).toContain('// Drupal.t(`Single line template literal`)');
175
+ });
176
+
177
+ it('should extract template literals with escaped content on single line', () => {
178
+ const code = 'const message = Drupal.t(`Message with ${variable} interpolation`);';
179
+ const id = 'test.js';
180
+
181
+ callTransform(plugin, code, id);
182
+ const output = generateOutput(plugin);
183
+
184
+ expect(output).toContain('// Drupal.t(`Message with ${variable} interpolation`)');
185
+ });
186
+
187
+ it('should extract multiline strings with parameters', () => {
188
+ const code = `const message = Drupal.t(\`Line 1
189
+ Line 2\`, {});`;
190
+ const id = 'test.js';
191
+
192
+ callTransform(plugin, code, id);
193
+ const output = generateOutput(plugin);
194
+
195
+ expect(output).toContain('// Drupal.t(`Line 1');
196
+ expect(output).toContain('Line 2`, {})');
197
+ });
198
+
199
+ it('should extract multiline formatPlural strings', () => {
200
+ const code = `const message = Drupal.formatPlural(5, \`One
201
+ item\`, \`Many
202
+ items\`, {});`;
203
+ const id = 'test.js';
204
+
205
+ callTransform(plugin, code, id);
206
+ const output = generateOutput(plugin);
207
+
208
+ expect(output).toContain('// Drupal.formatPlural(5,');
209
+ expect(output).toContain('`One');
210
+ expect(output).toContain('item`');
211
+ expect(output).toContain('`Many');
212
+ expect(output).toContain('items`, {})');
213
+ });
214
+ });
215
+
216
+ describe('Drupal.formatPlural() extraction', () => {
217
+ // NOTE: The current regex implementation requires at least a third parameter (replacements or options)
218
+ // formatPlural calls without any additional parameters after the two strings won't be matched
219
+
220
+ it('should extract formatPlural with empty replacements object', () => {
221
+ const code = `const msg = Drupal.formatPlural(5, '1 item', '@count items', {});`;
222
+ const id = 'test.js';
223
+
224
+ callTransform(plugin, code, id);
225
+ const output = generateOutput(plugin);
226
+
227
+ expect(output).toContain(`// Drupal.formatPlural(5, '1 item', '@count items', {})`);
228
+ });
229
+
230
+ it('should extract formatPlural with double quotes and empty object', () => {
231
+ const code = `const msg = Drupal.formatPlural(10, "1 item", "@count items", {});`;
232
+ const id = 'test.js';
233
+
234
+ callTransform(plugin, code, id);
235
+ const output = generateOutput(plugin);
236
+
237
+ expect(output).toContain(`// Drupal.formatPlural(10, "1 item", "@count items", {})`);
238
+ });
239
+
240
+ it('should extract formatPlural with replacements', () => {
241
+ const code = `const msg = Drupal.formatPlural(3, '1 item', '@count items', {'@count': 3});`;
242
+ const id = 'test.js';
243
+
244
+ callTransform(plugin, code, id);
245
+ const output = generateOutput(plugin);
246
+
247
+ expect(output).toContain(`// Drupal.formatPlural(3, '1 item', '@count items', {'@count': 3})`);
248
+ });
249
+
250
+ it('should extract formatPlural with options', () => {
251
+ const code = `const msg = Drupal.formatPlural(2, '1 item', '@count items', {}, {context: 'shopping'});`;
252
+ const id = 'test.js';
253
+
254
+ callTransform(plugin, code, id);
255
+ const output = generateOutput(plugin);
256
+
257
+ expect(output).toContain(`// Drupal.formatPlural(2, '1 item', '@count items', {}, {context: 'shopping'})`);
258
+ });
259
+
260
+ it('should extract (Drupal).formatPlural() calls', () => {
261
+ const code = `const msg = (Drupal).formatPlural(7, '1 item', '@count items', {});`;
262
+ const id = 'test.js';
263
+
264
+ callTransform(plugin, code, id);
265
+ const output = generateOutput(plugin);
266
+
267
+ expect(output).toContain(`// Drupal.formatPlural(7, '1 item', '@count items', {})`);
268
+ });
269
+
270
+ it('should extract multiple formatPlural calls', () => {
271
+ const code = `
272
+ const msg1 = Drupal.formatPlural(1, '1 item', '@count items', {});
273
+ const msg2 = Drupal.formatPlural(5, '1 user', '@count users', {});
274
+ `;
275
+ const id = 'test.js';
276
+
277
+ callTransform(plugin, code, id);
278
+ const output = generateOutput(plugin);
279
+
280
+ expect(output).toContain(`// Drupal.formatPlural(1, '1 item', '@count items', {})`);
281
+ expect(output).toContain(`// Drupal.formatPlural(5, '1 user', '@count users', {})`);
282
+ });
283
+
284
+ it('should deduplicate identical formatPlural calls', () => {
285
+ const code = `
286
+ const msg1 = Drupal.formatPlural(8, '1 item', '@count items', {});
287
+ const msg2 = Drupal.formatPlural(8, '1 item', '@count items', {});
288
+ `;
289
+ const id = 'test.js';
290
+
291
+ callTransform(plugin, code, id);
292
+ const output = generateOutput(plugin);
293
+
294
+ const matches = output.match(/Drupal\.formatPlural\(8, '1 item', '@count items', \{\}\)/g);
295
+ expect(matches).toHaveLength(1);
296
+ });
297
+
298
+ it('should extract formatPlural with parentheses in strings', () => {
299
+ const code = `const msg = Drupal.formatPlural(5, '1 item (single)', '@count items (multiple)', {});`;
300
+ const id = 'test.js';
301
+
302
+ callTransform(plugin, code, id);
303
+ const output = generateOutput(plugin);
304
+
305
+ expect(output).toContain(`// Drupal.formatPlural(5, '1 item (single)', '@count items (multiple)', {})`);
306
+ });
307
+ });
308
+
309
+ describe('mixed extraction', () => {
310
+ it('should extract both Drupal.t() and formatPlural() calls', () => {
311
+ const code = `
312
+ const msg1 = Drupal.t('Hello');
313
+ const msg2 = Drupal.formatPlural(4, '1 item', '@count items', {});
314
+ const msg3 = Drupal.t('Goodbye');
315
+ `;
316
+ const id = 'test.js';
317
+
318
+ callTransform(plugin, code, id);
319
+ const output = generateOutput(plugin);
320
+
321
+ expect(output).toContain(`// Drupal.t('Hello')`);
322
+ expect(output).toContain(`// Drupal.formatPlural(4, '1 item', '@count items', {})`);
323
+ expect(output).toContain(`// Drupal.t('Goodbye')`);
324
+ });
325
+
326
+ it('should sort all translations alphabetically', () => {
327
+ const code = `
328
+ const msg1 = Drupal.t('Zebra');
329
+ const msg2 = Drupal.t('Apple');
330
+ const msg3 = Drupal.formatPlural(9, '1', '@count', {});
331
+ `;
332
+ const id = 'test.js';
333
+
334
+ callTransform(plugin, code, id);
335
+ const output = generateOutput(plugin);
336
+
337
+ const lines = output.split('\n').filter(line => line.trim());
338
+ expect(lines[0]).toContain('formatPlural');
339
+ expect(lines[1]).toContain('Apple');
340
+ expect(lines[2]).toContain('Zebra');
341
+ });
342
+ });
343
+
344
+ describe('filter options', () => {
345
+ it('should respect include option', () => {
346
+ plugin = extractDrupalT({ include: '**/*.js' }) as Plugin;
347
+ const code = `const msg = Drupal.t('Test');`;
348
+
349
+ callTransform(plugin, code, 'test.js');
350
+ const output = generateOutput(plugin);
351
+ expect(output).toContain(`// Drupal.t('Test')`);
352
+
353
+ plugin = extractDrupalT({ include: '**/*.js' }) as Plugin;
354
+ callTransform(plugin, code, 'test.ts');
355
+ const output2 = generateOutput(plugin);
356
+ expect(output2).toBe('');
357
+ });
358
+
359
+ it('should respect exclude option', () => {
360
+ plugin = extractDrupalT({ exclude: '**/*.test.js' }) as Plugin;
361
+ const code = `const msg = Drupal.t('Test');`;
362
+
363
+ callTransform(plugin, code, 'main.js');
364
+ const output = generateOutput(plugin);
365
+ expect(output).toContain(`// Drupal.t('Test')`);
366
+
367
+ plugin = extractDrupalT({ exclude: '**/*.test.js' }) as Plugin;
368
+ callTransform(plugin, code, 'file.test.js');
369
+ const output2 = generateOutput(plugin);
370
+ expect(output2).toBe('');
371
+ });
372
+ });
373
+
374
+ describe('edge cases', () => {
375
+ it('should not extract incomplete Drupal.t() calls', () => {
376
+ const code = `const obj = { t: function() {} }; obj.t('not drupal');`;
377
+ const id = 'test.js';
378
+
379
+ callTransform(plugin, code, id);
380
+ const output = generateOutput(plugin);
381
+
382
+ expect(output).not.toContain('not drupal');
383
+ });
384
+
385
+ it('should handle empty strings', () => {
386
+ const code = `const msg = Drupal.t('');`;
387
+ const id = 'test.js';
388
+
389
+ callTransform(plugin, code, id);
390
+ const output = generateOutput(plugin);
391
+
392
+ expect(output).toContain(`// Drupal.t('')`);
393
+ });
394
+
395
+ it('should handle code with no translations', () => {
396
+ const code = `const x = 5; console.log('test');`;
397
+ const id = 'test.js';
398
+
399
+ callTransform(plugin, code, id);
400
+ const output = generateOutput(plugin);
401
+
402
+ expect(output).toBe('');
403
+ });
404
+ });
405
+ });
406
+
407
+ // Helper functions
408
+ function createPluginContext(): any {
409
+ const emittedFiles: any[] = [];
410
+ return {
411
+ emitFile(file: any) {
412
+ emittedFiles.push(file);
413
+ },
414
+ _emittedFiles: emittedFiles,
415
+ };
416
+ }
417
+
418
+ function callTransform(plugin: Plugin, code: string, id: string): void {
419
+ const context = createPluginContext();
420
+ const transform = plugin.transform;
421
+
422
+ if (typeof transform === 'function') {
423
+ transform.call(context, code, id);
424
+ } else if (transform && typeof transform === 'object' && 'handler' in transform) {
425
+ transform.handler.call(context, code, id);
426
+ }
427
+ }
428
+
429
+ function generateOutput(plugin: Plugin): string {
430
+ const context = createPluginContext();
431
+ const generateBundle = plugin.generateBundle;
432
+
433
+ if (typeof generateBundle === 'function') {
434
+ generateBundle.call(context, {} as any, {} as any, false);
435
+ } else if (generateBundle && typeof generateBundle === 'object' && 'handler' in generateBundle) {
436
+ generateBundle.handler.call(context, {} as any, {} as any, false);
437
+ }
438
+
439
+ if (context._emittedFiles.length === 0) {
440
+ return '';
441
+ }
442
+
443
+ return context._emittedFiles[0]?.source || '';
444
+ }
package/src/index.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { createFilter } from '@rollup/pluginutils';
2
+ import type { Plugin } from 'vite';
3
+
4
+ export interface ExtractDrupalTOptions {
5
+ include?: string | string[];
6
+ exclude?: string | string[];
7
+ }
8
+
9
+ export default function extractDrupalT(options: ExtractDrupalTOptions = {}): Plugin {
10
+ const filter = createFilter(options.include, options.exclude);
11
+ const translations = new Set() as Set<string>;
12
+
13
+ return {
14
+ name: 'extract-drupal-t',
15
+ config() {
16
+ return {
17
+ build: {
18
+ rollupOptions: {
19
+ external: ['Drupal'],
20
+ output: {
21
+ globals: {
22
+ Drupal: 'Drupal',
23
+ },
24
+ },
25
+ },
26
+ },
27
+ };
28
+ },
29
+ transform(code: string, id: string) {
30
+ if (!filter(id)) return null;
31
+
32
+ // Match Drupal.t('...') and (Drupal).t('...') calls, including parameters.
33
+ // Using [\s\S] instead of . to match multiline strings (including newlines)
34
+ const regex = /((\(Drupal\)|Drupal)\.t\((['"`][\s\S]*?['"`](?:,[\s\S]*?)*?)\))/g;
35
+ let match: RegExpExecArray | null;
36
+
37
+ while ((match = regex.exec(code)) !== null) {
38
+ translations.add(match[0]);
39
+ }
40
+
41
+ // Match Drupal.formatPlural('...', '...', '...', ...);
42
+ // Using [\s\S] instead of . to match multiline strings (including newlines)
43
+ const pluralRegex = /((\(Drupal\)|Drupal)\.formatPlural\((\d+),\s*(['"`][\s\S]*?['"`]),\s*(['"`][\s\S]*?['"`]),\s*([\s\S]*?)(?:,\s*([\s\S]*?))?\))/g;
44
+ let pluralMatch: RegExpExecArray | null;
45
+
46
+ while ((pluralMatch = pluralRegex.exec(code)) !== null) {
47
+ translations.add(pluralMatch[0]);
48
+ }
49
+
50
+ return null;
51
+ },
52
+ generateBundle() {
53
+ const bundleContent = Array.from(translations)
54
+ // Sort translations to ensure consistent order.
55
+ .sort()
56
+ // Format each translation as a comment.
57
+ // Replace '(Drupal)' with 'Drupal' for consistency.
58
+ // Drupal can find the translations it needs to include.
59
+ .map((translation) => `// ${translation.replace('(Drupal)', 'Drupal')}`)
60
+ .join('\n');
61
+
62
+ this.emitFile({
63
+ type: 'asset',
64
+ fileName: 'translations.js',
65
+ source: bundleContent,
66
+ });
67
+ },
68
+ };
69
+ }