fix default language fallback (#7479)

* fix default language fallback

* Update services/userlog/pkg/config/config.go

Co-authored-by: Martin <github@diemattels.at>

* Update services/notifications/pkg/config/config.go

Co-authored-by: Martin <github@diemattels.at>

* readme updated. local env vars removed

* Update changelog/unreleased/fix-default-mail-language-fallback.md

Co-authored-by: Martin <github@diemattels.at>

* update readme's and envvar texts

* fix changelog text

---------

Co-authored-by: Roman Perekhod <rperekhod@owncloud.com>
Co-authored-by: Martin <github@diemattels.at>
This commit is contained in:
Roman Perekhod
2023-10-17 09:56:48 +02:00
committed by GitHub
parent 9e81f02a25
commit 04a5ee283e
18 changed files with 80 additions and 33 deletions

View File

@@ -0,0 +1,6 @@
Bugfix: Fix default language fallback
Add the default language for the webui,
the settings, userlog and notification service.
https://github.com/owncloud/ocis/issues/7465

View File

@@ -66,3 +66,7 @@ Important: For the time being, the embedded ownCloud Web frontend only supports
* If a requested language code is not available, the service tries to fall back to the base language if available. For example, if the requested language-code `de_DE` is not available, the service tries to fall back to translations in the `de` folder.
* If the base language `de` is also not available, the service falls back to the system's default English (`en`),
which is the source of the texts provided by the code.
## Default Language
The default language can be defined via the `OCIS_DEFAULT_LANGUAGE` environment variable. See the `settings` service for a detailed description.

View File

@@ -116,7 +116,7 @@ func Server(cfg *config.Config) *cli.Command {
logger.Fatal().Err(err).Str("addr", cfg.Notifications.RevaGateway).Msg("could not get reva gateway selector")
}
valueService := settingssvc.NewValueService("com.owncloud.api.settings", grpcClient)
svc := service.NewEventsNotifier(evts, channel, logger, gatewaySelector, valueService, cfg.ServiceAccount.ServiceAccountID, cfg.ServiceAccount.ServiceAccountSecret, cfg.Notifications.EmailTemplatePath, cfg.WebUIURL)
svc := service.NewEventsNotifier(evts, channel, logger, gatewaySelector, valueService, cfg.ServiceAccount.ServiceAccountID, cfg.ServiceAccount.ServiceAccountSecret, cfg.Notifications.EmailTemplatePath, cfg.Notifications.DefaultLanguage, cfg.WebUIURL)
gr.Add(svc.Run, func(error) {
cancel()

View File

@@ -1,3 +1,4 @@
// Package config provides the service configuration.
package config
import (
@@ -31,6 +32,7 @@ type Notifications struct {
Events Events `yaml:"events"`
EmailTemplatePath string `yaml:"email_template_path" env:"OCIS_EMAIL_TEMPLATE_PATH;NOTIFICATIONS_EMAIL_TEMPLATE_PATH" desc:"Path to Email notification templates overriding embedded ones."`
TranslationPath string `yaml:"translation_path" env:"OCIS_TRANSLATION_PATH,NOTIFICATIONS_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. Note that file and folder naming rules apply, see the documentation for more details."`
DefaultLanguage string `yaml:"default_language" env:"OCIS_DEFAULT_LANGUAGE" desc:"The default language used by services and the WebUI. If not defined, English will be used as default. See the documentation for more details."`
RevaGateway string `yaml:"reva_gateway" env:"OCIS_REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata"`
GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"`
}

View File

@@ -9,9 +9,9 @@ import (
)
// NewTextTemplate replace the body message template placeholders with the translated template
func NewTextTemplate(mt MessageTemplate, locale string, translationPath string, vars map[string]string) (MessageTemplate, error) {
func NewTextTemplate(mt MessageTemplate, locale, defaultLocale string, translationPath string, vars map[string]string) (MessageTemplate, error) {
var err error
t := l10n.NewTranslator(locale, translationPath)
t := l10n.NewTranslator(locale, defaultLocale, translationPath)
mt.Subject, err = composeMessage(t.Translate(mt.Subject), vars)
if err != nil {
return mt, err
@@ -32,9 +32,9 @@ func NewTextTemplate(mt MessageTemplate, locale string, translationPath string,
}
// NewHTMLTemplate replace the body message template placeholders with the translated template
func NewHTMLTemplate(mt MessageTemplate, locale string, translationPath string, vars map[string]string) (MessageTemplate, error) {
func NewHTMLTemplate(mt MessageTemplate, locale, defaultLocale string, translationPath string, vars map[string]string) (MessageTemplate, error) {
var err error
t := l10n.NewTranslator(locale, translationPath)
t := l10n.NewTranslator(locale, defaultLocale, translationPath)
mt.Subject, err = composeMessage(t.Translate(mt.Subject), vars)
if err != nil {
return mt, err

View File

@@ -26,8 +26,8 @@ var (
)
// RenderEmailTemplate renders the email template for a new share
func RenderEmailTemplate(mt MessageTemplate, locale string, emailTemplatePath string, translationPath string, vars map[string]string) (*channels.Message, error) {
textMt, err := NewTextTemplate(mt, locale, translationPath, vars)
func RenderEmailTemplate(mt MessageTemplate, locale, defaultLocale string, emailTemplatePath string, translationPath string, vars map[string]string) (*channels.Message, error) {
textMt, err := NewTextTemplate(mt, locale, defaultLocale, translationPath, vars)
if err != nil {
return nil, err
}
@@ -39,7 +39,7 @@ func RenderEmailTemplate(mt MessageTemplate, locale string, emailTemplatePath st
if err != nil {
return nil, err
}
htmlMt, err := NewHTMLTemplate(mt, locale, translationPath, escapeStringMap(vars))
htmlMt, err := NewHTMLTemplate(mt, locale, defaultLocale, translationPath, escapeStringMap(vars))
if err != nil {
return nil, err
}

View File

@@ -22,11 +22,19 @@ type Translator interface {
}
type translator struct {
l *gotext.Locale
locale *gotext.Locale
}
// NewTranslator Create Translator with library path and language code and load default domain
func NewTranslator(local string, path string) Translator {
func NewTranslator(locale, defaultLocale string, path string) Translator {
l := newLocate(locale, path)
if locale != "en" && len(l.GetTranslations()) == 0 {
l = newLocate(defaultLocale, path)
}
return &translator{locale: l}
}
func newLocate(local string, path string) *gotext.Locale {
var l *gotext.Locale
if path == "" {
filesystem, _ := fs.Sub(_translationFS, "locale")
@@ -35,9 +43,9 @@ func NewTranslator(local string, path string) Translator {
l = gotext.NewLocale(path, local)
}
l.AddDomain(_domain) // make domain configurable only if needed
return &translator{l: l}
return l
}
func (t *translator) Translate(str string) string {
return t.l.Get(str)
return t.locale.Get(str)
}

View File

@@ -42,7 +42,7 @@ func NewEventsNotifier(
logger log.Logger,
gatewaySelector pool.Selectable[gateway.GatewayAPIClient],
valueService settingssvc.ValueService,
serviceAccountID, serviceAccountSecret, emailTemplatePath, ocisURL string) Service {
serviceAccountID, serviceAccountSecret, emailTemplatePath, defaultLanguage, ocisURL string) Service {
return eventsNotifier{
logger: logger,
@@ -54,6 +54,7 @@ func NewEventsNotifier(
serviceAccountID: serviceAccountID,
serviceAccountSecret: serviceAccountSecret,
emailTemplatePath: emailTemplatePath,
defaultLanguage: defaultLanguage,
ocisURL: ocisURL,
}
}
@@ -67,6 +68,7 @@ type eventsNotifier struct {
valueService settingssvc.ValueService
emailTemplatePath string
translationPath string
defaultLanguage string
ocisURL string
serviceAccountID string
serviceAccountSecret string
@@ -109,7 +111,7 @@ func (s eventsNotifier) render(ctx context.Context, template email.MessageTempla
locale := s.getUserLang(ctx, usr.GetId())
fields[granteeFieldName] = usr.GetDisplayName()
rendered, err := email.RenderEmailTemplate(template, locale, s.emailTemplatePath, s.translationPath, fields)
rendered, err := email.RenderEmailTemplate(template, locale, s.defaultLanguage, s.emailTemplatePath, s.translationPath, fields)
if err != nil {
return nil, err
}

View File

@@ -77,7 +77,7 @@ var _ = Describe("Notifications", func() {
cfg := defaults.FullDefaultConfig()
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
ch := make(chan events.Event)
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gatewaySelector, vs, "", "", "", "")
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gatewaySelector, vs, "", "", "", "", "")
go evts.Run()
ch <- ev
@@ -275,7 +275,7 @@ var _ = Describe("Notifications X-Site Scripting", func() {
cfg := defaults.FullDefaultConfig()
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
ch := make(chan events.Event)
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gatewaySelector, vs, "", "", "", "")
evts := service.NewEventsNotifier(ch, tc, log.NewLogger(), gatewaySelector, vs, "", "", "", "", "")
go evts.Run()
ch <- ev

View File

@@ -76,6 +76,16 @@ The settings service needs to know the ID's of service accounts but it doesn't n
## Default Language
The default language can be defined via `SETTINGS_DEFAULT_LANGUAGE` environment variable. If this variable is not defined, English will be used as default. The value has the ISO 639-1 format ("de", "en", etc.) and is limited by the list supported languages. This setting can be used to set the default language for invitation emails.
The default language can be defined via the `OCIS_DEFAULT_LANGUAGE` environment variable. If this variable is not defined, English will be used as default. The value has the ISO 639-1 format ("de", "en", etc.) and is limited by the list supported languages. This setting can be used to set the default language for notification and invitation emails.
Important developer note: the list of supported languages is at the moment not easy defineable, as it is the minimum intersection of languages shown in the WebUI and languages defined in the ocis code for the use of notifications. Even more, not all languages where there are translations available on transifex, are available in the WebUI respectively for ocis notifications, and the translation rate for existing languages is partially not that high. You will see therefore quite often English default strings though a supported language may exist and was selected.
Important developer note: the list of supported languages is at the moment not easy defineable, as it is the minimum intersection of languages shown in the WebUI and languages defined in the ocis code for the use of notifications and userlog. Even more, not all languages where there are translations available on transifex, are available in the WebUI respectively for ocis notifications, and the translation rate for existing languages is partially not that high. You will see therefore quite often English default strings though a supported language may exist and was selected.
The `OCIS_DEFAULT_LANGUAGE` setting impacts the `notification` and `userlog` services and the WebUI. Note that translations must exist for all named components to be presented correctly.
* If `OCIS_DEFAULT_LANGUAGE` **is not set**, the expected behavior is:
* The `notification` and `userlog` services and the WebUI use English by default until a user sets another language in the WebUI via _Account -> Language_.
* If a user sets another language in the WebUI in _Account -> Language_, then the `notification` and `userlog` services and WebUI use the language defined by the user. If no translation is found, it falls back to English.
* If `OCIS_DEFAULT_LANGUAGE` **is set**, the expected behavior is:
* The `notification` and `userlog` services and the WebUI use `OCIS_DEFAULT_LANGUAGE` by default until a user sets another language in the WebUI via _Account -> Language_.
* If a user sets another language in the WebUI in _Account -> Language_, the `notification` and `userlog` services and WebUI use the language defined by the user. If no translation is found, it falls back to `OCIS_DEFAULT_LANGUAGE` and then to English.

View File

@@ -39,7 +39,7 @@ type Config struct {
ServiceAccountIDAdmin string `yaml:"service_account_id_admin" env:"OCIS_SERVICE_ACCOUNT_ID;SETTINGS_SERVICE_ACCOUNT_ID_ADMIN" desc:"The ID of the service account having the admin role. See the 'auth-service' service description for more details."`
DefaultLanguage string `yaml:"default_language" env:"SETTINGS_DEFAULT_LANGUAGE" desc:"The default language. If not defined, English will be used as default. See the documentation for more details."`
DefaultLanguage string `yaml:"default_language" env:"OCIS_DEFAULT_LANGUAGE" desc:"The default language used by services and the WebUI. If not defined, English will be used as default. See the documentation for more details."`
Context context.Context `yaml:"-"`
}

View File

@@ -76,3 +76,7 @@ Important: For the time being, the embedded ownCloud Web frontend only supports
* If a requested language code is not available, the service tries to fall back to the base language if available. For example, if the requested language-code `de_DE` is not available, the service tries to fall back to translations in the `de` folder.
* If the base language `de` is also not available, the service falls back to the system's default English (`en`),
which is the source of the texts provided by the code.
## Default Language
The default language can be defined via the `OCIS_DEFAULT_LANGUAGE` environment variable. See the `settings` service for a detailed description.

View File

@@ -24,6 +24,7 @@ type Config struct {
RevaGateway string `yaml:"reva_gateway" env:"OCIS_REVA_GATEWAY" desc:"CS3 gateway used to look up user metadata"`
TranslationPath string `yaml:"translation_path" env:"OCIS_TRANSLATION_PATH;USERLOG_TRANSLATION_PATH" desc:"(optional) Set this to a path with custom translations to overwrite the builtin translations. Note that file and folder naming rules apply, see the documentation for more details."`
DefaultLanguage string `yaml:"default_language" env:"OCIS_DEFAULT_LANGUAGE" desc:"The default language used by services and the WebUI. If not defined, English will be used as default. See the documentation for more details."`
Events Events `yaml:"events"`
Persistence Persistence `yaml:"persistence"`

View File

@@ -55,6 +55,7 @@ type Converter struct {
gwc gateway.GatewayAPIClient
serviceName string
translationPath string
defaultLanguage string
serviceAccountContext context.Context
// cached within one request not to query other service too much
@@ -64,12 +65,13 @@ type Converter struct {
}
// NewConverter returns a new Converter
func NewConverter(ctx context.Context, loc string, gwc gateway.GatewayAPIClient, name string, translationPath string) *Converter {
func NewConverter(ctx context.Context, loc string, gwc gateway.GatewayAPIClient, name, translationPath, defaultLanguage string) *Converter {
return &Converter{
locale: loc,
gwc: gwc,
serviceName: name,
translationPath: translationPath,
defaultLanguage: defaultLanguage,
serviceAccountContext: ctx,
spaces: make(map[string]*storageprovider.StorageSpace),
users: make(map[string]*user.User),
@@ -137,7 +139,7 @@ func (c *Converter) spaceDeletedMessage(eventid string, executant *user.UserId,
return OC10Notification{}, err
}
subj, subjraw, msg, msgraw, err := composeMessage(SpaceDeleted, c.locale, c.translationPath, map[string]interface{}{
subj, subjraw, msg, msgraw, err := composeMessage(SpaceDeleted, c.locale, c.defaultLanguage, c.translationPath, map[string]interface{}{
"username": usr.GetDisplayName(),
"spacename": spacename,
})
@@ -173,7 +175,7 @@ func (c *Converter) spaceMessage(eventid string, nt NotificationTemplate, execut
return OC10Notification{}, err
}
subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.translationPath, map[string]interface{}{
subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.defaultLanguage, c.translationPath, map[string]interface{}{
"username": usr.GetDisplayName(),
"spacename": space.GetName(),
})
@@ -207,7 +209,7 @@ func (c *Converter) shareMessage(eventid string, nt NotificationTemplate, execut
return OC10Notification{}, err
}
subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.translationPath, map[string]interface{}{
subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.defaultLanguage, c.translationPath, map[string]interface{}{
"username": usr.GetDisplayName(),
"resourcename": info.GetName(),
})
@@ -231,7 +233,7 @@ func (c *Converter) shareMessage(eventid string, nt NotificationTemplate, execut
}
func (c *Converter) virusMessage(eventid string, nt NotificationTemplate, executant *user.User, rid *storageprovider.ResourceId, filename string, virus string, ts time.Time) (OC10Notification, error) {
subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.translationPath, map[string]interface{}{
subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.defaultLanguage, c.translationPath, map[string]interface{}{
"resourcename": filename,
"virusdescription": virus,
})
@@ -265,7 +267,7 @@ func (c *Converter) virusMessage(eventid string, nt NotificationTemplate, execut
}
func (c *Converter) policiesMessage(eventid string, nt NotificationTemplate, executant *user.User, filename string, ts time.Time) (OC10Notification, error) {
subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.translationPath, map[string]interface{}{
subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.defaultLanguage, c.translationPath, map[string]interface{}{
"resourcename": filename,
})
if err != nil {
@@ -293,7 +295,7 @@ func (c *Converter) policiesMessage(eventid string, nt NotificationTemplate, exe
}
func (c *Converter) deprovisionMessage(nt NotificationTemplate, deproDate string) (OC10Notification, error) {
subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.translationPath, map[string]interface{}{
subj, subjraw, msg, msgraw, err := composeMessage(nt, c.locale, c.defaultLanguage, c.translationPath, map[string]interface{}{
"date": deproDate,
})
if err != nil {
@@ -347,8 +349,8 @@ func (c *Converter) getUser(ctx context.Context, userID *user.UserId) (*user.Use
return usr, err
}
func composeMessage(nt NotificationTemplate, locale string, path string, vars map[string]interface{}) (string, string, string, string, error) {
subjectraw, messageraw := loadTemplates(nt, locale, path)
func composeMessage(nt NotificationTemplate, locale, defaultLocale, path string, vars map[string]interface{}) (string, string, string, string, error) {
subjectraw, messageraw := loadTemplates(nt, locale, defaultLocale, path)
subject, err := executeTemplate(subjectraw, vars)
if err != nil {
@@ -359,7 +361,7 @@ func composeMessage(nt NotificationTemplate, locale string, path string, vars ma
return subject, subjectraw, message, messageraw, err
}
func loadTemplates(nt NotificationTemplate, locale string, path string) (string, string) {
func newLocate(locale string, path string) *gotext.Locale {
// Create Locale with library path and language code and load default domain
var l *gotext.Locale
if path == "" {
@@ -369,6 +371,14 @@ func loadTemplates(nt NotificationTemplate, locale string, path string) (string,
l = gotext.NewLocale(path, locale)
}
l.AddDomain(_domain) // make domain configurable only if needed
return l
}
func loadTemplates(nt NotificationTemplate, locale, defaultLocale, path string) (string, string) {
l := newLocate(locale, path)
if locale != "en" && len(l.GetTranslations()) == 0 {
l = newLocate(defaultLocale, path)
}
return l.Get(nt.Subject), l.Get(nt.Message)
}

View File

@@ -59,7 +59,7 @@ func (ul *UserlogService) HandleGetEvents(w http.ResponseWriter, r *http.Request
return
}
conv := NewConverter(ctx, r.Header.Get(HeaderAcceptLanguage), gwc, ul.cfg.Service.Name, ul.cfg.TranslationPath)
conv := NewConverter(ctx, r.Header.Get(HeaderAcceptLanguage), gwc, ul.cfg.Service.Name, ul.cfg.TranslationPath, ul.cfg.DefaultLanguage)
var outdatedEvents []string
resp := GetEventResponseOC10{}

View File

@@ -337,7 +337,7 @@ func (ul *UserlogService) addEventToUser(ctx context.Context, userid string, eve
}
func (ul *UserlogService) sendSSE(ctx context.Context, userid string, event events.Event, gwc gateway.GatewayAPIClient) error {
ev, err := NewConverter(ctx, ul.getUserLocale(userid), gwc, ul.cfg.Service.Name, ul.cfg.TranslationPath).ConvertEvent(event.ID, event.Event)
ev, err := NewConverter(ctx, ul.getUserLocale(userid), gwc, ul.cfg.Service.Name, ul.cfg.TranslationPath, ul.cfg.DefaultLanguage).ConvertEvent(event.ID, event.Event)
if err != nil {
return err
}

View File

@@ -183,7 +183,7 @@ Feature: Email notification
@env-config
Scenario: group members get an email notification in default language when someone shares a file with the group
Given the config "SETTINGS_DEFAULT_LANGUAGE" has been set to "de"
Given the config "OCIS_DEFAULT_LANGUAGE" has been set to "de"
And user "Carol" has been created with default attributes and without skeleton files
And group "group1" has been created
And user "Brian" has been added to group "group1"

View File

@@ -270,7 +270,7 @@ Feature: Notification
@env-config
Scenario: get a notification about a file share in default languages
Given the config "SETTINGS_DEFAULT_LANGUAGE" has been set to "de"
Given the config "OCIS_DEFAULT_LANGUAGE" has been set to "de"
And user "Alice" has shared entry "textfile1.txt" with user "Brian" with permissions "17"
When user "Brian" lists all notifications
Then the HTTP status code should be "200"