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{})
}
+1
View File
@@ -35,6 +35,7 @@ type Features struct {
SkipHostEnvVarsEnabled bool
SriChecksEnabled bool
PluginsCDNSyncLoaderEnabled bool
LocalizationForPlugins bool
}
// NewPluginManagementCfg returns a new PluginManagementCfg.
@@ -153,3 +153,22 @@ func getBaseDir(pluginDir string) string {
}
return baseDir
}
func (s *Service) GetTranslations(n PluginInfo) (map[string]string, error) {
pathToTranslations, err := s.RelativeURL(n, "locales")
if err != nil {
return nil, fmt.Errorf("get locales: %w", err)
}
// loop through all the languages specified in the plugin.json and add them to the list
translations := map[string]string{}
for _, language := range n.pluginJSON.Languages {
file := fmt.Sprintf("%s.json", n.pluginJSON.ID)
translations[language], err = url.JoinPath(pathToTranslations, language, file)
if err != nil {
return nil, fmt.Errorf("join path: %w", err)
}
}
return translations, nil
}
@@ -190,6 +190,21 @@ func TestService(t *testing.T) {
require.NoError(t, err)
require.Equal(t, oneCDNRelativeURL, u)
})
t.Run("GetTranslations", func(t *testing.T) {
pluginInfo := NewPluginInfo(jsonData["one"], plugins.ClassExternal, pluginFS("one"), nil)
pluginInfo.pluginJSON.Languages = []string{"en-US", "pt-BR"}
translations, err := svc.GetTranslations(pluginInfo)
require.NoError(t, err)
oneCDNURL, err := url.JoinPath(tc.cdnBaseURL, "one", "1.0.0", "public", "plugins", "one")
require.NoError(t, err)
enURL, err := url.JoinPath(oneCDNURL, "locales", "en-US", "one.json")
require.NoError(t, err)
ptBRURL, err := url.JoinPath(oneCDNURL, "locales", "pt-BR", "one.json")
require.NoError(t, err)
require.Equal(t, map[string]string{"en-US": enURL, "pt-BR": ptBRURL}, translations)
})
})
}
}
@@ -44,7 +44,7 @@ type Opts struct {
// New returns a new Bootstrap stage.
func New(cfg *config.PluginManagementCfg, opts Opts) *Bootstrap {
if opts.ConstructFunc == nil {
opts.ConstructFunc = DefaultConstructFunc(signature.DefaultCalculator(cfg), assetpath.DefaultService(cfg))
opts.ConstructFunc = DefaultConstructFunc(cfg, signature.DefaultCalculator(cfg), assetpath.DefaultService(cfg))
}
if opts.DecorateFuncs == nil {
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath"
)
@@ -16,11 +17,12 @@ type pluginFactoryFunc func(p *plugins.FoundBundle, pluginClass plugins.Class, s
// service to set the plugin's BaseURL, Module, Logos and Screenshots fields.
type DefaultPluginFactory struct {
assetPath *assetpath.Service
features *config.Features
}
// NewDefaultPluginFactory returns a new DefaultPluginFactory.
func NewDefaultPluginFactory(assetPath *assetpath.Service) *DefaultPluginFactory {
return &DefaultPluginFactory{assetPath: assetPath}
func NewDefaultPluginFactory(features *config.Features, assetPath *assetpath.Service) *DefaultPluginFactory {
return &DefaultPluginFactory{assetPath: assetPath, features: features}
}
func (f *DefaultPluginFactory) createPlugin(bundle *plugins.FoundBundle, class plugins.Class,
@@ -74,6 +76,13 @@ func (f *DefaultPluginFactory) newPlugin(p plugins.FoundPlugin, class plugins.Cl
if err = setImages(plugin, f.assetPath, info); err != nil {
return nil, err
}
if f.features.LocalizationForPlugins {
if err := setTranslations(plugin, f.assetPath, info); err != nil {
return nil, err
}
}
return plugin, nil
}
@@ -99,3 +108,13 @@ func setImages(p *plugins.Plugin, assetPath *assetpath.Service, info assetpath.P
}
return nil
}
func setTranslations(p *plugins.Plugin, assetPath *assetpath.Service, info assetpath.PluginInfo) error {
translations, err := assetPath.GetTranslations(info)
if err != nil {
return fmt.Errorf("set translations: %w", err)
}
p.Translations = translations
return nil
}
@@ -22,8 +22,8 @@ type DefaultConstructor struct {
}
// DefaultConstructFunc is the default ConstructFunc used for the Construct step of the Bootstrap stage.
func DefaultConstructFunc(signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) ConstructFunc {
return NewDefaultConstructor(signatureCalculator, assetPath).Construct
func DefaultConstructFunc(cfg *config.PluginManagementCfg, signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) ConstructFunc {
return NewDefaultConstructor(cfg, signatureCalculator, assetPath).Construct
}
// DefaultDecorateFuncs are the default DecorateFuncs used for the Decorate step of the Bootstrap stage.
@@ -37,9 +37,9 @@ func DefaultDecorateFuncs(cfg *config.PluginManagementCfg) []DecorateFunc {
}
// NewDefaultConstructor returns a new DefaultConstructor.
func NewDefaultConstructor(signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) *DefaultConstructor {
func NewDefaultConstructor(cfg *config.PluginManagementCfg, signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) *DefaultConstructor {
return &DefaultConstructor{
pluginFactoryFunc: NewDefaultPluginFactory(assetPath).createPlugin,
pluginFactoryFunc: NewDefaultPluginFactory(&cfg.Features, assetPath).createPlugin,
signatureCalculator: signatureCalculator,
log: log.New("plugins.construct"),
}
+34 -31
View File
@@ -267,14 +267,15 @@ type Signature struct {
type PluginMetaDTO struct {
JSONData
Signature SignatureStatus `json:"signature"`
Module string `json:"module"`
ModuleHash string `json:"moduleHash,omitempty"`
BaseURL string `json:"baseUrl"`
Angular AngularMeta `json:"angular"`
MultiValueFilterOperators bool `json:"multiValueFilterOperators"`
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
Extensions Extensions `json:"extensions"`
Signature SignatureStatus `json:"signature"`
Module string `json:"module"`
ModuleHash string `json:"moduleHash,omitempty"`
BaseURL string `json:"baseUrl"`
Angular AngularMeta `json:"angular"`
MultiValueFilterOperators bool `json:"multiValueFilterOperators"`
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
Extensions Extensions `json:"extensions"`
Translations map[string]string `json:"translations,omitempty"`
}
type DataSourceDTO struct {
@@ -310,32 +311,34 @@ type DataSourceDTO struct {
}
type PanelDTO struct {
ID string `json:"id"`
Name string `json:"name"`
AliasIDs []string `json:"aliasIds,omitempty"`
Info Info `json:"info"`
HideFromList bool `json:"hideFromList"`
Sort int `json:"sort"`
SkipDataQuery bool `json:"skipDataQuery"`
ReleaseState string `json:"state"`
BaseURL string `json:"baseUrl"`
Signature string `json:"signature"`
Module string `json:"module"`
Angular AngularMeta `json:"angular"`
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
ModuleHash string `json:"moduleHash,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
AliasIDs []string `json:"aliasIds,omitempty"`
Info Info `json:"info"`
HideFromList bool `json:"hideFromList"`
Sort int `json:"sort"`
SkipDataQuery bool `json:"skipDataQuery"`
ReleaseState string `json:"state"`
BaseURL string `json:"baseUrl"`
Signature string `json:"signature"`
Module string `json:"module"`
Angular AngularMeta `json:"angular"`
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
ModuleHash string `json:"moduleHash,omitempty"`
Translations map[string]string `json:"translations,omitempty"`
}
type AppDTO struct {
ID string `json:"id"`
Path string `json:"path"`
Version string `json:"version"`
Preload bool `json:"preload"`
Angular AngularMeta `json:"angular"`
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
Extensions Extensions `json:"extensions"`
Dependencies Dependencies `json:"dependencies"`
ModuleHash string `json:"moduleHash,omitempty"`
ID string `json:"id"`
Path string `json:"path"`
Version string `json:"version"`
Preload bool `json:"preload"`
Angular AngularMeta `json:"angular"`
LoadingStrategy LoadingStrategy `json:"loadingStrategy"`
Extensions Extensions `json:"extensions"`
Dependencies Dependencies `json:"dependencies"`
ModuleHash string `json:"moduleHash,omitempty"`
Translations map[string]string `json:"translations,omitempty"`
}
const (
+5
View File
@@ -64,6 +64,8 @@ type Plugin struct {
SkipHostEnvVars bool
mu sync.Mutex
Translations map[string]string
}
var (
@@ -129,6 +131,9 @@ type JSONData struct {
// App Service Auth Registration
IAM *auth.IAM `json:"iam,omitempty"`
// List of languages supported by the plugin
Languages []string `json:"languages,omitempty"`
}
func ReadPluginJSON(reader io.Reader) (JSONData, error) {
+102
View File
@@ -403,6 +403,108 @@ func Test_ReadPluginJSON(t *testing.T) {
},
},
},
{
name: "can read languages in a datasource plugin",
pluginJSON: func(t *testing.T) io.ReadCloser {
pJSON := `{
"id": "myorg-languages-datasource",
"name": "Languages Datasource",
"type": "datasource",
"languages": ["en-US", "pt-BR"]
}`
return io.NopCloser(strings.NewReader(pJSON))
},
expected: JSONData{
ID: "myorg-languages-datasource",
Name: "Languages Datasource",
Type: TypeDataSource,
Languages: []string{"en-US", "pt-BR"},
Extensions: Extensions{
AddedLinks: []AddedLink{},
AddedComponents: []AddedComponent{},
AddedFunctions: []AddedFunction{},
ExposedComponents: []ExposedComponent{},
ExtensionPoints: []ExtensionPoint{},
},
Dependencies: Dependencies{
GrafanaVersion: "*",
Plugins: []Dependency{},
Extensions: ExtensionsDependencies{
ExposedComponents: []string{},
},
},
},
},
{
name: "can read languages in a panel plugin",
pluginJSON: func(t *testing.T) io.ReadCloser {
pJSON := `{
"id": "myorg-languages-panel",
"name": "Languages Panel",
"type": "panel",
"languages": ["en-US", "pt-BR"]
}`
return io.NopCloser(strings.NewReader(pJSON))
},
expected: JSONData{
ID: "myorg-languages-panel",
Name: "Languages Panel",
Type: TypePanel,
Languages: []string{"en-US", "pt-BR"},
Extensions: Extensions{
AddedLinks: []AddedLink{},
AddedComponents: []AddedComponent{},
AddedFunctions: []AddedFunction{},
ExposedComponents: []ExposedComponent{},
ExtensionPoints: []ExtensionPoint{},
},
Dependencies: Dependencies{
GrafanaVersion: "*",
Plugins: []Dependency{},
Extensions: ExtensionsDependencies{
ExposedComponents: []string{},
},
},
},
},
{
name: "can read languages in an app plugin",
pluginJSON: func(t *testing.T) io.ReadCloser {
pJSON := `{
"id": "myorg-languages-app",
"name": "Languages App",
"type": "app",
"languages": ["en-US", "pt-BR"]
}`
return io.NopCloser(strings.NewReader(pJSON))
},
expected: JSONData{
ID: "myorg-languages-app",
Name: "Languages App",
Type: TypeApp,
Languages: []string{"en-US", "pt-BR"},
Extensions: Extensions{
AddedLinks: []AddedLink{},
AddedComponents: []AddedComponent{},
AddedFunctions: []AddedFunction{},
ExposedComponents: []ExposedComponent{},
ExtensionPoints: []ExtensionPoint{},
},
Dependencies: Dependencies{
GrafanaVersion: "*",
Plugins: []Dependency{},
Extensions: ExtensionsDependencies{
ExposedComponents: []string{},
},
},
},
},
}
for _, tt := range tests {
@@ -1977,7 +1977,7 @@ func TestGetDashboardUIDByID(t *testing.T) {
func TestUnstructuredToLegacyDashboard(t *testing.T) {
k8sCliMock := new(client.MockK8sHandler)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{"user:useruid": &user.User{ID: 10, UID: "useruid"}}, nil)
k8sCliMock.On("GetUsersFromMeta", mock.Anything, mock.Anything).Return(map[string]*user.User{"user:useruid": {ID: 10, UID: "useruid"}}, nil)
dr := &DashboardServiceImpl{
k8sclient: k8sCliMock,
}
@@ -73,7 +73,7 @@ func TestDashboardVersionService(t *testing.T) {
obj, err := utils.MetaAccessor(dash)
require.NoError(t, err)
obj.SetUpdatedTimestamp(&updatedTimestamp)
mockCli.On("GetUsersFromMeta", mock.Anything, []string{"user:1", ""}).Return(map[string]*user.User{"user:1": &user.User{ID: 1}}, nil)
mockCli.On("GetUsersFromMeta", mock.Anything, []string{"user:1", ""}).Return(map[string]*user.User{"user:1": {ID: 1}}, nil)
mockCli.On("List", mock.Anything, int64(1), mock.Anything).Return(&unstructured.UnstructuredList{
Items: []unstructured.Unstructured{*dash}}, nil).Once()
res, err := dashboardVersionService.Get(context.Background(), &dashver.GetDashboardVersionQuery{
+7
View File
@@ -1818,6 +1818,13 @@ var (
HideFromDocs: true,
Expression: "true", // enabled by default
},
{
Name: "localizationForPlugins",
Description: "Enables localization for plugins",
Stage: FeatureStageExperimental,
Owner: grafanaPluginsPlatformSquad,
FrontendOnly: false,
},
}
)
+1
View File
@@ -239,3 +239,4 @@ alertingMigrationUI,experimental,@grafana/alerting-squad,false,false,true
unifiedStorageHistoryPruner,experimental,@grafana/search-and-storage,false,false,false
unifiedStorageGrpcConnectionPool,experimental,@grafana/search-and-storage,false,false,false
alertingRuleRecoverDeleted,GA,@grafana/alerting-squad,false,false,true
localizationForPlugins,experimental,@grafana/plugins-platform-backend,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
239 unifiedStorageHistoryPruner experimental @grafana/search-and-storage false false false
240 unifiedStorageGrpcConnectionPool experimental @grafana/search-and-storage false false false
241 alertingRuleRecoverDeleted GA @grafana/alerting-squad false false true
242 localizationForPlugins experimental @grafana/plugins-platform-backend false false false
+4
View File
@@ -966,4 +966,8 @@ const (
// FlagAlertingRuleRecoverDeleted
// Enables the UI functionality to recover and view deleted alert rules
FlagAlertingRuleRecoverDeleted = "alertingRuleRecoverDeleted"
// FlagLocalizationForPlugins
// Enables localization for plugins
FlagLocalizationForPlugins = "localizationForPlugins"
)
+12
View File
@@ -2469,6 +2469,18 @@
"frontend": true
}
},
{
"metadata": {
"name": "localizationForPlugins",
"resourceVersion": "1742561476144",
"creationTimestamp": "2025-03-21T12:51:16Z"
},
"spec": {
"description": "Enables localization for plugins",
"stage": "experimental",
"codeowner": "@grafana/plugins-platform-backend"
}
},
{
"metadata": {
"name": "logQLScope",
@@ -44,7 +44,7 @@ func ProvideDiscoveryStage(cfg *config.PluginManagementCfg, pf finder.Finder, pr
func ProvideBootstrapStage(cfg *config.PluginManagementCfg, sc plugins.SignatureCalculator, a *assetpath.Service) *bootstrap.Bootstrap {
return bootstrap.New(cfg, bootstrap.Opts{
ConstructFunc: bootstrap.DefaultConstructFunc(sc, a),
ConstructFunc: bootstrap.DefaultConstructFunc(cfg, sc, a),
DecorateFuncs: bootstrap.DefaultDecorateFuncs(cfg),
})
}
@@ -34,6 +34,7 @@ func ProvidePluginManagementConfig(cfg *setting.Cfg, settingProvider setting.Pro
SkipHostEnvVarsEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSkipHostEnvVars),
SriChecksEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsSriChecks),
PluginsCDNSyncLoaderEnabled: features.IsEnabledGlobally(featuremgmt.FlagPluginsCDNSyncLoader),
LocalizationForPlugins: features.IsEnabledGlobally(featuremgmt.FlagLocalizationForPlugins),
},
cfg.AngularSupportEnabled,
cfg.GrafanaComAPIURL,
@@ -35,6 +35,8 @@ type Plugin struct {
Angular plugins.AngularMeta
ExternalService *auth.ExternalService
Translations map[string]string
}
func (p Plugin) SupportsStreaming() bool {
@@ -76,6 +78,7 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin {
BaseURL: p.BaseURL,
ExternalService: p.ExternalService,
Angular: p.Angular,
Translations: p.Translations,
}
if p.Parent != nil {
@@ -0,0 +1,22 @@
package pluginstore
import (
"testing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/stretchr/testify/require"
)
func TestToGrafanaDTO(t *testing.T) {
plugin := &plugins.Plugin{
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",
},
}
t.Run("Translations", func(t *testing.T) {
dto := ToGrafanaDTO(plugin)
require.Equal(t, plugin.Translations, dto.Translations)
})
}