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
@@ -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"),
}