Skip to content

Commit eb6889c

Browse files
justjam2013bwp91
authored andcommitted
add support for plugin ui i18n
1 parent e7117fd commit eb6889c

7 files changed

Lines changed: 156 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ All notable changes to `homebridge-config-ui-x` will be documented in this file.
1010
- add status to security system modal
1111
- move reset status layout button into row
1212
- expose unique id on accessory info modal + copy icon (@NorthernMan54)
13+
- add support for plugin ui i18n (from #2597) (@justjam2013)
1314

1415
### Other Changes
1516

src/modules/plugins/plugins.interfaces.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export interface HomebridgePlugin {
3939
to: string
4040
switch: string
4141
}
42+
directories?: {
43+
schemas?: string
44+
}
4245
}
4346

4447
export interface HomebridgePluginUiMetadata {
@@ -157,6 +160,9 @@ export interface IPackageJson {
157160
private?: boolean
158161
publishConfig?: { registry?: string }
159162
deprecated?: string
163+
directories?: {
164+
schemas?: string
165+
}
160166
}
161167

162168
export type NpmFunding = { type: string, url: string } | string | Array<{ type: string, url: string } | string>

src/modules/plugins/plugins.service.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1088,7 +1088,21 @@ export class PluginsService {
10881088
throw new NotFoundException()
10891089
}
10901090

1091-
const schemaPath = resolve(plugin.installPath, pluginName, 'config.schema.json')
1091+
let schemaPath: string
1092+
1093+
const i18nPath = plugin.directories?.schemas
1094+
if (i18nPath) {
1095+
const lang = this.configService.ui.lang === 'auto' ? 'en' : this.configService.ui.lang
1096+
1097+
if (lang && lang !== 'en' && lang !== 'auto') {
1098+
const i18nSchemaPath = resolve(plugin.installPath, pluginName, i18nPath, `config.schema.${lang}.json`)
1099+
if (existsSync(i18nSchemaPath)) {
1100+
schemaPath = i18nSchemaPath
1101+
}
1102+
}
1103+
}
1104+
1105+
schemaPath ??= resolve(plugin.installPath, pluginName, 'config.schema.json')
10921106

10931107
let configSchema = await readJson(schemaPath)
10941108

@@ -1652,6 +1666,9 @@ export class PluginsService {
16521666
// Only verified plugins can show donation links
16531667
plugin.funding = (plugin.verifiedPlugin || plugin.verifiedPlusPlugin) ? pkgJson.funding : undefined
16541668

1669+
// Add directories for i18n schema support
1670+
plugin.directories = pkgJson.directories
1671+
16551672
// If the plugin is private, do not attempt to query npm
16561673
if (pkgJson.private) {
16571674
plugin.publicPackage = false

test/e2e/plugins.e2e-spec.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,102 @@ describe('PluginController (e2e)', () => {
288288
expect(res.json().pluginType).toBe('platform')
289289
})
290290

291+
it('GET /plugins/config-schema/:plugin-name (i18n - French)', async () => {
292+
// Mock the language setting to French
293+
const originalLang = (pluginsService as any).configService.ui.lang;
294+
(pluginsService as any).configService.ui.lang = 'fr'
295+
296+
const res = await app.inject({
297+
method: 'GET',
298+
path: '/plugins/config-schema/homebridge-mock-plugin',
299+
headers: {
300+
authorization,
301+
},
302+
})
303+
304+
expect(res.statusCode).toBe(200)
305+
expect(res.json().pluginAlias).toBe('ExampleHomebridgePlugin')
306+
expect(res.json().pluginType).toBe('platform')
307+
// Verify French translation is loaded
308+
expect(res.json().schema.properties.name.title).toBe('Nom')
309+
expect(res.json().schema.properties.name.default).toBe('Exemple de plateforme dynamique')
310+
311+
// Restore original language
312+
;(pluginsService as any).configService.ui.lang = originalLang
313+
})
314+
315+
it('GET /plugins/config-schema/:plugin-name (i18n - German)', async () => {
316+
// Mock the language setting to German
317+
const originalLang = (pluginsService as any).configService.ui.lang;
318+
(pluginsService as any).configService.ui.lang = 'de'
319+
320+
const res = await app.inject({
321+
method: 'GET',
322+
path: '/plugins/config-schema/homebridge-mock-plugin',
323+
headers: {
324+
authorization,
325+
},
326+
})
327+
328+
expect(res.statusCode).toBe(200)
329+
expect(res.json().pluginAlias).toBe('ExampleHomebridgePlugin')
330+
expect(res.json().pluginType).toBe('platform')
331+
// Verify German translation is loaded
332+
expect(res.json().schema.properties.name.title).toBe('Name')
333+
expect(res.json().schema.properties.name.default).toBe('Beispiel Dynamische Plattform')
334+
335+
// Restore original language
336+
;(pluginsService as any).configService.ui.lang = originalLang
337+
})
338+
339+
it('GET /plugins/config-schema/:plugin-name (i18n - fallback to base for unsupported language)', async () => {
340+
// Mock the language setting to a language that doesn't have a translation
341+
const originalLang = (pluginsService as any).configService.ui.lang;
342+
(pluginsService as any).configService.ui.lang = 'es'
343+
344+
const res = await app.inject({
345+
method: 'GET',
346+
path: '/plugins/config-schema/homebridge-mock-plugin',
347+
headers: {
348+
authorization,
349+
},
350+
})
351+
352+
expect(res.statusCode).toBe(200)
353+
expect(res.json().pluginAlias).toBe('ExampleHomebridgePlugin')
354+
expect(res.json().pluginType).toBe('platform')
355+
// Verify base English schema is loaded as fallback
356+
expect(res.json().schema.properties.name.title).toBe('Name')
357+
expect(res.json().schema.properties.name.default).toBe('Example Dynamic Platform')
358+
359+
// Restore original language
360+
;(pluginsService as any).configService.ui.lang = originalLang
361+
})
362+
363+
it('GET /plugins/config-schema/:plugin-name (i18n - English explicitly)', async () => {
364+
// Mock the language setting to English (should skip i18n directory)
365+
const originalLang = (pluginsService as any).configService.ui.lang;
366+
(pluginsService as any).configService.ui.lang = 'en'
367+
368+
const res = await app.inject({
369+
method: 'GET',
370+
path: '/plugins/config-schema/homebridge-mock-plugin',
371+
headers: {
372+
authorization,
373+
},
374+
})
375+
376+
expect(res.statusCode).toBe(200)
377+
expect(res.json().pluginAlias).toBe('ExampleHomebridgePlugin')
378+
expect(res.json().pluginType).toBe('platform')
379+
// Verify base English schema is loaded (not from i18n directory)
380+
expect(res.json().schema.properties.name.title).toBe('Name')
381+
expect(res.json().schema.properties.name.default).toBe('Example Dynamic Platform')
382+
383+
// Restore original language
384+
;(pluginsService as any).configService.ui.lang = originalLang
385+
})
386+
291387
it('GET /plugins/changelog/:plugin-name', async () => {
292388
const res = await app.inject({
293389
method: 'GET',
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"pluginAlias": "ExampleHomebridgePlugin",
3+
"pluginType": "platform",
4+
"singular": true,
5+
"schema": {
6+
"type": "object",
7+
"properties": {
8+
"name": {
9+
"title": "Name",
10+
"type": "string",
11+
"required": true,
12+
"default": "Beispiel Dynamische Plattform"
13+
}
14+
}
15+
}
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"pluginAlias": "ExampleHomebridgePlugin",
3+
"pluginType": "platform",
4+
"singular": true,
5+
"schema": {
6+
"type": "object",
7+
"properties": {
8+
"name": {
9+
"title": "Nom",
10+
"type": "string",
11+
"required": true,
12+
"default": "Exemple de plateforme dynamique"
13+
}
14+
}
15+
}
16+
}

test/mocks/plugins/homebridge-mock-plugin/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
"engines": {
1212
"homebridge": ">0.4.53"
1313
},
14+
"directories": {
15+
"schemas": "i18n"
16+
},
1417
"dependencies": {},
1518
"devDependencies": {}
1619
}

0 commit comments

Comments
 (0)