i18n: wires up translations for plugins (#102853)

* i18n: consolidate i18n types & runtime services

* Chore: updates after PR feedback

* Chore: updates after feedback

* Chore: updates after feedback

* Chore: adds feature toggle

* Chore: adds locale to backend

* Chore: adds locales to i18n instance

* Chore: fix missing path in CODEOWNERS

* Chore: fix go lint issues

* Chore: fix missing path in CODEOWNERS

* Chore: updates after PR feedback

* Trigger build

* Chore: updates after PR feedback

* Chore: use resolved language for lookup

* Chore: updates after PR feedback

* Update pkg/plugins/plugins.go

Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>

* Chore: updates after PR feedback

* Chore: updates after PR feedback

---------

Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
This commit is contained in:
Hugo Häggmark
2025-03-31 06:38:38 +02:00
committed by GitHub
co-authored by Will Browne
parent 7ea0fab606
commit 18ae5d7f0c
29 changed files with 759 additions and 46 deletions
+6 -2
View File
@@ -154,6 +154,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
Sort: getPanelSort(panel.ID),
Angular: panel.Angular,
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), panel),
Translations: panel.Translations,
}
}
@@ -489,6 +490,7 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug
Angular: plugin.Angular,
MultiValueFilterOperators: plugin.MultiValueFilterOperators,
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), plugin),
Translations: plugin.Translations,
}
if ds.JsonData == nil {
@@ -571,8 +573,9 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug
Signature: ds.Signature,
Module: ds.Module,
// ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), ds),
BaseURL: ds.BaseURL,
Angular: ds.Angular,
BaseURL: ds.BaseURL,
Angular: ds.Angular,
Translations: ds.Translations,
},
}
if ds.Name == grafanads.DatasourceName {
@@ -597,6 +600,7 @@ func (hs *HTTPServer) newAppDTO(ctx context.Context, plugin pluginstore.Plugin,
Extensions: plugin.Extensions,
Dependencies: plugin.Dependencies,
ModuleHash: hs.pluginAssets.ModuleHash(ctx, plugin),
Translations: plugin.Translations,
}
if settings.Enabled {
+234
View File
@@ -1,6 +1,7 @@
package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
@@ -24,6 +25,10 @@ import (
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/datasources"
datafakes "github.com/grafana/grafana/pkg/services/datasources/fakes"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
@@ -34,6 +39,7 @@ import (
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/updatechecker"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
@@ -96,6 +102,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
SocialService: socialimpl.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeCacheStorage(), nil, &ssosettingstests.MockService{}),
managedPluginsService: managedplugins.NewNoop(),
tracer: tracing.InitializeTracerForTest(),
DataSourcesService: &datafakes.FakeDataSourceService{},
}
m := web.New()
@@ -455,6 +462,233 @@ func newAppSettings(id string, enabled bool) map[string]*pluginsettings.DTO {
}
}
func TestHTTPServer_GetFrontendSettings_translations(t *testing.T) {
type settings struct {
Datasources map[string]plugins.DataSourceDTO `json:"datasources"`
Panels map[string]*plugins.PanelDTO `json:"panels"`
Apps map[string]*plugins.AppDTO `json:"apps"`
}
tests := []struct {
desc string
pluginStore func() pluginstore.Store
expected settings
signedInUser *user.SignedInUser
}{
{
desc: "built in datasource plugin with translations",
pluginStore: func() pluginstore.Store {
return &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{
{
Module: fmt.Sprintf("/%s/module.js", "test-app"),
JSONData: plugins.JSONData{
ID: "test-app",
Info: plugins.Info{Version: "0.5.0"},
Type: plugins.TypeDataSource,
BuiltIn: true,
},
Translations: map[string]string{
"en-US": "public/plugins/test-app/locales/en-US/test-app.json",
"pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json",
},
},
},
}
},
expected: settings{
Datasources: map[string]plugins.DataSourceDTO{
"": {
Type: string(plugins.TypeDataSource),
JSONData: make(map[string]any),
PluginMeta: &plugins.PluginMetaDTO{
JSONData: plugins.JSONData{
ID: "test-app",
Info: plugins.Info{Version: "0.5.0"},
Type: plugins.TypeDataSource,
BuiltIn: true,
},
Module: "/test-app/module.js",
Translations: map[string]string{
"en-US": "public/plugins/test-app/locales/en-US/test-app.json",
"pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json",
},
},
},
},
Panels: map[string]*plugins.PanelDTO{},
Apps: map[string]*plugins.AppDTO{},
},
},
{
desc: "non-builtin datasource plugin with translations",
pluginStore: func() pluginstore.Store {
return &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{
{
Module: fmt.Sprintf("/%s/module.js", "test-app"),
JSONData: plugins.JSONData{
ID: "test-app",
Info: plugins.Info{Version: "0.5.0"},
Type: plugins.TypeDataSource,
},
Translations: map[string]string{
"en-US": "public/plugins/test-app/locales/en-US/test-app.json",
"pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json",
},
},
},
}
},
signedInUser: &user.SignedInUser{
OrgID: 1,
},
expected: settings{
Datasources: map[string]plugins.DataSourceDTO{
"test-app": {
Type: "test-app",
Name: "test-app",
JSONData: make(map[string]any),
Module: "/test-app/module.js",
PluginMeta: &plugins.PluginMetaDTO{
Module: "/test-app/module.js",
JSONData: plugins.JSONData{
ID: "test-app",
Info: plugins.Info{Version: "0.5.0"},
Type: plugins.TypeDataSource,
},
LoadingStrategy: "script",
Translations: map[string]string{
"en-US": "public/plugins/test-app/locales/en-US/test-app.json",
"pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json",
},
},
},
},
Panels: map[string]*plugins.PanelDTO{},
Apps: map[string]*plugins.AppDTO{},
},
},
{
desc: "panel plugin with translations",
pluginStore: func() pluginstore.Store {
return &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{
{
Module: fmt.Sprintf("/%s/module.js", "test-app"),
JSONData: plugins.JSONData{
ID: "test-app",
Info: plugins.Info{Version: "0.5.0"},
Type: plugins.TypePanel,
},
Translations: map[string]string{
"en-US": "public/plugins/test-app/locales/en-US/test-app.json",
"pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json",
},
},
},
}
},
expected: settings{
Datasources: map[string]plugins.DataSourceDTO{},
Panels: map[string]*plugins.PanelDTO{
"test-app": {
ID: "test-app",
Info: plugins.Info{Version: "0.5.0"},
Sort: 100,
Module: "/test-app/module.js",
LoadingStrategy: "script",
ModuleHash: "",
Translations: map[string]string{
"en-US": "public/plugins/test-app/locales/en-US/test-app.json",
"pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json",
},
},
},
Apps: map[string]*plugins.AppDTO{},
},
},
{
desc: "app plugin with translations",
pluginStore: func() pluginstore.Store {
return &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{
{
Module: fmt.Sprintf("/%s/module.js", "test-app"),
JSONData: plugins.JSONData{
ID: "test-app",
Info: plugins.Info{Version: "0.5.0"},
Type: plugins.TypeApp,
},
Translations: map[string]string{
"en-US": "public/plugins/test-app/locales/en-US/test-app.json",
"pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json",
},
},
},
}
},
expected: settings{
Datasources: map[string]plugins.DataSourceDTO{},
Panels: map[string]*plugins.PanelDTO{},
Apps: map[string]*plugins.AppDTO{
"test-app": {
ID: "test-app",
LoadingStrategy: "script",
ModuleHash: "",
Path: "/test-app/module.js",
Version: "0.5.0",
Translations: map[string]string{
"en-US": "public/plugins/test-app/locales/en-US/test-app.json",
"pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json",
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
cfg := setting.NewCfg()
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), test.pluginStore(), nil, nil)
// Create a request with the appropriate context
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
recorder := httptest.NewRecorder()
if test.signedInUser != nil {
_, err := hs.DataSourcesService.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{
Name: "test-app",
Type: "test-app",
OrgID: 1,
})
require.NoError(t, err)
m.UseMiddleware(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
reqContext := &contextmodel.ReqContext{
Context: web.FromContext(ctx),
SignedInUser: test.signedInUser,
PublicDashboardAccessToken: "test-token",
}
ctx = context.WithValue(ctx, ctxkey.Key{}, reqContext)
*reqContext.Req = *reqContext.Req.WithContext(ctx)
next.ServeHTTP(w, r.WithContext(ctx))
})
})
}
m.ServeHTTP(recorder, req)
var got settings
err := json.Unmarshal(recorder.Body.Bytes(), &got)
require.NoError(t, err)
require.Equal(t, http.StatusOK, recorder.Code)
require.EqualValues(t, test.expected, got)
})
}
}
func newPluginAssets() func() *pluginassets.Service {
return newPluginAssetsWithConfig(&config.PluginManagementCfg{})
}