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:
co-authored by
Will Browne
parent
7ea0fab606
commit
18ae5d7f0c
@@ -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 {
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user