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
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user