Skip to content

Commit 82f335f

Browse files
committed
implement 'add i18n' script with full automation
Implements complete i18n setup automation: - Updates docker-compose.yaml with localizationForPlugins feature toggle - Updates plugin.json with languages array and grafanaDependency >= 12.1.0 - Creates locale folders and translation files for selected locales - Adds @grafana/i18n dependency to package.json - Adds i18next-cli dev dependency and i18n-extract script - Creates i18next.config.ts with proper extraction configuration - Updates eslint.config.mjs with i18n linting rules - Adds i18n imports to module.ts/tsx automatically safely runs multiple times without breaking - Comprehensive test suite with 8 passing tests covering: * Idempotency verification * Single and multiple locale support * Skipping when already configured * Handling existing feature toggles * Support for both .ts and .tsx modules * Version-aware dependency updates * Edge cases (missing scripts, etc.) This automates all manual steps shown in grafana/grafana-plugin-examples#588 Fri Oct 17 09:48:52 2025 +0200 implement 'add' command with pre-flight checks implement 'add i18n' script with full automation document new 'add' command and 'add i18n' feature include additions scripts in rollup build packages/create-plugin/src/additions/scripts/add-i18n.test.ts packages/create-plugin/src/additions/scripts/add-i18n.ts
1 parent a133c5d commit 82f335f

File tree

2 files changed

+747
-0
lines changed

2 files changed

+747
-0
lines changed
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { Context } from '../../migrations/context.js';
4+
import migrate from './add-i18n.js';
5+
6+
describe('add-i18n', () => {
7+
it('should be idempotent', async () => {
8+
const context = new Context('/virtual');
9+
10+
// Set up a minimal plugin structure
11+
context.addFile(
12+
'src/plugin.json',
13+
JSON.stringify({
14+
id: 'test-plugin',
15+
type: 'panel',
16+
name: 'Test Plugin',
17+
dependencies: {
18+
grafanaDependency: '>=11.0.0',
19+
},
20+
})
21+
);
22+
context.addFile('docker-compose.yaml', 'services:\n grafana:\n environment:\n FOO: bar');
23+
context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {} }));
24+
context.addFile(
25+
'eslint.config.mjs',
26+
'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
27+
);
28+
context.addFile(
29+
'src/module.ts',
30+
'import { PanelPlugin } from "@grafana/data";\nexport const plugin = new PanelPlugin();'
31+
);
32+
33+
const migrateWithOptions = (ctx: Context) => migrate(ctx, { locales: ['en-US'] });
34+
await expect(migrateWithOptions).toBeIdempotent(context);
35+
});
36+
37+
it('should add i18n support with a single locale', () => {
38+
const context = new Context('/virtual');
39+
40+
// Set up a minimal plugin structure
41+
context.addFile(
42+
'src/plugin.json',
43+
JSON.stringify({
44+
id: 'test-plugin',
45+
type: 'panel',
46+
name: 'Test Plugin',
47+
dependencies: {
48+
grafanaDependency: '>=11.0.0',
49+
},
50+
})
51+
);
52+
context.addFile(
53+
'docker-compose.yaml',
54+
`services:
55+
grafana:
56+
environment:
57+
FOO: bar`
58+
);
59+
context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {}, scripts: {} }));
60+
context.addFile(
61+
'eslint.config.mjs',
62+
'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
63+
);
64+
context.addFile(
65+
'src/module.ts',
66+
'import { PanelPlugin } from "@grafana/data";\nexport const plugin = new PanelPlugin();'
67+
);
68+
69+
const result = migrate(context, { locales: ['en-US'] });
70+
71+
// Check plugin.json was updated
72+
const pluginJson = JSON.parse(result.getFile('src/plugin.json') || '{}');
73+
expect(pluginJson.languages).toEqual(['en-US']);
74+
expect(pluginJson.dependencies.grafanaDependency).toBe('>=12.1.0');
75+
76+
// Check locale file was created
77+
expect(result.doesFileExist('src/locales/en-US/test-plugin.json')).toBe(true);
78+
const localeContent = result.getFile('src/locales/en-US/test-plugin.json');
79+
expect(JSON.parse(localeContent || '{}')).toEqual({});
80+
81+
// Check package.json was updated with dependencies
82+
const packageJson = JSON.parse(result.getFile('package.json') || '{}');
83+
expect(packageJson.dependencies['@grafana/i18n']).toBeDefined();
84+
expect(packageJson.devDependencies['i18next-cli']).toBeDefined();
85+
expect(packageJson.scripts['i18n-extract']).toBe('i18next-cli extract --sync-primary');
86+
87+
// Check docker-compose.yaml was updated
88+
const dockerCompose = result.getFile('docker-compose.yaml');
89+
expect(dockerCompose).toContain('localizationForPlugins');
90+
91+
// Check module.ts was updated
92+
const moduleTs = result.getFile('src/module.ts');
93+
expect(moduleTs).toContain('@grafana/i18n');
94+
95+
// Check i18next.config.ts was created
96+
expect(result.doesFileExist('i18next.config.ts')).toBe(true);
97+
const i18nextConfig = result.getFile('i18next.config.ts');
98+
expect(i18nextConfig).toContain('defineConfig');
99+
expect(i18nextConfig).toContain('pluginJson.id');
100+
});
101+
102+
it('should add i18n support with multiple locales', () => {
103+
const context = new Context('/virtual');
104+
105+
context.addFile(
106+
'src/plugin.json',
107+
JSON.stringify({
108+
id: 'test-plugin',
109+
type: 'panel',
110+
name: 'Test Plugin',
111+
dependencies: {
112+
grafanaDependency: '>=11.0.0',
113+
},
114+
})
115+
);
116+
context.addFile(
117+
'docker-compose.yaml',
118+
`services:
119+
grafana:
120+
environment:
121+
FOO: bar`
122+
);
123+
context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {}, scripts: {} }));
124+
context.addFile(
125+
'eslint.config.mjs',
126+
'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
127+
);
128+
context.addFile(
129+
'src/module.ts',
130+
'import { PanelPlugin } from "@grafana/data";\nexport const plugin = new PanelPlugin();'
131+
);
132+
133+
const result = migrate(context, { locales: ['en-US', 'es-ES', 'sv-SE'] });
134+
135+
// Check plugin.json has all locales
136+
const pluginJson = JSON.parse(result.getFile('src/plugin.json') || '{}');
137+
expect(pluginJson.languages).toEqual(['en-US', 'es-ES', 'sv-SE']);
138+
139+
// Check all locale files were created
140+
expect(result.doesFileExist('src/locales/en-US/test-plugin.json')).toBe(true);
141+
expect(result.doesFileExist('src/locales/es-ES/test-plugin.json')).toBe(true);
142+
expect(result.doesFileExist('src/locales/sv-SE/test-plugin.json')).toBe(true);
143+
});
144+
145+
it('should skip if i18n is already configured', () => {
146+
const context = new Context('/virtual');
147+
148+
// Set up a plugin with i18n already configured
149+
context.addFile(
150+
'src/plugin.json',
151+
JSON.stringify({
152+
id: 'test-plugin',
153+
type: 'panel',
154+
name: 'Test Plugin',
155+
languages: ['en-US'], // Already configured
156+
dependencies: {
157+
grafanaDependency: '>=12.1.0',
158+
},
159+
})
160+
);
161+
context.addFile(
162+
'docker-compose.yaml',
163+
`services:
164+
grafana:
165+
environment:
166+
FOO: bar`
167+
);
168+
context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {}, scripts: {} }));
169+
context.addFile(
170+
'eslint.config.mjs',
171+
'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
172+
);
173+
context.addFile(
174+
'src/module.ts',
175+
'import { PanelPlugin } from "@grafana/data";\nimport { i18n } from "@grafana/i18n";\nexport const plugin = new PanelPlugin();'
176+
);
177+
178+
// Flush the context to simulate these files existing on "disk"
179+
const initialChanges = Object.keys(context.listChanges()).length;
180+
181+
const result = migrate(context, { locales: ['es-ES'] });
182+
183+
// Should not add any NEW changes beyond the initial setup
184+
const finalChanges = Object.keys(result.listChanges()).length;
185+
expect(finalChanges).toBe(initialChanges);
186+
});
187+
188+
it('should handle existing feature toggles in docker-compose.yaml', () => {
189+
const context = new Context('/virtual');
190+
191+
context.addFile(
192+
'src/plugin.json',
193+
JSON.stringify({
194+
id: 'test-plugin',
195+
type: 'panel',
196+
name: 'Test Plugin',
197+
dependencies: {
198+
grafanaDependency: '>=11.0.0',
199+
},
200+
})
201+
);
202+
context.addFile(
203+
'docker-compose.yaml',
204+
`services:
205+
grafana:
206+
environment:
207+
GF_FEATURE_TOGGLES_ENABLE: someOtherFeature`
208+
);
209+
context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {}, scripts: {} }));
210+
context.addFile(
211+
'eslint.config.mjs',
212+
'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
213+
);
214+
context.addFile(
215+
'src/module.ts',
216+
'import { PanelPlugin } from "@grafana/data";\nexport const plugin = new PanelPlugin();'
217+
);
218+
219+
const result = migrate(context, { locales: ['en-US'] });
220+
221+
const dockerCompose = result.getFile('docker-compose.yaml');
222+
expect(dockerCompose).toContain('someOtherFeature,localizationForPlugins');
223+
});
224+
225+
it('should work with module.tsx instead of module.ts', () => {
226+
const context = new Context('/virtual');
227+
228+
context.addFile(
229+
'src/plugin.json',
230+
JSON.stringify({
231+
id: 'test-plugin',
232+
type: 'panel',
233+
name: 'Test Plugin',
234+
dependencies: {
235+
grafanaDependency: '>=11.0.0',
236+
},
237+
})
238+
);
239+
context.addFile(
240+
'docker-compose.yaml',
241+
`services:
242+
grafana:
243+
environment:
244+
FOO: bar`
245+
);
246+
context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {}, scripts: {} }));
247+
context.addFile(
248+
'eslint.config.mjs',
249+
'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
250+
);
251+
context.addFile(
252+
'src/module.tsx',
253+
'import { PanelPlugin } from "@grafana/data";\nexport const plugin = new PanelPlugin();'
254+
);
255+
256+
const result = migrate(context, { locales: ['en-US'] });
257+
258+
const moduleTsx = result.getFile('src/module.tsx');
259+
expect(moduleTsx).toContain('@grafana/i18n');
260+
});
261+
262+
it('should not update grafanaDependency if it is already >= 12.1.0', () => {
263+
const context = new Context('/virtual');
264+
265+
context.addFile(
266+
'src/plugin.json',
267+
JSON.stringify({
268+
id: 'test-plugin',
269+
type: 'panel',
270+
name: 'Test Plugin',
271+
dependencies: {
272+
grafanaDependency: '>=13.0.0',
273+
},
274+
})
275+
);
276+
context.addFile(
277+
'docker-compose.yaml',
278+
`services:
279+
grafana:
280+
environment:
281+
FOO: bar`
282+
);
283+
context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {}, scripts: {} }));
284+
context.addFile(
285+
'eslint.config.mjs',
286+
'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
287+
);
288+
context.addFile(
289+
'src/module.ts',
290+
'import { PanelPlugin } from "@grafana/data";\nexport const plugin = new PanelPlugin();'
291+
);
292+
293+
const result = migrate(context, { locales: ['en-US'] });
294+
295+
const pluginJson = JSON.parse(result.getFile('src/plugin.json') || '{}');
296+
expect(pluginJson.dependencies.grafanaDependency).toBe('>=13.0.0');
297+
});
298+
299+
it('should handle plugins without existing scripts in package.json', () => {
300+
const context = new Context('/virtual');
301+
302+
context.addFile(
303+
'src/plugin.json',
304+
JSON.stringify({
305+
id: 'test-plugin',
306+
type: 'panel',
307+
name: 'Test Plugin',
308+
dependencies: {
309+
grafanaDependency: '>=11.0.0',
310+
},
311+
})
312+
);
313+
context.addFile(
314+
'docker-compose.yaml',
315+
`services:
316+
grafana:
317+
environment:
318+
FOO: bar`
319+
);
320+
context.addFile('package.json', JSON.stringify({ dependencies: {}, devDependencies: {} })); // No scripts field
321+
context.addFile(
322+
'eslint.config.mjs',
323+
'import { defineConfig } from "eslint/config";\nexport default defineConfig([]);'
324+
);
325+
context.addFile(
326+
'src/module.ts',
327+
'import { PanelPlugin } from "@grafana/data";\nexport const plugin = new PanelPlugin();'
328+
);
329+
330+
const result = migrate(context, { locales: ['en-US'] });
331+
332+
const packageJson = JSON.parse(result.getFile('package.json') || '{}');
333+
expect(packageJson.scripts['i18n-extract']).toBe('i18next-cli extract --sync-primary');
334+
});
335+
});

0 commit comments

Comments
 (0)