From 70a118c0fd45f8ab2510c80a0110f24be21f8785 Mon Sep 17 00:00:00 2001 From: Saturnino Abril Date: Wed, 23 May 2018 20:36:20 +0800 Subject: remove license check when enforcing password requirements (#8840) Signed-off-by: Saturnino Abril --- app/authentication.go | 5 +---- model/license.go | 6 ------ model/license_test.go | 5 ----- utils/config.go | 13 +++++-------- utils/license.go | 1 - 5 files changed, 6 insertions(+), 24 deletions(-) diff --git a/app/authentication.go b/app/authentication.go index 5c91f8038..087a9b230 100644 --- a/app/authentication.go +++ b/app/authentication.go @@ -36,10 +36,7 @@ func (tl TokenLocation) String() string { } func (a *App) IsPasswordValid(password string) *model.AppError { - if license := a.License(); license != nil && *license.Features.PasswordRequirements { - return utils.IsPasswordValidWithSettings(password, &a.Config().PasswordSettings) - } - return utils.IsPasswordValid(password) + return utils.IsPasswordValidWithSettings(password, &a.Config().PasswordSettings) } func (a *App) CheckPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError { diff --git a/model/license.go b/model/license.go index 942a18d55..b69c13c54 100644 --- a/model/license.go +++ b/model/license.go @@ -49,7 +49,6 @@ type Features struct { CustomBrand *bool `json:"custom_brand"` MHPNS *bool `json:"mhpns"` SAML *bool `json:"saml"` - PasswordRequirements *bool `json:"password_requirements"` Elasticsearch *bool `json:"elastic_search"` Announcement *bool `json:"announcement"` ThemeManagement *bool `json:"theme_management"` @@ -73,7 +72,6 @@ func (f *Features) ToMap() map[string]interface{} { "custom_brand": *f.CustomBrand, "mhpns": *f.MHPNS, "saml": *f.SAML, - "password": *f.PasswordRequirements, "elastic_search": *f.Elasticsearch, "email_notification_contents": *f.EmailNotificationContents, "data_retention": *f.DataRetention, @@ -131,10 +129,6 @@ func (f *Features) SetDefaults() { f.SAML = NewBool(*f.FutureFeatures) } - if f.PasswordRequirements == nil { - f.PasswordRequirements = NewBool(*f.FutureFeatures) - } - if f.Elasticsearch == nil { f.Elasticsearch = NewBool(*f.FutureFeatures) } diff --git a/model/license_test.go b/model/license_test.go index f953d47b3..4b40c4101 100644 --- a/model/license_test.go +++ b/model/license_test.go @@ -24,7 +24,6 @@ func TestLicenseFeaturesToMap(t *testing.T) { CheckTrue(t, m["custom_brand"].(bool)) CheckTrue(t, m["mhpns"].(bool)) CheckTrue(t, m["saml"].(bool)) - CheckTrue(t, m["password"].(bool)) CheckTrue(t, m["elastic_search"].(bool)) CheckTrue(t, m["email_notification_contents"].(bool)) CheckTrue(t, m["data_retention"].(bool)) @@ -46,7 +45,6 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) { CheckTrue(t, *f.CustomBrand) CheckTrue(t, *f.MHPNS) CheckTrue(t, *f.SAML) - CheckTrue(t, *f.PasswordRequirements) CheckTrue(t, *f.Elasticsearch) CheckTrue(t, *f.EmailNotificationContents) CheckTrue(t, *f.DataRetention) @@ -67,7 +65,6 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) { *f.CustomBrand = true *f.MHPNS = true *f.SAML = true - *f.PasswordRequirements = true *f.Elasticsearch = true *f.DataRetention = true *f.EmailNotificationContents = true @@ -85,7 +82,6 @@ func TestLicenseFeaturesSetDefaults(t *testing.T) { CheckTrue(t, *f.CustomBrand) CheckTrue(t, *f.MHPNS) CheckTrue(t, *f.SAML) - CheckTrue(t, *f.PasswordRequirements) CheckTrue(t, *f.Elasticsearch) CheckTrue(t, *f.EmailNotificationContents) CheckTrue(t, *f.DataRetention) @@ -168,7 +164,6 @@ func TestLicenseToFromJson(t *testing.T) { CheckBool(t, *f1.CustomBrand, *f.CustomBrand) CheckBool(t, *f1.MHPNS, *f.MHPNS) CheckBool(t, *f1.SAML, *f.SAML) - CheckBool(t, *f1.PasswordRequirements, *f.PasswordRequirements) CheckBool(t, *f1.Elasticsearch, *f.Elasticsearch) CheckBool(t, *f1.DataRetention, *f.DataRetention) CheckBool(t, *f1.FutureFeatures, *f.FutureFeatures) diff --git a/utils/config.go b/utils/config.go index dd782c0fc..18e25c999 100644 --- a/utils/config.go +++ b/utils/config.go @@ -602,6 +602,11 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L props["DataRetentionMessageRetentionDays"] = "0" props["DataRetentionEnableFileDeletion"] = "false" props["DataRetentionFileRetentionDays"] = "0" + props["PasswordMinimumLength"] = fmt.Sprintf("%v", *c.PasswordSettings.MinimumLength) + props["PasswordRequireLowercase"] = strconv.FormatBool(*c.PasswordSettings.Lowercase) + props["PasswordRequireUppercase"] = strconv.FormatBool(*c.PasswordSettings.Uppercase) + props["PasswordRequireNumber"] = strconv.FormatBool(*c.PasswordSettings.Number) + props["PasswordRequireSymbol"] = strconv.FormatBool(*c.PasswordSettings.Symbol) if license != nil { props["ExperimentalTownSquareIsReadOnly"] = strconv.FormatBool(*c.TeamSettings.ExperimentalTownSquareIsReadOnly) @@ -662,14 +667,6 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L props["EnableSignUpWithOffice365"] = strconv.FormatBool(c.Office365Settings.Enable) } - if *license.Features.PasswordRequirements { - props["PasswordMinimumLength"] = fmt.Sprintf("%v", *c.PasswordSettings.MinimumLength) - props["PasswordRequireLowercase"] = strconv.FormatBool(*c.PasswordSettings.Lowercase) - props["PasswordRequireUppercase"] = strconv.FormatBool(*c.PasswordSettings.Uppercase) - props["PasswordRequireNumber"] = strconv.FormatBool(*c.PasswordSettings.Number) - props["PasswordRequireSymbol"] = strconv.FormatBool(*c.PasswordSettings.Symbol) - } - if *license.Features.Announcement { props["EnableBanner"] = strconv.FormatBool(*c.AnnouncementSettings.EnableBanner) props["BannerText"] = *c.AnnouncementSettings.BannerText diff --git a/utils/license.go b/utils/license.go index aa89026ea..832d41557 100644 --- a/utils/license.go +++ b/utils/license.go @@ -139,7 +139,6 @@ func GetClientLicense(l *model.License) map[string]string { props["Compliance"] = strconv.FormatBool(*l.Features.Compliance) props["CustomBrand"] = strconv.FormatBool(*l.Features.CustomBrand) props["MHPNS"] = strconv.FormatBool(*l.Features.MHPNS) - props["PasswordRequirements"] = strconv.FormatBool(*l.Features.PasswordRequirements) props["Announcement"] = strconv.FormatBool(*l.Features.Announcement) props["Elasticsearch"] = strconv.FormatBool(*l.Features.Elasticsearch) props["DataRetention"] = strconv.FormatBool(*l.Features.DataRetention) -- cgit v1.2.3-1-g7c22 From 0a666a56560713a084847ce683b940c1aa84acc0 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 23 May 2018 10:28:19 -0400 Subject: MM-10649: soften Channels.ExtraUpdateAt deprecation (#8843) The previous complete removal of this field resulted in an incompatibility with 4.x servers that could not handle the now null column field. Instead, ensure this field is at least always set to 0, with a plan to remove it altogether in a future release. --- model/channel.go | 2 ++ store/storetest/channel_store.go | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/model/channel.go b/model/channel.go index 749b8dc94..950e910dd 100644 --- a/model/channel.go +++ b/model/channel.go @@ -44,6 +44,7 @@ type Channel struct { Purpose string `json:"purpose"` LastPostAt int64 `json:"last_post_at"` TotalMsgCount int64 `json:"total_msg_count"` + ExtraUpdateAt int64 `json:"extra_update_at"` CreatorId string `json:"creator_id"` } @@ -132,6 +133,7 @@ func (o *Channel) PreSave() { o.CreateAt = GetMillis() o.UpdateAt = o.CreateAt + o.ExtraUpdateAt = 0 } func (o *Channel) PreUpdate() { diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index ccb7b87b8..9abb62856 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -713,6 +713,9 @@ func testChannelMemberStore(t *testing.T, ss store.Store) { c1.Type = model.CHANNEL_OPEN c1 = *store.Must(ss.Channel().Save(&c1, -1)).(*model.Channel) + c1t1 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel) + assert.EqualValues(t, 0, c1t1.ExtraUpdateAt, "ExtraUpdateAt should be 0") + u1 := model.User{} u1.Email = model.NewId() u1.Nickname = model.NewId() @@ -737,6 +740,9 @@ func testChannelMemberStore(t *testing.T, ss store.Store) { o2.NotifyProps = model.GetDefaultChannelNotifyProps() store.Must(ss.Channel().SaveMember(&o2)) + c1t2 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel) + assert.EqualValues(t, 0, c1t2.ExtraUpdateAt, "ExtraUpdateAt should be 0") + count := (<-ss.Channel().GetMemberCount(o1.ChannelId, true)).Data.(int64) if count != 2 { t.Fatal("should have saved 2 members") @@ -767,6 +773,9 @@ func testChannelMemberStore(t *testing.T, ss store.Store) { t.Fatal("should have removed 1 member") } + c1t3 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel) + assert.EqualValues(t, 0, c1t3.ExtraUpdateAt, "ExtraUpdateAt should be 0") + member := (<-ss.Channel().GetMember(o1.ChannelId, o1.UserId)).Data.(*model.ChannelMember) if member.ChannelId != o1.ChannelId { t.Fatal("should have go member") @@ -775,6 +784,9 @@ func testChannelMemberStore(t *testing.T, ss store.Store) { if err := (<-ss.Channel().SaveMember(&o1)).Err; err == nil { t.Fatal("Should have been a duplicate") } + + c1t4 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel) + assert.EqualValues(t, 0, c1t4.ExtraUpdateAt, "ExtraUpdateAt should be 0") } func testChannelDeleteMemberStore(t *testing.T, ss store.Store) { @@ -785,6 +797,9 @@ func testChannelDeleteMemberStore(t *testing.T, ss store.Store) { c1.Type = model.CHANNEL_OPEN c1 = *store.Must(ss.Channel().Save(&c1, -1)).(*model.Channel) + c1t1 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel) + assert.EqualValues(t, 0, c1t1.ExtraUpdateAt, "ExtraUpdateAt should be 0") + u1 := model.User{} u1.Email = model.NewId() u1.Nickname = model.NewId() @@ -809,6 +824,9 @@ func testChannelDeleteMemberStore(t *testing.T, ss store.Store) { o2.NotifyProps = model.GetDefaultChannelNotifyProps() store.Must(ss.Channel().SaveMember(&o2)) + c1t2 := (<-ss.Channel().Get(c1.Id, false)).Data.(*model.Channel) + assert.EqualValues(t, 0, c1t2.ExtraUpdateAt, "ExtraUpdateAt should be 0") + count := (<-ss.Channel().GetMemberCount(o1.ChannelId, false)).Data.(int64) if count != 2 { t.Fatal("should have saved 2 members") -- cgit v1.2.3-1-g7c22 From 5c21bdc1783e5cd17169436e7ccfacdd1b637907 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 23 May 2018 13:17:08 -0400 Subject: allow tuning *IdleConn* for intra-cluster messages (#8799) * allow tuning *IdleConn* for inter cluster messages * default MaxIdleConnsPerHost to 128 --- config/default.json | 5 ++++- model/config.go | 31 +++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/config/default.json b/config/default.json index c80ff48de..1c4608c03 100644 --- a/config/default.json +++ b/config/default.json @@ -323,7 +323,10 @@ "UseExperimentalGossip": false, "ReadOnlyConfig": true, "GossipPort": 8074, - "StreamingPort": 8075 + "StreamingPort": 8075, + "MaxIdleConns": 100, + "MaxIdleConnsPerHost": 128, + "IdleConnTimeoutMilliseconds": 90000 }, "MetricsSettings": { "Enable": false, diff --git a/model/config.go b/model/config.go index 7a2125061..7c11860d2 100644 --- a/model/config.go +++ b/model/config.go @@ -460,14 +460,17 @@ func (s *ServiceSettings) SetDefaults() { } type ClusterSettings struct { - Enable *bool - ClusterName *string - OverrideHostname *string - UseIpAddress *bool - UseExperimentalGossip *bool - ReadOnlyConfig *bool - GossipPort *int - StreamingPort *int + Enable *bool + ClusterName *string + OverrideHostname *string + UseIpAddress *bool + UseExperimentalGossip *bool + ReadOnlyConfig *bool + GossipPort *int + StreamingPort *int + MaxIdleConns *int + MaxIdleConnsPerHost *int + IdleConnTimeoutMilliseconds *int } func (s *ClusterSettings) SetDefaults() { @@ -502,6 +505,18 @@ func (s *ClusterSettings) SetDefaults() { if s.StreamingPort == nil { s.StreamingPort = NewInt(8075) } + + if s.MaxIdleConns == nil { + s.MaxIdleConns = NewInt(100) + } + + if s.MaxIdleConnsPerHost == nil { + s.MaxIdleConnsPerHost = NewInt(128) + } + + if s.IdleConnTimeoutMilliseconds == nil { + s.IdleConnTimeoutMilliseconds = NewInt(90000) + } } type MetricsSettings struct { -- cgit v1.2.3-1-g7c22 From 847c181ec9b73e51daf39efc5c597eff2e7cdb31 Mon Sep 17 00:00:00 2001 From: Jesse Hallam Date: Wed, 23 May 2018 14:26:35 -0400 Subject: MM-8622: Improved plugin error reporting (#8737) * allow `Wait()`ing on the supervisor In the event the plugin supervisor shuts down a plugin for crashing too many times, the new `Wait()` interface allows the `ActivatePlugin` to accept a callback function to trigger when `supervisor.Wait()` returns. If the supervisor shuts down normally, this callback is invoked with a nil error, otherwise any error reported by the supervisor is passed along. * improve plugin activation/deactivation logic Avoid triggering activation of previously failed-to-start plugins just becase something in the configuration changed. Now, intelligently compare the global enable bit as well as the each individual plugin's enabled bit. * expose store to manipulate PluginStatuses * expose API to fetch plugin statuses * keep track of whether or not plugin sandboxing is supported * transition plugin statuses * restore error on plugin activation if already active * don't initialize test plugins until successfully loaded * emit websocket events when plugin statuses change * skip pruning if already initialized * MM-8622: maintain plugin statuses in memory Switch away from persisting plugin statuses to the database, and maintain in memory instead. This will be followed by a cluster interface to query the in-memory status of plugin statuses from all cluster nodes. At the same time, rename `cluster_discovery_id` on the `PluginStatus` model object to `cluster_id`. * MM-8622: aggregate plugin statuses across cluster * fetch cluster plugin statuses when emitting websocket notification * address unit test fixes after rebasing * relax (poor) racey unit test re: supervisor.Wait() * make store-mocks --- api4/plugin.go | 23 ++- app/app.go | 6 +- app/apptestlib.go | 30 ++-- app/cluster_discovery.go | 8 + app/plugin.go | 240 ++++++++++++++++++++++++--- app/plugin_test.go | 59 ++++++- einterfaces/cluster.go | 1 + i18n/en.json | 16 ++ model/client4.go | 12 ++ model/cluster_discovery.go | 2 +- model/plugin_status.go | 44 +++++ model/websocket_message.go | 77 ++++----- plugin/pluginenv/environment.go | 12 +- plugin/pluginenv/environment_test.go | 24 +-- plugin/rpcplugin/rpcplugintest/supervisor.go | 39 +++++ plugin/rpcplugin/supervisor.go | 15 +- plugin/supervisor.go | 1 + 17 files changed, 516 insertions(+), 93 deletions(-) create mode 100644 model/plugin_status.go diff --git a/api4/plugin.go b/api4/plugin.go index 37fbf12cd..ab026ab5f 100644 --- a/api4/plugin.go +++ b/api4/plugin.go @@ -23,6 +23,7 @@ func (api *API) InitPlugin() { api.BaseRoutes.Plugins.Handle("", api.ApiSessionRequired(getPlugins)).Methods("GET") api.BaseRoutes.Plugin.Handle("", api.ApiSessionRequired(removePlugin)).Methods("DELETE") + api.BaseRoutes.Plugins.Handle("/statuses", api.ApiSessionRequired(getPluginStatuses)).Methods("GET") api.BaseRoutes.Plugin.Handle("/activate", api.ApiSessionRequired(activatePlugin)).Methods("POST") api.BaseRoutes.Plugin.Handle("/deactivate", api.ApiSessionRequired(deactivatePlugin)).Methods("POST") @@ -97,6 +98,26 @@ func getPlugins(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(response.ToJson())) } +func getPluginStatuses(c *Context, w http.ResponseWriter, r *http.Request) { + if !*c.App.Config().PluginSettings.Enable { + c.Err = model.NewAppError("getPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) + return + } + + if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + response, err := c.App.GetClusterPluginStatuses() + if err != nil { + c.Err = err + return + } + + w.Write([]byte(response.ToJson())) +} + func removePlugin(c *Context, w http.ResponseWriter, r *http.Request) { c.RequirePluginId() if c.Err != nil { @@ -104,7 +125,7 @@ func removePlugin(c *Context, w http.ResponseWriter, r *http.Request) { } if !*c.App.Config().PluginSettings.Enable { - c.Err = model.NewAppError("getPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) + c.Err = model.NewAppError("removePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) return } diff --git a/app/app.go b/app/app.go index 2cdf333c1..6de75855c 100644 --- a/app/app.go +++ b/app/app.go @@ -38,8 +38,10 @@ type App struct { Log *mlog.Logger - PluginEnv *pluginenv.Environment - PluginConfigListenerId string + PluginEnv *pluginenv.Environment + PluginConfigListenerId string + IsPluginSandboxSupported bool + pluginStatuses map[string]*model.PluginStatus EmailBatching *EmailBatchingJob diff --git a/app/apptestlib.go b/app/apptestlib.go index b245ddabf..7fc78c9c9 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -336,6 +336,10 @@ func (s *mockPluginSupervisor) Start(api plugin.API) error { return s.hooks.OnActivate(api) } +func (s *mockPluginSupervisor) Wait() error { + return nil +} + func (s *mockPluginSupervisor) Stop() error { return nil } @@ -353,17 +357,6 @@ func (me *TestHelper) InstallPlugin(manifest *model.Manifest, hooks plugin.Hooks me.tempWorkspace = dir } - pluginDir := filepath.Join(me.tempWorkspace, "plugins") - webappDir := filepath.Join(me.tempWorkspace, "webapp") - me.App.InitPlugins(pluginDir, webappDir, func(bundle *model.BundleInfo) (plugin.Supervisor, error) { - if hooks, ok := me.pluginHooks[bundle.Manifest.Id]; ok { - return &mockPluginSupervisor{hooks}, nil - } - return pluginenv.DefaultSupervisorProvider(bundle) - }) - - me.pluginHooks[manifest.Id] = hooks - manifestCopy := *manifest if manifestCopy.Backend == nil { manifestCopy.Backend = &model.ManifestBackend{} @@ -373,6 +366,9 @@ func (me *TestHelper) InstallPlugin(manifest *model.Manifest, hooks plugin.Hooks panic(err) } + pluginDir := filepath.Join(me.tempWorkspace, "plugins") + webappDir := filepath.Join(me.tempWorkspace, "webapp") + if err := os.MkdirAll(filepath.Join(pluginDir, manifest.Id), 0700); err != nil { panic(err) } @@ -380,6 +376,15 @@ func (me *TestHelper) InstallPlugin(manifest *model.Manifest, hooks plugin.Hooks if err := ioutil.WriteFile(filepath.Join(pluginDir, manifest.Id, "plugin.json"), manifestBytes, 0600); err != nil { panic(err) } + + me.App.InitPlugins(pluginDir, webappDir, func(bundle *model.BundleInfo) (plugin.Supervisor, error) { + if hooks, ok := me.pluginHooks[bundle.Manifest.Id]; ok { + return &mockPluginSupervisor{hooks}, nil + } + return pluginenv.DefaultSupervisorProvider(bundle) + }) + + me.pluginHooks[manifest.Id] = hooks } func (me *TestHelper) ResetRoleMigration() { @@ -415,6 +420,9 @@ func (me *FakeClusterInterface) GetClusterStats() ([]*model.ClusterStats, *model func (me *FakeClusterInterface) GetLogs(page, perPage int) ([]string, *model.AppError) { return []string{}, nil } +func (me *FakeClusterInterface) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { + return nil, nil +} func (me *FakeClusterInterface) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError { return nil } diff --git a/app/cluster_discovery.go b/app/cluster_discovery.go index f7443680c..250744279 100644 --- a/app/cluster_discovery.go +++ b/app/cluster_discovery.go @@ -85,3 +85,11 @@ func (a *App) IsLeader() bool { return true } } + +func (a *App) GetClusterId() string { + if a.Cluster == nil { + return "" + } + + return a.Cluster.GetClusterId() +} diff --git a/app/plugin.go b/app/plugin.go index 0d3415f4c..f6cb6bdda 100644 --- a/app/plugin.go +++ b/app/plugin.go @@ -37,6 +37,31 @@ var prepackagedPlugins map[string]func(string) ([]byte, error) = map[string]func "zoom": zoom.Asset, } +func (a *App) notifyPluginStatusesChanged() error { + pluginStatuses, err := a.GetClusterPluginStatuses() + if err != nil { + return err + } + + // Notify any system admins. + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PLUGIN_STATUSES_CHANGED, "", "", "", nil) + message.Add("plugin_statuses", pluginStatuses) + message.Broadcast.ContainsSensitiveData = true + a.Publish(message) + + return nil +} + +func (a *App) setPluginStatusState(id string, state int) error { + if _, ok := a.pluginStatuses[id]; !ok { + return nil + } + + a.pluginStatuses[id].State = state + + return a.notifyPluginStatusesChanged() +} + func (a *App) initBuiltInPlugins() { plugins := map[string]builtinplugin.Plugin{ "ldapextras": &ldapextras.Plugin{}, @@ -77,30 +102,100 @@ func (a *App) setPluginsActive(activate bool) { continue } - id := plugin.Manifest.Id + enabled := false + if state, ok := a.Config().PluginSettings.PluginStates[plugin.Manifest.Id]; ok { + enabled = state.Enable + } + + a.pluginStatuses[plugin.Manifest.Id] = &model.PluginStatus{ + ClusterId: a.GetClusterId(), + PluginId: plugin.Manifest.Id, + PluginPath: filepath.Dir(plugin.ManifestPath), + IsSandboxed: a.IsPluginSandboxSupported, + Name: plugin.Manifest.Name, + Description: plugin.Manifest.Description, + Version: plugin.Manifest.Version, + } + + if activate && enabled { + a.setPluginActive(plugin, activate) + } else if !activate { + a.setPluginActive(plugin, activate) + } + } + + if err := a.notifyPluginStatusesChanged(); err != nil { + mlog.Error("failed to notify plugin status changed", mlog.Err(err)) + } +} + +func (a *App) setPluginActiveById(id string, activate bool) { + plugins, err := a.PluginEnv.Plugins() + if err != nil { + mlog.Error(fmt.Sprintf("Cannot setPluginActiveById(%t)", activate), mlog.String("plugin_id", id), mlog.Err(err)) + return + } - pluginState := &model.PluginState{Enable: false} - if state, ok := a.Config().PluginSettings.PluginStates[id]; ok { - pluginState = state + for _, plugin := range plugins { + if plugin.Manifest != nil && plugin.Manifest.Id == id { + a.setPluginActive(plugin, activate) } + } +} + +func (a *App) setPluginActive(plugin *model.BundleInfo, activate bool) { + if plugin.Manifest == nil { + return + } - active := a.PluginEnv.IsPluginActive(id) + id := plugin.Manifest.Id - if activate && pluginState.Enable && !active { + active := a.PluginEnv.IsPluginActive(id) + + if activate { + if !active { if err := a.activatePlugin(plugin.Manifest); err != nil { mlog.Error("Plugin failed to activate", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("err", err.DetailedError)) } + } - } else if (!activate || !pluginState.Enable) && active { + } else if !activate { + if active { if err := a.deactivatePlugin(plugin.Manifest); err != nil { mlog.Error("Plugin failed to deactivate", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("err", err.DetailedError)) } + } else { + if err := a.setPluginStatusState(plugin.Manifest.Id, model.PluginStateNotRunning); err != nil { + mlog.Error("Plugin status state failed to update", mlog.String("plugin_id", plugin.Manifest.Id), mlog.String("err", err.Error())) + } } } } func (a *App) activatePlugin(manifest *model.Manifest) *model.AppError { - if err := a.PluginEnv.ActivatePlugin(manifest.Id); err != nil { + mlog.Debug("Activating plugin", mlog.String("plugin_id", manifest.Id)) + + if err := a.setPluginStatusState(manifest.Id, model.PluginStateStarting); err != nil { + return model.NewAppError("activatePlugin", "app.plugin.set_plugin_status_state.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + onError := func(err error) { + mlog.Debug("Plugin failed to stay running", mlog.String("plugin_id", manifest.Id), mlog.Err(err)) + + if err := a.setPluginStatusState(manifest.Id, model.PluginStateFailedToStayRunning); err != nil { + mlog.Error("Failed to record plugin status", mlog.String("plugin_id", manifest.Id), mlog.Err(err)) + } + } + + if err := a.PluginEnv.ActivatePlugin(manifest.Id, onError); err != nil { + if err := a.setPluginStatusState(manifest.Id, model.PluginStateFailedToStart); err != nil { + return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusBadRequest) + } + + if err := a.setPluginStatusState(manifest.Id, model.PluginStateRunning); err != nil { return model.NewAppError("activatePlugin", "app.plugin.activate.app_error", nil, err.Error(), http.StatusBadRequest) } @@ -115,6 +210,12 @@ func (a *App) activatePlugin(manifest *model.Manifest) *model.AppError { } func (a *App) deactivatePlugin(manifest *model.Manifest) *model.AppError { + mlog.Debug("Deactivating plugin", mlog.String("plugin_id", manifest.Id)) + + if err := a.setPluginStatusState(manifest.Id, model.PluginStateStopping); err != nil { + return model.NewAppError("EnablePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusInternalServerError) + } + if err := a.PluginEnv.DeactivatePlugin(manifest.Id); err != nil { return model.NewAppError("deactivatePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest) } @@ -127,6 +228,10 @@ func (a *App) deactivatePlugin(manifest *model.Manifest) *model.AppError { a.Publish(message) } + if err := a.setPluginStatusState(manifest.Id, model.PluginStateNotRunning); err != nil { + return model.NewAppError("deactivatePlugin", "app.plugin.deactivate.app_error", nil, err.Error(), http.StatusBadRequest) + } + mlog.Info("Deactivated plugin", mlog.String("plugin_id", manifest.Id)) return nil } @@ -166,7 +271,8 @@ func (a *App) installPlugin(pluginFile io.Reader, allowPrepackaged bool) (*model return nil, model.NewAppError("installPlugin", "app.plugin.manifest.app_error", nil, err.Error(), http.StatusBadRequest) } - if _, ok := prepackagedPlugins[manifest.Id]; ok && !allowPrepackaged { + _, isPrepackaged := prepackagedPlugins[manifest.Id] + if isPrepackaged && !allowPrepackaged { return nil, model.NewAppError("installPlugin", "app.plugin.prepackaged.app_error", nil, "", http.StatusBadRequest) } @@ -185,16 +291,33 @@ func (a *App) installPlugin(pluginFile io.Reader, allowPrepackaged bool) (*model } } - err = utils.CopyDir(tmpPluginDir, filepath.Join(a.PluginEnv.SearchPath(), manifest.Id)) + pluginPath := filepath.Join(a.PluginEnv.SearchPath(), manifest.Id) + err = utils.CopyDir(tmpPluginDir, pluginPath) if err != nil { return nil, model.NewAppError("installPlugin", "app.plugin.mvdir.app_error", nil, err.Error(), http.StatusInternalServerError) } - // Should add manifest validation and error handling here + a.pluginStatuses[manifest.Id] = &model.PluginStatus{ + ClusterId: a.GetClusterId(), + PluginId: manifest.Id, + PluginPath: pluginPath, + State: model.PluginStateNotRunning, + IsSandboxed: a.IsPluginSandboxSupported, + IsPrepackaged: isPrepackaged, + Name: manifest.Name, + Description: manifest.Description, + Version: manifest.Version, + } + + if err := a.notifyPluginStatusesChanged(); err != nil { + mlog.Error("failed to notify plugin status changed", mlog.Err(err)) + } return manifest, nil } +// GetPlugins returned the plugins installed on this server, including the manifests needed to +// enable plugins with web functionality. func (a *App) GetPlugins() (*model.PluginsResponse, *model.AppError) { if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable { return nil, model.NewAppError("GetPlugins", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) @@ -240,6 +363,39 @@ func (a *App) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) { return manifests, nil } +// GetPluginStatuses returns the status for plugins installed on this server. +func (a *App) GetPluginStatuses() (model.PluginStatuses, *model.AppError) { + if !*a.Config().PluginSettings.Enable { + return nil, model.NewAppError("GetPluginStatuses", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + pluginStatuses := make([]*model.PluginStatus, 0, len(a.pluginStatuses)) + for _, pluginStatus := range a.pluginStatuses { + pluginStatuses = append(pluginStatuses, pluginStatus) + } + + return pluginStatuses, nil +} + +// GetClusterPluginStatuses returns the status for plugins installed anywhere in the cluster. +func (a *App) GetClusterPluginStatuses() (model.PluginStatuses, *model.AppError) { + pluginStatuses, err := a.GetPluginStatuses() + if err != nil { + return nil, err + } + + if a.Cluster != nil && *a.Config().ClusterSettings.Enable { + clusterPluginStatuses, err := a.Cluster.GetPluginStatuses() + if err != nil { + return nil, model.NewAppError("GetClusterPluginStatuses", "app.plugin.get_cluster_plugin_statuses.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + pluginStatuses = append(pluginStatuses, clusterPluginStatuses...) + } + + return pluginStatuses, nil +} + func (a *App) RemovePlugin(id string) *model.AppError { return a.removePlugin(id, false) } @@ -284,10 +440,16 @@ func (a *App) removePlugin(id string, allowPrepackaged bool) *model.AppError { return model.NewAppError("removePlugin", "app.plugin.remove.app_error", nil, err.Error(), http.StatusInternalServerError) } + delete(a.pluginStatuses, manifest.Id) + if err := a.notifyPluginStatusesChanged(); err != nil { + mlog.Error("failed to notify plugin status changed", mlog.Err(err)) + } + return nil } -// EnablePlugin will set the config for an installed plugin to enabled, triggering activation if inactive. +// EnablePlugin will set the config for an installed plugin to enabled, triggering asynchronous +// activation if inactive anywhere in the cluster. func (a *App) EnablePlugin(id string) *model.AppError { if a.PluginEnv == nil || !*a.Config().PluginSettings.Enable { return model.NewAppError("EnablePlugin", "app.plugin.disabled.app_error", nil, "", http.StatusNotImplemented) @@ -310,8 +472,8 @@ func (a *App) EnablePlugin(id string) *model.AppError { return model.NewAppError("EnablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest) } - if err := a.activatePlugin(manifest); err != nil { - return err + if err := a.setPluginStatusState(manifest.Id, model.PluginStateStarting); err != nil { + return model.NewAppError("EnablePlugin", "app.plugin.set_plugin_status_state.app_error", nil, err.Error(), http.StatusInternalServerError) } a.UpdateConfig(func(cfg *model.Config) { @@ -351,6 +513,10 @@ func (a *App) DisablePlugin(id string) *model.AppError { return model.NewAppError("DisablePlugin", "app.plugin.not_installed.app_error", nil, "", http.StatusBadRequest) } + if err := a.setPluginStatusState(manifest.Id, model.PluginStateStopping); err != nil { + return model.NewAppError("EnablePlugin", "app.plugin.set_plugin_status_state.app_error", nil, err.Error(), http.StatusInternalServerError) + } + a.UpdateConfig(func(cfg *model.Config) { cfg.PluginSettings.PluginStates[id] = &model.PluginState{Enable: false} }) @@ -363,16 +529,18 @@ func (a *App) DisablePlugin(id string) *model.AppError { } func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride pluginenv.SupervisorProviderFunc) { - if !*a.Config().PluginSettings.Enable { + if a.PluginEnv != nil { return } - if a.PluginEnv != nil { + if !*a.Config().PluginSettings.Enable { return } mlog.Info("Starting up plugins") + a.pluginStatuses = make(map[string]*model.PluginStatus) + if err := os.Mkdir(pluginPath, 0744); err != nil && !os.IsExist(err) { mlog.Error("Failed to start up plugins", mlog.Err(err)) return @@ -398,13 +566,19 @@ func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride plug }), } - if supervisorOverride != nil { - options = append(options, pluginenv.SupervisorProvider(supervisorOverride)) - } else if err := sandbox.CheckSupport(); err != nil { + if err := sandbox.CheckSupport(); err != nil { + a.IsPluginSandboxSupported = false mlog.Warn("plugin sandboxing is not supported. plugins will run with the same access level as the server. See documentation to learn more: https://developers.mattermost.com/extend/plugins/security/", mlog.Err(err)) - options = append(options, pluginenv.SupervisorProvider(rpcplugin.SupervisorProvider)) } else { + a.IsPluginSandboxSupported = true + } + + if supervisorOverride != nil { + options = append(options, pluginenv.SupervisorProvider(supervisorOverride)) + } else if a.IsPluginSandboxSupported { options = append(options, pluginenv.SupervisorProvider(sandbox.SupervisorProvider)) + } else { + options = append(options, pluginenv.SupervisorProvider(rpcplugin.SupervisorProvider)) } if env, err := pluginenv.New(options...); err != nil { @@ -431,12 +605,34 @@ func (a *App) InitPlugins(pluginPath, webappPath string, supervisorOverride plug } a.RemoveConfigListener(a.PluginConfigListenerId) - a.PluginConfigListenerId = a.AddConfigListener(func(_, cfg *model.Config) { + a.PluginConfigListenerId = a.AddConfigListener(func(oldCfg *model.Config, cfg *model.Config) { if a.PluginEnv == nil { return } - a.setPluginsActive(*cfg.PluginSettings.Enable) + if *oldCfg.PluginSettings.Enable != *cfg.PluginSettings.Enable { + a.setPluginsActive(*cfg.PluginSettings.Enable) + } else { + plugins := map[string]bool{} + for id := range oldCfg.PluginSettings.PluginStates { + plugins[id] = true + } + for id := range cfg.PluginSettings.PluginStates { + plugins[id] = true + } + + for id := range plugins { + oldPluginState := oldCfg.PluginSettings.PluginStates[id] + pluginState := cfg.PluginSettings.PluginStates[id] + + wasEnabled := oldPluginState != nil && oldPluginState.Enable + isEnabled := pluginState != nil && pluginState.Enable + + if wasEnabled != isEnabled { + a.setPluginActiveById(id, isEnabled) + } + } + } for _, err := range a.PluginEnv.Hooks().OnConfigurationChange() { mlog.Error(err.Error()) diff --git a/app/plugin_test.go b/app/plugin_test.go index 9ad5dc1fa..db5954d4d 100644 --- a/app/plugin_test.go +++ b/app/plugin_test.go @@ -7,8 +7,8 @@ import ( "errors" "net/http" "net/http/httptest" - "strings" "testing" + "time" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" @@ -158,6 +158,20 @@ func TestPluginCommands(t *testing.T) { require.Nil(t, th.App.EnablePlugin("foo")) + // Ideally, we would wait for the websocket activation event instead of just sleeping. + time.Sleep(500 * time.Millisecond) + + pluginStatuses, err := th.App.GetPluginStatuses() + require.Nil(t, err) + found := false + for _, pluginStatus := range pluginStatuses { + if pluginStatus.PluginId == "foo" { + require.Equal(t, model.PluginStateRunning, pluginStatus.State) + found = true + } + } + require.True(t, found, "failed to find plugin foo in plugin statuses") + resp, err := th.App.ExecuteCommand(&model.CommandArgs{ Command: "/foo2", TeamId: th.BasicTeam.Id, @@ -216,7 +230,46 @@ func TestPluginBadActivation(t *testing.T) { t.Run("EnablePlugin bad activation", func(t *testing.T) { err := th.App.EnablePlugin("foo") - assert.NotNil(t, err) - assert.True(t, strings.Contains(err.DetailedError, "won't activate for some reason")) + assert.Nil(t, err) + + // Ideally, we would wait for the websocket activation event instead of just + // sleeping. + time.Sleep(500 * time.Millisecond) + + pluginStatuses, err := th.App.GetPluginStatuses() + require.Nil(t, err) + found := false + for _, pluginStatus := range pluginStatuses { + if pluginStatus.PluginId == "foo" { + require.Equal(t, model.PluginStateFailedToStart, pluginStatus.State) + found = true + } + } + require.True(t, found, "failed to find plugin foo in plugin statuses") + }) +} + +func TestGetPluginStatusesDisabled(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = false }) + + _, err := th.App.GetPluginStatuses() + require.EqualError(t, err, "GetPluginStatuses: Plugins have been disabled. Please check your logs for details., ") +} + +func TestGetPluginStatuses(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.PluginSettings.Enable = true + }) + + pluginStatuses, err := th.App.GetPluginStatuses() + require.Nil(t, err) + require.NotNil(t, pluginStatuses) } diff --git a/einterfaces/cluster.go b/einterfaces/cluster.go index b5ef4772a..dd9c57f11 100644 --- a/einterfaces/cluster.go +++ b/einterfaces/cluster.go @@ -21,5 +21,6 @@ type ClusterInterface interface { NotifyMsg(buf []byte) GetClusterStats() ([]*model.ClusterStats, *model.AppError) GetLogs(page, perPage int) ([]string, *model.AppError) + GetPluginStatuses() (model.PluginStatuses, *model.AppError) ConfigChanged(previousConfig *model.Config, newConfig *model.Config, sendToOtherServer bool) *model.AppError } diff --git a/i18n/en.json b/i18n/en.json index 24e49278c..9f008a64b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3854,6 +3854,10 @@ "id": "app.plugin.deactivate.app_error", "translation": "Unable to deactivate plugin" }, + { + "id": "app.plugin.delete_plugin_status_state.app_error", + "translation": "Unable to delete plugin status state." + }, { "id": "app.plugin.disabled.app_error", "translation": "Plugins have been disabled. Please check your logs for details." @@ -3898,10 +3902,18 @@ "id": "app.plugin.not_installed.app_error", "translation": "Plugin is not installed" }, + { + "id": "app.plugin.prepackaged.app_error", + "translation": "Cannot install prepackaged plugin" + }, { "id": "app.plugin.remove.app_error", "translation": "Unable to delete plugin" }, + { + "id": "app.plugin.set_plugin_status_state.app_error", + "translation": "Unable to set plugin status state." + }, { "id": "app.plugin.upload_disabled.app_error", "translation": "Plugins and/or plugin uploads have been disabled." @@ -4798,6 +4810,10 @@ "id": "model.client.writer.app_error", "translation": "Unable to build multipart request" }, + { + "id": "model.cluster.is_valid.id.app_error", + "translation": "Invalid Id" + }, { "id": "model.command.is_valid.create_at.app_error", "translation": "Create at must be a valid time" diff --git a/model/client4.go b/model/client4.go index d245fe6c0..97dd30790 100644 --- a/model/client4.go +++ b/model/client4.go @@ -3534,6 +3534,18 @@ func (c *Client4) GetPlugins() (*PluginsResponse, *Response) { } } +// GetPluginStatuses will return the plugins installed on any server in the cluster, for reporting +// to the administrator via the system console. +// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. +func (c *Client4) GetPluginStatuses() (PluginStatuses, *Response) { + if r, err := c.DoApiGet(c.GetPluginsRoute(), "/statuses"); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return PluginStatusesFromJson(r.Body), BuildResponse(r) + } +} + // RemovePlugin will deactivate and delete a plugin. // WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE. func (c *Client4) RemovePlugin(id string) (bool, *Response) { diff --git a/model/cluster_discovery.go b/model/cluster_discovery.go index 89e5fc95e..5d5b0465d 100644 --- a/model/cluster_discovery.go +++ b/model/cluster_discovery.go @@ -86,7 +86,7 @@ func FilterClusterDiscovery(vs []*ClusterDiscovery, f func(*ClusterDiscovery) bo func (o *ClusterDiscovery) IsValid() *AppError { if len(o.Id) != 26 { - return NewAppError("Channel.IsValid", "model.channel.is_valid.id.app_error", nil, "", http.StatusBadRequest) + return NewAppError("ClusterDiscovery.IsValid", "model.cluster.is_valid.id.app_error", nil, "", http.StatusBadRequest) } if len(o.ClusterName) == 0 { diff --git a/model/plugin_status.go b/model/plugin_status.go new file mode 100644 index 000000000..1ae64ff89 --- /dev/null +++ b/model/plugin_status.go @@ -0,0 +1,44 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +import ( + "encoding/json" + "io" +) + +const ( + PluginStateNotRunning = 0 + PluginStateStarting = 1 + PluginStateRunning = 2 + PluginStateFailedToStart = 3 + PluginStateFailedToStayRunning = 4 + PluginStateStopping = 5 +) + +// PluginStatus provides a cluster-aware view of installed plugins. +type PluginStatus struct { + PluginId string `json:"plugin_id"` + ClusterId string `json:"cluster_id"` + PluginPath string `json:"plugin_path"` + State int `json:"state"` + IsSandboxed bool `json:"is_sandboxed"` + IsPrepackaged bool `json:"is_prepackaged"` + Name string `json:"name"` + Description string `json:"description"` + Version string `json:"version"` +} + +type PluginStatuses []*PluginStatus + +func (m *PluginStatuses) ToJson() string { + b, _ := json.Marshal(m) + return string(b) +} + +func PluginStatusesFromJson(data io.Reader) PluginStatuses { + var m PluginStatuses + json.NewDecoder(data).Decode(&m) + return m +} diff --git a/model/websocket_message.go b/model/websocket_message.go index 08c238480..071975d6c 100644 --- a/model/websocket_message.go +++ b/model/websocket_message.go @@ -10,44 +10,45 @@ import ( ) const ( - WEBSOCKET_EVENT_TYPING = "typing" - WEBSOCKET_EVENT_POSTED = "posted" - WEBSOCKET_EVENT_POST_EDITED = "post_edited" - WEBSOCKET_EVENT_POST_DELETED = "post_deleted" - WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted" - WEBSOCKET_EVENT_CHANNEL_CREATED = "channel_created" - WEBSOCKET_EVENT_CHANNEL_UPDATED = "channel_updated" - WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED = "channel_member_updated" - WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added" - WEBSOCKET_EVENT_GROUP_ADDED = "group_added" - WEBSOCKET_EVENT_NEW_USER = "new_user" - WEBSOCKET_EVENT_ADDED_TO_TEAM = "added_to_team" - WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team" - WEBSOCKET_EVENT_UPDATE_TEAM = "update_team" - WEBSOCKET_EVENT_DELETE_TEAM = "delete_team" - WEBSOCKET_EVENT_USER_ADDED = "user_added" - WEBSOCKET_EVENT_USER_UPDATED = "user_updated" - WEBSOCKET_EVENT_USER_ROLE_UPDATED = "user_role_updated" - WEBSOCKET_EVENT_MEMBERROLE_UPDATED = "memberrole_updated" - WEBSOCKET_EVENT_USER_REMOVED = "user_removed" - WEBSOCKET_EVENT_PREFERENCE_CHANGED = "preference_changed" - WEBSOCKET_EVENT_PREFERENCES_CHANGED = "preferences_changed" - WEBSOCKET_EVENT_PREFERENCES_DELETED = "preferences_deleted" - WEBSOCKET_EVENT_EPHEMERAL_MESSAGE = "ephemeral_message" - WEBSOCKET_EVENT_STATUS_CHANGE = "status_change" - WEBSOCKET_EVENT_HELLO = "hello" - WEBSOCKET_EVENT_WEBRTC = "webrtc" - WEBSOCKET_AUTHENTICATION_CHALLENGE = "authentication_challenge" - WEBSOCKET_EVENT_REACTION_ADDED = "reaction_added" - WEBSOCKET_EVENT_REACTION_REMOVED = "reaction_removed" - WEBSOCKET_EVENT_RESPONSE = "response" - WEBSOCKET_EVENT_EMOJI_ADDED = "emoji_added" - WEBSOCKET_EVENT_CHANNEL_VIEWED = "channel_viewed" - WEBSOCKET_EVENT_PLUGIN_ACTIVATED = "plugin_activated" // EXPERIMENTAL - SUBJECT TO CHANGE - WEBSOCKET_EVENT_PLUGIN_DEACTIVATED = "plugin_deactivated" // EXPERIMENTAL - SUBJECT TO CHANGE - WEBSOCKET_EVENT_ROLE_UPDATED = "role_updated" - WEBSOCKET_EVENT_LICENSE_CHANGED = "license_changed" - WEBSOCKET_EVENT_CONFIG_CHANGED = "config_changed" + WEBSOCKET_EVENT_TYPING = "typing" + WEBSOCKET_EVENT_POSTED = "posted" + WEBSOCKET_EVENT_POST_EDITED = "post_edited" + WEBSOCKET_EVENT_POST_DELETED = "post_deleted" + WEBSOCKET_EVENT_CHANNEL_DELETED = "channel_deleted" + WEBSOCKET_EVENT_CHANNEL_CREATED = "channel_created" + WEBSOCKET_EVENT_CHANNEL_UPDATED = "channel_updated" + WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED = "channel_member_updated" + WEBSOCKET_EVENT_DIRECT_ADDED = "direct_added" + WEBSOCKET_EVENT_GROUP_ADDED = "group_added" + WEBSOCKET_EVENT_NEW_USER = "new_user" + WEBSOCKET_EVENT_ADDED_TO_TEAM = "added_to_team" + WEBSOCKET_EVENT_LEAVE_TEAM = "leave_team" + WEBSOCKET_EVENT_UPDATE_TEAM = "update_team" + WEBSOCKET_EVENT_DELETE_TEAM = "delete_team" + WEBSOCKET_EVENT_USER_ADDED = "user_added" + WEBSOCKET_EVENT_USER_UPDATED = "user_updated" + WEBSOCKET_EVENT_USER_ROLE_UPDATED = "user_role_updated" + WEBSOCKET_EVENT_MEMBERROLE_UPDATED = "memberrole_updated" + WEBSOCKET_EVENT_USER_REMOVED = "user_removed" + WEBSOCKET_EVENT_PREFERENCE_CHANGED = "preference_changed" + WEBSOCKET_EVENT_PREFERENCES_CHANGED = "preferences_changed" + WEBSOCKET_EVENT_PREFERENCES_DELETED = "preferences_deleted" + WEBSOCKET_EVENT_EPHEMERAL_MESSAGE = "ephemeral_message" + WEBSOCKET_EVENT_STATUS_CHANGE = "status_change" + WEBSOCKET_EVENT_HELLO = "hello" + WEBSOCKET_EVENT_WEBRTC = "webrtc" + WEBSOCKET_AUTHENTICATION_CHALLENGE = "authentication_challenge" + WEBSOCKET_EVENT_REACTION_ADDED = "reaction_added" + WEBSOCKET_EVENT_REACTION_REMOVED = "reaction_removed" + WEBSOCKET_EVENT_RESPONSE = "response" + WEBSOCKET_EVENT_EMOJI_ADDED = "emoji_added" + WEBSOCKET_EVENT_CHANNEL_VIEWED = "channel_viewed" + WEBSOCKET_EVENT_PLUGIN_ACTIVATED = "plugin_activated" // EXPERIMENTAL - SUBJECT TO CHANGE + WEBSOCKET_EVENT_PLUGIN_DEACTIVATED = "plugin_deactivated" // EXPERIMENTAL - SUBJECT TO CHANGE + WEBSOCKET_EVENT_PLUGIN_STATUSES_CHANGED = "plugin_statuses_changed" // EXPERIMENTAL - SUBJECT TO CHANGE + WEBSOCKET_EVENT_ROLE_UPDATED = "role_updated" + WEBSOCKET_EVENT_LICENSE_CHANGED = "license_changed" + WEBSOCKET_EVENT_CONFIG_CHANGED = "config_changed" ) type WebSocketMessage interface { diff --git a/plugin/pluginenv/environment.go b/plugin/pluginenv/environment.go index 947eda86d..f704aa5bb 100644 --- a/plugin/pluginenv/environment.go +++ b/plugin/pluginenv/environment.go @@ -108,7 +108,7 @@ func (env *Environment) IsPluginActive(pluginId string) bool { } // Activates the plugin with the given id. -func (env *Environment) ActivatePlugin(id string) error { +func (env *Environment) ActivatePlugin(id string, onError func(error)) error { env.mutex.Lock() defer env.mutex.Unlock() @@ -117,7 +117,7 @@ func (env *Environment) ActivatePlugin(id string) error { } if _, ok := env.activePlugins[id]; ok { - return nil + return fmt.Errorf("plugin already active: %v", id) } plugins, err := ScanSearchPath(env.searchPath) if err != nil { @@ -156,6 +156,14 @@ func (env *Environment) ActivatePlugin(id string) error { if err := supervisor.Start(api); err != nil { return errors.Wrapf(err, "unable to start plugin: %v", id) } + if onError != nil { + go func() { + err := supervisor.Wait() + if err != nil { + onError(err) + } + }() + } activePlugin.Supervisor = supervisor } diff --git a/plugin/pluginenv/environment_test.go b/plugin/pluginenv/environment_test.go index 91d639f69..8c1397799 100644 --- a/plugin/pluginenv/environment_test.go +++ b/plugin/pluginenv/environment_test.go @@ -56,6 +56,10 @@ func (m *MockSupervisor) Hooks() plugin.Hooks { return m.Called().Get(0).(plugin.Hooks) } +func (m *MockSupervisor) Wait() error { + return m.Called().Get(0).(error) +} + func initTmpDir(t *testing.T, files map[string]string) string { success := false dir, err := ioutil.TempDir("", "mm-plugin-test") @@ -130,7 +134,7 @@ func TestEnvironment(t *testing.T) { activePlugins := env.ActivePlugins() assert.Len(t, activePlugins, 0) - assert.Error(t, env.ActivatePlugin("x")) + assert.Error(t, env.ActivatePlugin("x", nil)) var api struct{ plugin.API } var supervisor MockSupervisor @@ -145,11 +149,11 @@ func TestEnvironment(t *testing.T) { supervisor.On("Stop").Return(nil) supervisor.On("Hooks").Return(&hooks) - assert.NoError(t, env.ActivatePlugin("foo")) + assert.NoError(t, env.ActivatePlugin("foo", nil)) assert.Equal(t, env.ActivePluginIds(), []string{"foo"}) activePlugins = env.ActivePlugins() assert.Len(t, activePlugins, 1) - assert.NoError(t, env.ActivatePlugin("foo")) + assert.Error(t, env.ActivatePlugin("foo", nil)) assert.True(t, env.IsPluginActive("foo")) hooks.On("OnDeactivate").Return(nil) @@ -157,7 +161,7 @@ func TestEnvironment(t *testing.T) { assert.Error(t, env.DeactivatePlugin("foo")) assert.False(t, env.IsPluginActive("foo")) - assert.NoError(t, env.ActivatePlugin("foo")) + assert.NoError(t, env.ActivatePlugin("foo", nil)) assert.Equal(t, env.ActivePluginIds(), []string{"foo"}) assert.Equal(t, env.SearchPath(), dir) @@ -184,7 +188,7 @@ func TestEnvironment_DuplicatePluginError(t *testing.T) { require.NoError(t, err) defer env.Shutdown() - assert.Error(t, env.ActivatePlugin("foo")) + assert.Error(t, env.ActivatePlugin("foo", nil)) assert.Empty(t, env.ActivePluginIds()) } @@ -200,7 +204,7 @@ func TestEnvironment_BadSearchPathError(t *testing.T) { require.NoError(t, err) defer env.Shutdown() - assert.Error(t, env.ActivatePlugin("foo")) + assert.Error(t, env.ActivatePlugin("foo", nil)) assert.Empty(t, env.ActivePluginIds()) } @@ -244,7 +248,7 @@ func TestEnvironment_ActivatePluginErrors(t *testing.T) { hooks.Mock = mock.Mock{} provider.Mock = mock.Mock{} setup() - assert.Error(t, env.ActivatePlugin("foo")) + assert.Error(t, env.ActivatePlugin("foo", nil)) assert.Empty(t, env.ActivePluginIds()) supervisor.AssertExpectations(t) hooks.AssertExpectations(t) @@ -285,7 +289,7 @@ func TestEnvironment_ShutdownError(t *testing.T) { hooks.On("OnDeactivate").Return(fmt.Errorf("test error")) - assert.NoError(t, env.ActivatePlugin("foo")) + assert.NoError(t, env.ActivatePlugin("foo", nil)) assert.Equal(t, env.ActivePluginIds(), []string{"foo"}) assert.Len(t, env.Shutdown(), 2) } @@ -332,7 +336,7 @@ func TestEnvironment_ConcurrentHookInvocations(t *testing.T) { } }) - assert.NoError(t, env.ActivatePlugin("foo")) + assert.NoError(t, env.ActivatePlugin("foo", nil)) rec := httptest.NewRecorder() @@ -391,7 +395,7 @@ func TestEnvironment_HooksForPlugins(t *testing.T) { Text: "bar", }, nil) - assert.NoError(t, env.ActivatePlugin("foo")) + assert.NoError(t, env.ActivatePlugin("foo", nil)) assert.Equal(t, env.ActivePluginIds(), []string{"foo"}) resp, appErr, err := env.HooksForPlugin("foo").ExecuteCommand(&model.CommandArgs{ diff --git a/plugin/rpcplugin/rpcplugintest/supervisor.go b/plugin/rpcplugin/rpcplugintest/supervisor.go index 2ae065621..d225f96fc 100644 --- a/plugin/rpcplugin/rpcplugintest/supervisor.go +++ b/plugin/rpcplugin/rpcplugintest/supervisor.go @@ -174,6 +174,14 @@ func testSupervisor_PluginCrash(t *testing.T, sp SupervisorProviderFunc) { bundle := model.BundleInfoForPath(dir) supervisor, err := sp(bundle) require.NoError(t, err) + + var supervisorWaitErr error + supervisorWaitDone := make(chan bool, 1) + go func() { + supervisorWaitErr = supervisor.Wait() + close(supervisorWaitDone) + }() + require.NoError(t, supervisor.Start(&api)) failed := false @@ -189,7 +197,21 @@ func testSupervisor_PluginCrash(t *testing.T, sp SupervisorProviderFunc) { time.Sleep(time.Millisecond * 100) } assert.True(t, recovered) + + select { + case <-supervisorWaitDone: + require.Fail(t, "supervisor.Wait() unexpectedly returned") + case <-time.After(500 * time.Millisecond): + } + require.NoError(t, supervisor.Stop()) + + select { + case <-supervisorWaitDone: + require.Nil(t, supervisorWaitErr) + case <-time.After(5000 * time.Millisecond): + require.Fail(t, "supervisor.Wait() failed to return") + } } // Crashed plugins should be relaunched at most three times. @@ -239,6 +261,14 @@ func testSupervisor_PluginRepeatedlyCrash(t *testing.T, sp SupervisorProviderFun bundle := model.BundleInfoForPath(dir) supervisor, err := sp(bundle) require.NoError(t, err) + + var supervisorWaitErr error + supervisorWaitDone := make(chan bool, 1) + go func() { + supervisorWaitErr = supervisor.Wait() + close(supervisorWaitDone) + }() + require.NoError(t, supervisor.Start(&api)) for attempt := 1; attempt <= 4; attempt++ { @@ -264,10 +294,19 @@ func testSupervisor_PluginRepeatedlyCrash(t *testing.T, sp SupervisorProviderFun } if attempt < 4 { + require.Nil(t, supervisorWaitErr) require.True(t, recovered, "failed to recover after attempt %d", attempt) } else { require.False(t, recovered, "unexpectedly recovered after attempt %d", attempt) } } + + select { + case <-supervisorWaitDone: + require.NotNil(t, supervisorWaitErr) + case <-time.After(500 * time.Millisecond): + require.Fail(t, "supervisor.Wait() failed to return after plugin crashed") + } + require.NoError(t, supervisor.Stop()) } diff --git a/plugin/rpcplugin/supervisor.go b/plugin/rpcplugin/supervisor.go index 6e26d5682..246747c89 100644 --- a/plugin/rpcplugin/supervisor.go +++ b/plugin/rpcplugin/supervisor.go @@ -32,6 +32,7 @@ type Supervisor struct { cancel context.CancelFunc newProcess func(context.Context) (Process, io.ReadWriteCloser, error) pluginId string + pluginErr error } var _ plugin.Supervisor = (*Supervisor)(nil) @@ -55,6 +56,13 @@ func (s *Supervisor) Start(api plugin.API) error { } } +// Waits for the supervisor to stop (on demand or of its own accord), returning any error that +// triggered the supervisor to stop. +func (s *Supervisor) Wait() error { + <-s.done + return s.pluginErr +} + // Stops the plugin. func (s *Supervisor) Stop() error { s.cancel() @@ -70,7 +78,7 @@ func (s *Supervisor) Hooks() plugin.Hooks { func (s *Supervisor) run(ctx context.Context, start chan<- error, api plugin.API) { defer func() { - s.done <- true + close(s.done) }() done := ctx.Done() for i := 0; i <= MaxProcessRestarts; i++ { @@ -81,10 +89,11 @@ func (s *Supervisor) run(ctx context.Context, start chan<- error, api plugin.API default: start = nil if i < MaxProcessRestarts { - mlog.Debug("Plugin terminated unexpectedly", mlog.String("plugin_id", s.pluginId)) + mlog.Error("Plugin terminated unexpectedly", mlog.String("plugin_id", s.pluginId)) time.Sleep(time.Duration((1 + i*i)) * time.Second) } else { - mlog.Debug("Plugin terminated unexpectedly too many times", mlog.String("plugin_id", s.pluginId), mlog.Int("max_process_restarts", MaxProcessRestarts)) + s.pluginErr = fmt.Errorf("plugin terminated unexpectedly too many times") + mlog.Error("Plugin shutdown", mlog.String("plugin_id", s.pluginId), mlog.Int("max_process_restarts", MaxProcessRestarts), mlog.Err(s.pluginErr)) } } } diff --git a/plugin/supervisor.go b/plugin/supervisor.go index 6cb7445f7..f20df7040 100644 --- a/plugin/supervisor.go +++ b/plugin/supervisor.go @@ -7,6 +7,7 @@ package plugin // type is only relevant to the server, and isn't used by the plugins themselves. type Supervisor interface { Start(API) error + Wait() error Stop() error Hooks() Hooks } -- cgit v1.2.3-1-g7c22 From 47f3c064db885c2cb2e75c195ea24e2ef687891d Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Thu, 24 May 2018 08:25:52 -0400 Subject: translations PR 20180522 (#8837) * translations PR 20180522 * Fix italian translation --- i18n/de.json | 36 ++++++++++++++++++++++++++++++++---- i18n/en.json | 40 ++++++++++++++++++++-------------------- i18n/es.json | 50 +++++++++++++++++++++++++++++++++++++++----------- i18n/fr.json | 36 ++++++++++++++++++++++++++++++++---- i18n/it.json | 46 +++++++++++++++++++++++++++++++++++++--------- i18n/ja.json | 36 ++++++++++++++++++++++++++++++++---- i18n/ko.json | 42 +++++++++++++++++++++++++++++++++++------- i18n/nl.json | 42 +++++++++++++++++++++++++++++++++++------- i18n/pl.json | 42 +++++++++++++++++++++++++++++++++++------- i18n/pt-BR.json | 48 ++++++++++++++++++++++++++++++++++++++---------- i18n/ru.json | 42 +++++++++++++++++++++++++++++++++++------- i18n/tr.json | 54 +++++++++++++++++++++++++++++++++++++++++------------- i18n/zh-CN.json | 50 +++++++++++++++++++++++++++++++++++++++----------- i18n/zh-TW.json | 36 ++++++++++++++++++++++++++++++++---- 14 files changed, 482 insertions(+), 118 deletions(-) diff --git a/i18n/de.json b/i18n/de.json index df7f4ab21..58c4829e8 100644 --- a/i18n/de.json +++ b/i18n/de.json @@ -822,6 +822,10 @@ "id": "api.command_invite.permission.app_error", "translation": "Sie haben nicht die nötigen Berechtigungen um {{.User}} dem Kanal {{.Channel}} hinzuzufügen." }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels." + }, { "id": "api.command_invite.success", "translation": "{{.User}} wurde dem Kanal {{.Channel}} hinzugefügt." @@ -3778,6 +3782,14 @@ "id": "app.notification.body.intro.direct.generic", "translation": "Sie haben eine neue Direktnachricht von {{.SenderName}}" }, + { + "id": "app.notification.body.intro.group_message.full", + "translation": "Sie haben eine neue Direktnachricht." + }, + { + "id": "app.notification.body.intro.group_message.generic", + "translation": "Sie haben eine neue Direktnachricht von {{.SenderName}}" + }, { "id": "app.notification.body.intro.notification.full", "translation": "Sie haben eine neue Benachrichtigung." @@ -3794,6 +3806,14 @@ "id": "app.notification.body.text.direct.generic", "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}}. {{.Month}}" }, + { + "id": "app.notification.body.text.group_message.full", + "translation": "KANAL: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}}.{{.Month}}" + }, + { + "id": "app.notification.body.text.group_message.generic", + "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}}. {{.Month}}" + }, { "id": "app.notification.body.text.notification.full", "translation": "KANAL: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}}.{{.Month}}" @@ -3806,6 +3826,14 @@ "id": "app.notification.subject.direct.full", "translation": "[{{.SiteName}}] Neue Direktnachricht von {{.SenderDisplayName}} am {{.Day}}.{{.Month}}.{{.Year}}" }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{ .SiteName }}] Benachrichtigung in {{ .TeamName}} am {{.Day}}.{{.Month}}.{{.Year}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] Neue Benachrichtigung für {{.Day}}. {{.Month}} {{.Year}}" + }, { "id": "app.notification.subject.notification.full", "translation": "[{{ .SiteName }}] Benachrichtigung in {{ .TeamName}} am {{.Day}}.{{.Month}}.{{.Year}}" @@ -5022,6 +5050,10 @@ "id": "model.config.is_valid.ldap_lastname", "translation": "AD/LDAP-Feld \"Nachnameattribut\" ist erforderlich." }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "AD/LDAP-Feld \"ID Attribut\" ist erforderlich." + }, { "id": "model.config.is_valid.ldap_max_page_size.app_error", "translation": "Ungültiger Wert für die MaxPageSize." @@ -5922,10 +5954,6 @@ "id": "store.sql_channel.delete.channel.app_error", "translation": "Der Kanal konnte nicht gelöscht werden" }, - { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "Problem beim Aktualisieren des zuletzt Aktualisiert Zeitpunkt für Mitglied" - }, { "id": "store.sql_channel.get.existing.app_error", "translation": "Der bestehende Kanal konnte nicht gefunden werden" diff --git a/i18n/en.json b/i18n/en.json index 9f008a64b..d802eb76b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3782,14 +3782,6 @@ "id": "app.notification.body.intro.direct.generic", "translation": "You have a new Direct Message from @{{.SenderName}}" }, - { - "id": "app.notification.body.intro.notification.full", - "translation": "You have a new notification." - }, - { - "id": "app.notification.body.intro.notification.generic", - "translation": "You have a new notification from @{{.SenderName}}" - }, { "id": "app.notification.body.intro.group_message.full", "translation": "You have a new Group Message." @@ -3798,6 +3790,14 @@ "id": "app.notification.body.intro.group_message.generic", "translation": "You have a new Group Message from @{{.SenderName}}" }, + { + "id": "app.notification.body.intro.notification.full", + "translation": "You have a new notification." + }, + { + "id": "app.notification.body.intro.notification.generic", + "translation": "You have a new notification from @{{.SenderName}}" + }, { "id": "app.notification.body.text.direct.full", "translation": "@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" @@ -3807,29 +3807,25 @@ "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { - "id": "app.notification.body.text.notification.full", + "id": "app.notification.body.text.group_message.full", "translation": "Channel: {{.ChannelName}}
@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { - "id": "app.notification.body.text.notification.generic", + "id": "app.notification.body.text.group_message.generic", "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { - "id": "app.notification.body.text.group_message.full", + "id": "app.notification.body.text.notification.full", "translation": "Channel: {{.ChannelName}}
@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { - "id": "app.notification.body.text.group_message.generic", + "id": "app.notification.body.text.notification.generic", "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { "id": "app.notification.subject.direct.full", "translation": "[{{.SiteName}}] New Direct Message from @{{.SenderDisplayName}} on {{.Month}} {{.Day}}, {{.Year}}" }, - { - "id": "app.notification.subject.notification.full", - "translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}" - }, { "id": "app.notification.subject.group_message.full", "translation": "[{{ .SiteName }}] New Group Message in {{ .ChannelName}} on {{.Month}} {{.Day}}, {{.Year}}" @@ -3838,6 +3834,10 @@ "id": "app.notification.subject.group_message.generic", "translation": "[{{ .SiteName }}] New Group Message on {{.Month}} {{.Day}}, {{.Year}}" }, + { + "id": "app.notification.subject.notification.full", + "translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}" + }, { "id": "app.plugin.activate.app_error", "translation": "Unable to activate extracted plugin." @@ -5062,14 +5062,14 @@ "id": "model.config.is_valid.ldap_id", "translation": "AD/LDAP field \"ID Attribute\" is required." }, - { - "id": "model.config.is_valid.ldap_login_id", - "translation": "AD/LDAP field \"Login ID Attribute\" is required." - }, { "id": "model.config.is_valid.ldap_lastname", "translation": "AD/LDAP field \"Last Name Attribute\" is required." }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "AD/LDAP field \"Login ID Attribute\" is required." + }, { "id": "model.config.is_valid.ldap_max_page_size.app_error", "translation": "Invalid max page size value." diff --git a/i18n/es.json b/i18n/es.json index 0fb44ec97..af93e16ec 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -822,6 +822,10 @@ "id": "api.command_invite.permission.app_error", "translation": "No tienes suficientes permisos para agregar a {{.User}} en {{.Channel}}." }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "No se pudo encontrar el canal {{.Channel}}. Por favor utiliza el identificador del canal." + }, { "id": "api.command_invite.success", "translation": "{{.User}} agregado al canal {{.Channel}}." @@ -3772,11 +3776,19 @@ }, { "id": "app.notification.body.intro.direct.full", - "translation": "Tienes un nuevo mensaje directo." + "translation": "Tienes un nuevo Mensaje Directo." }, { "id": "app.notification.body.intro.direct.generic", - "translation": "Tienes un nuevo mensaje directo de {{.SenderName}}" + "translation": "Tienes un nuevo Mensaje Directo de @{{.SenderName}}" + }, + { + "id": "app.notification.body.intro.group_message.full", + "translation": "Tienes un nuevo Mensaje de Grupo." + }, + { + "id": "app.notification.body.intro.group_message.generic", + "translation": "Tienes un nuevo mensaje de Grupo de @{{.SenderName}}" }, { "id": "app.notification.body.intro.notification.full", @@ -3784,19 +3796,27 @@ }, { "id": "app.notification.body.intro.notification.generic", - "translation": "Tienes una nueva notificación de {{.SenderName}}" + "translation": "Tienes una nueva notificación de @{{.SenderName}}" }, { "id": "app.notification.body.text.direct.full", - "translation": "{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}} {{.Month}}" + "translation": "@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}} {{.Month}}" }, { "id": "app.notification.body.text.direct.generic", "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}} {{.Month}}" }, + { + "id": "app.notification.body.text.group_message.full", + "translation": "Canal: {{.ChannelName}}
@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}" + }, + { + "id": "app.notification.body.text.group_message.generic", + "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}} {{.Month}}" + }, { "id": "app.notification.body.text.notification.full", - "translation": "Canal: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}" + "translation": "Canal: {{.ChannelName}}
@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}" }, { "id": "app.notification.body.text.notification.generic", @@ -3804,7 +3824,15 @@ }, { "id": "app.notification.subject.direct.full", - "translation": "[{{.SiteName}}] Nuevo Mensaje Directo de {{.SenderDisplayName}} el {{.Day}} {{.Month}}, {{.Year}}" + "translation": "[{{.SiteName}}] Nuevo Mensaje Directo de @{{.SenderDisplayName}} el {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{ .SiteName }}] Nuevo Mensaje de Grupo en {{ .TeamName}} el {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] Nuevo Mensaje de Grupo el {{.Day}} {{.Month}}, {{.Year}}" }, { "id": "app.notification.subject.notification.full", @@ -5022,6 +5050,10 @@ "id": "model.config.is_valid.ldap_lastname", "translation": "El campo AD/LDAP \"Atributo Apellido\" es obligatorio." }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "El campo AD/LDAP \"Atributo Login ID\" es obligatorio." + }, { "id": "model.config.is_valid.ldap_max_page_size.app_error", "translation": "El valor del tamaño de página no es válido." @@ -5196,7 +5228,7 @@ }, { "id": "model.config.is_valid.site_url.app_error", - "translation": "URL del Sitio debe ser una URL válida y empezar con http:// o https://." + "translation": "URL del Sitio debe estar asignado, ser una URL válida y empezar con http:// o https://." }, { "id": "model.config.is_valid.site_url_email_batching.app_error", @@ -5922,10 +5954,6 @@ "id": "store.sql_channel.delete.channel.app_error", "translation": "No pudimos eliminar el canal" }, - { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "Problema actualizando el último momento de actualización de los miembros" - }, { "id": "store.sql_channel.get.existing.app_error", "translation": "No pudimos encontrar el canal" diff --git a/i18n/fr.json b/i18n/fr.json index c408bc60d..7fca34fd0 100644 --- a/i18n/fr.json +++ b/i18n/fr.json @@ -822,6 +822,10 @@ "id": "api.command_invite.permission.app_error", "translation": "Vous n'avez pas les permissions nécessaires pour ajouter {{.User}} dans {{.Channel}}." }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels." + }, { "id": "api.command_invite.success", "translation": "{{.User}} a été ajouté dans {{.Channel}}." @@ -3778,6 +3782,14 @@ "id": "app.notification.body.intro.direct.generic", "translation": "Vous avez un nouveau message personnel de {{.SenderName}}" }, + { + "id": "app.notification.body.intro.group_message.full", + "translation": "Vous avez un nouveau message personnel." + }, + { + "id": "app.notification.body.intro.group_message.generic", + "translation": "Vous avez un nouveau message personnel de {{.SenderName}}" + }, { "id": "app.notification.body.intro.notification.full", "translation": "Vous avez une nouvelle notification." @@ -3794,6 +3806,14 @@ "id": "app.notification.body.text.direct.generic", "translation": "{{.Day}}/{{.Month}}, {{.Hour}}:{{.Minute}} {{.Timezone}}" }, + { + "id": "app.notification.body.text.group_message.full", + "translation": "CANAL : {{.ChannelName}}
{{.SenderName}} - {{.Day}}/{{.Month}}, {{.Hour}}:{{.Minute}} {{.TimeZone}}" + }, + { + "id": "app.notification.body.text.group_message.generic", + "translation": "{{.Day}}/{{.Month}}, {{.Hour}}:{{.Minute}} {{.Timezone}}" + }, { "id": "app.notification.body.text.notification.full", "translation": "CANAL : {{.ChannelName}}
{{.SenderName}} - {{.Day}}/{{.Month}}, {{.Hour}}:{{.Minute}} {{.TimeZone}}" @@ -3806,6 +3826,14 @@ "id": "app.notification.subject.direct.full", "translation": "[{{.SiteName}}] Nouveau message personnel de {{.SenderDisplayName}} du {{.Day}}/{{.Month}}/{{.Year}}" }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{.SiteName}}] Notification dans {{.TeamName}} le {{.Day}}/{{.Month}}/{{.Year}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] New Notification for {{.Day}} {{.Month}}, {{.Year}}" + }, { "id": "app.notification.subject.notification.full", "translation": "[{{.SiteName}}] Notification dans {{.TeamName}} le {{.Day}}/{{.Month}}/{{.Year}}" @@ -5022,6 +5050,10 @@ "id": "model.config.is_valid.ldap_lastname", "translation": "Le champ AD/LDAP \"Last Name Attribute\" est obligatoire." }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "Le champ AD/LDAP \"ID Attribute\" est obligatoire." + }, { "id": "model.config.is_valid.ldap_max_page_size.app_error", "translation": "Valeur de la taille maximale de page invalide." @@ -5922,10 +5954,6 @@ "id": "store.sql_channel.delete.channel.app_error", "translation": "Impossible de supprimer le canal" }, - { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "Problème de mise à jour de la date de dernière mise à jour des membres" - }, { "id": "store.sql_channel.get.existing.app_error", "translation": "Impossible de trouver le canal existant" diff --git a/i18n/it.json b/i18n/it.json index a96329287..b063ffbb9 100644 --- a/i18n/it.json +++ b/i18n/it.json @@ -822,6 +822,10 @@ "id": "api.command_invite.permission.app_error", "translation": "Non si hanno permessi sufficienti per aggiungere {{.User}} in {{.Channel}}." }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "Impossibile trovare il canale {{.Channel}}. Utilizzare il gestore canale per identificarli." + }, { "id": "api.command_invite.success", "translation": "{{.User}} aggiunto al canale {{.Channel}}." @@ -3776,7 +3780,15 @@ }, { "id": "app.notification.body.intro.direct.generic", - "translation": "Hai un nuovo messaggio diretto da {{.SenderName}}" + "translation": "Hai un nuovo messaggio diretto da @{{.SenderName}}" + }, + { + "id": "app.notification.body.intro.group_message.full", + "translation": "Hai un nuovo messaggio di gruppo." + }, + { + "id": "app.notification.body.intro.group_message.generic", + "translation": "Hai un nuovo messaggio di gruppo da @{{.SenderName}}" }, { "id": "app.notification.body.intro.notification.full", @@ -3784,19 +3796,27 @@ }, { "id": "app.notification.body.intro.notification.generic", - "translation": "Hai una nuova notifica da {{.SenderName}}" + "translation": "Hai una nuova notifica da @{{.SenderName}}" }, { "id": "app.notification.body.text.direct.full", - "translation": "{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}}/{{.Month}}" + "translation": "@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}" }, { "id": "app.notification.body.text.direct.generic", "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}}/{{.Month}}" }, + { + "id": "app.notification.body.text.group_message.full", + "translation": "Canale: {{.ChannelName}}
@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + }, + { + "id": "app.notification.body.text.group_message.generic", + "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}" + }, { "id": "app.notification.body.text.notification.full", - "translation": "CANALE: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}" + "translation": "Canale: {{.ChannelName}}
@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { "id": "app.notification.body.text.notification.generic", @@ -3804,7 +3824,15 @@ }, { "id": "app.notification.subject.direct.full", - "translation": "[{{.SiteName}}] Nuovo messaggio diretto da {{.SenderDisplayName}} il {{.Day}}/{{.Month}}/{{.Year}}" + "translation": "[{{.SiteName}}] Nuovo messaggio diretto da @{{.SenderDisplayName}} il {{.Month}} {{.Day}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{ .SiteName }}] Nuovo messaggio di gruppo in {{ .ChannelName}} del {{.Month}} {{.Day}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] Nuovo messaggio di gruppo on {{.Month}} {{.Day}}, {{.Year}}" }, { "id": "app.notification.subject.notification.full", @@ -5022,6 +5050,10 @@ "id": "model.config.is_valid.ldap_lastname", "translation": "Il campo AD/LDAP \"Last Name Attribute\" è richiesto." }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "Il campo AD/LDAP \"Login ID Attribute\" è richiesto." + }, { "id": "model.config.is_valid.ldap_max_page_size.app_error", "translation": "Valore non valido: dimensione massima pagina." @@ -5922,10 +5954,6 @@ "id": "store.sql_channel.delete.channel.app_error", "translation": "Non è possibile cancellare il canale" }, - { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "Problema nell'aggiornamento dell'ultimo aggiornamento utenti" - }, { "id": "store.sql_channel.get.existing.app_error", "translation": "Non è possibile trovare il canale" diff --git a/i18n/ja.json b/i18n/ja.json index 2458d7c09..daa007a3a 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -822,6 +822,10 @@ "id": "api.command_invite.permission.app_error", "translation": "{{.User}} を {{.Channel}} に追加する権限がありません。" }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels." + }, { "id": "api.command_invite.success", "translation": "{{.User}} がチャンネル {{.Channel}} に追加されました。" @@ -3778,6 +3782,14 @@ "id": "app.notification.body.intro.direct.generic", "translation": "{{.SenderName}} からの新しいダイレクトメッセージがあります" }, + { + "id": "app.notification.body.intro.group_message.full", + "translation": "新しいダイレクトメッセージがあります。" + }, + { + "id": "app.notification.body.intro.group_message.generic", + "translation": "{{.SenderName}} からの新しいダイレクトメッセージがあります" + }, { "id": "app.notification.body.intro.notification.full", "translation": "新しい通知があります。" @@ -3794,6 +3806,14 @@ "id": "app.notification.body.text.direct.generic", "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}" }, + { + "id": "app.notification.body.text.group_message.full", + "translation": "チャンネル: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + }, + { + "id": "app.notification.body.text.group_message.generic", + "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}" + }, { "id": "app.notification.body.text.notification.full", "translation": "チャンネル: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" @@ -3806,6 +3826,14 @@ "id": "app.notification.subject.direct.full", "translation": "[{{ .SiteName}}] {{.Month}} {{.Day}}, {{.Year}} {{.SenderDisplayName}} からの新しいダイレクトメッセージ" }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{.SiteName}}] {{.Month}} {{.Day}}, {{.Year}} {{.TeamName}}の通知" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] {{.Month}} {{.Day}}, {{.Year}} の新着通知[{{.SiteName}}] {{.Month}} {{.Day}}, {{.Year}} の新着通知" + }, { "id": "app.notification.subject.notification.full", "translation": "[{{.SiteName}}] {{.Month}} {{.Day}}, {{.Year}} {{.TeamName}}の通知" @@ -5022,6 +5050,10 @@ "id": "model.config.is_valid.ldap_lastname", "translation": "AD/LDAP項目 \"苗字(ラストネーム)の属性値\" は必須です。" }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "AD/LDAP項目 \"ID属性値\" は必須です。" + }, { "id": "model.config.is_valid.ldap_max_page_size.app_error", "translation": "最大ページサイズの値が不正です。" @@ -5922,10 +5954,6 @@ "id": "store.sql_channel.delete.channel.app_error", "translation": "チャンネルを削除できませんでした" }, - { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "メンバーの最終更新時刻の更新に問題があります" - }, { "id": "store.sql_channel.get.existing.app_error", "translation": "チャンネルが見付かりませんでした" diff --git a/i18n/ko.json b/i18n/ko.json index cd5e8b9dd..d9c06ffca 100644 --- a/i18n/ko.json +++ b/i18n/ko.json @@ -822,6 +822,10 @@ "id": "api.command_invite.permission.app_error", "translation": "{{.Channel}} 에 {{.User}}를 추가할 권한이 없습니다." }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels." + }, { "id": "api.command_invite.success", "translation": "{{.Channel}} 채널에 {{.User}} 가 추가되었습니다." @@ -3776,7 +3780,15 @@ }, { "id": "app.notification.body.intro.direct.generic", - "translation": "You have a new direct message from {{.SenderName}}" + "translation": "You have a new Direct Message from @{{.SenderName}}" + }, + { + "id": "app.notification.body.intro.group_message.full", + "translation": "한개의 신규 메시지가 있습니다." + }, + { + "id": "app.notification.body.intro.group_message.generic", + "translation": "You have a new Group Message from @{{.SenderName}}" }, { "id": "app.notification.body.intro.notification.full", @@ -3784,7 +3796,7 @@ }, { "id": "app.notification.body.intro.notification.generic", - "translation": "You have a new notification from {{.SenderName}}" + "translation": "You have a new notification from @{{.SenderName}}" }, { "id": "app.notification.body.text.direct.full", @@ -3794,6 +3806,14 @@ "id": "app.notification.body.text.direct.generic", "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}" }, + { + "id": "app.notification.body.text.group_message.full", + "translation": "채널: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + }, + { + "id": "app.notification.body.text.group_message.generic", + "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}" + }, { "id": "app.notification.body.text.notification.full", "translation": "채널: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" @@ -3806,9 +3826,17 @@ "id": "app.notification.subject.direct.full", "translation": "[{{.SiteName}}] {{.SenderDisplayName}} (으)로부터 {{.Month}} {{.Day}}, {{.Year}} 에 새로운 개인 메시지가 왔습니다." }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{.SiteName}}] {{.SenderDisplayName}} (으)로부터 {{.Month}} {{.Day}}, {{.Year}} 에 새로운 개인 메시지가 왔습니다." + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] 새 알림 {{.Month}} {{.Day}}, {{.Year}}" + }, { "id": "app.notification.subject.notification.full", - "translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}" + "translation": "[{{.SiteName}}] {{.SenderDisplayName}} (으)로부터 {{.Month}} {{.Day}}, {{.Year}} 에 새로운 개인 메시지가 왔습니다." }, { "id": "app.plugin.activate.app_error", @@ -5022,6 +5050,10 @@ "id": "model.config.is_valid.ldap_lastname", "translation": "AD/LDAP의 \"Last Name Attribute\" 항목이 필요합니다." }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "AD/LDAP의 \"ID Attribute\" 항목이 필요합니다." + }, { "id": "model.config.is_valid.ldap_max_page_size.app_error", "translation": "Invalid max page size value." @@ -5922,10 +5954,6 @@ "id": "store.sql_channel.delete.channel.app_error", "translation": "채널을 삭제하지 못했습니다." }, - { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "Problem updating members last updated time" - }, { "id": "store.sql_channel.get.existing.app_error", "translation": "존재하는 채널을 찾지 못했습니다." diff --git a/i18n/nl.json b/i18n/nl.json index 1294dae85..14edb8d66 100644 --- a/i18n/nl.json +++ b/i18n/nl.json @@ -822,6 +822,10 @@ "id": "api.command_invite.permission.app_error", "translation": "You don't have enough permissions to add {{.User}} in {{.Channel}}." }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels." + }, { "id": "api.command_invite.success", "translation": "{{.User}} added to {{.Channel}} channel." @@ -3776,7 +3780,15 @@ }, { "id": "app.notification.body.intro.direct.generic", - "translation": "You have a new direct message from {{.SenderName}}" + "translation": "You have a new Direct Message from @{{.SenderName}}" + }, + { + "id": "app.notification.body.intro.group_message.full", + "translation": "U heeft een nieuw bericht." + }, + { + "id": "app.notification.body.intro.group_message.generic", + "translation": "You have a new Group Message from @{{.SenderName}}" }, { "id": "app.notification.body.intro.notification.full", @@ -3784,7 +3796,7 @@ }, { "id": "app.notification.body.intro.notification.generic", - "translation": "You have a new notification from {{.SenderName}}" + "translation": "You have a new notification from @{{.SenderName}}" }, { "id": "app.notification.body.text.direct.full", @@ -3794,6 +3806,14 @@ "id": "app.notification.body.text.direct.generic", "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}" }, + { + "id": "app.notification.body.text.group_message.full", + "translation": "KANAAL: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + }, + { + "id": "app.notification.body.text.group_message.generic", + "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}" + }, { "id": "app.notification.body.text.notification.full", "translation": "KANAAL: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" @@ -3806,9 +3826,17 @@ "id": "app.notification.subject.direct.full", "translation": "[{{.SiteName}}] Nieuw direct bericht van {{.SenderDisplayName}} op {{.Month}} {{.Day}} {{.Year}}" }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{.SiteName}}] Nieuw direct bericht van {{.SenderDisplayName}} op {{.Month}} {{.Day}} {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] Nieuwe Notificatie voor {{.Month}} {{.Day}}, {{.Year}}" + }, { "id": "app.notification.subject.notification.full", - "translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}" + "translation": "[{{.SiteName}}] Nieuw direct bericht van {{.SenderDisplayName}} op {{.Month}} {{.Day}} {{.Year}}" }, { "id": "app.plugin.activate.app_error", @@ -5022,6 +5050,10 @@ "id": "model.config.is_valid.ldap_lastname", "translation": "AD/LDAP veld \"Last Name Attribute\" is verplicht." }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "AD/LDAP veld \"ID Attribute\" is verplicht." + }, { "id": "model.config.is_valid.ldap_max_page_size.app_error", "translation": "Ongeldige max page size waarde." @@ -5922,10 +5954,6 @@ "id": "store.sql_channel.delete.channel.app_error", "translation": "Het kanaal kan niet verwijderd worden" }, - { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "Probleem bij het bijwerken van de leden laatst bijgewerkte tijd" - }, { "id": "store.sql_channel.get.existing.app_error", "translation": "Het kanaal kon niet gevonden worden" diff --git a/i18n/pl.json b/i18n/pl.json index 3f03eabb6..aa95466a5 100644 --- a/i18n/pl.json +++ b/i18n/pl.json @@ -822,6 +822,10 @@ "id": "api.command_invite.permission.app_error", "translation": "You don't have enough permissions to add {{.User}} in {{.Channel}}." }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels." + }, { "id": "api.command_invite.success", "translation": "{{.User}} added to {{.Channel}} channel." @@ -3776,7 +3780,15 @@ }, { "id": "app.notification.body.intro.direct.generic", - "translation": "You have a new direct message from {{.SenderName}}" + "translation": "You have a new Direct Message from @{{.SenderName}}" + }, + { + "id": "app.notification.body.intro.group_message.full", + "translation": "Masz nową wiadomość." + }, + { + "id": "app.notification.body.intro.group_message.generic", + "translation": "You have a new Group Message from @{{.SenderName}}" }, { "id": "app.notification.body.intro.notification.full", @@ -3784,7 +3796,7 @@ }, { "id": "app.notification.body.intro.notification.generic", - "translation": "You have a new notification from {{.SenderName}}" + "translation": "You have a new notification from @{{.SenderName}}" }, { "id": "app.notification.body.text.direct.full", @@ -3794,6 +3806,14 @@ "id": "app.notification.body.text.direct.generic", "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}" }, + { + "id": "app.notification.body.text.group_message.full", + "translation": "KANAŁ: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + }, + { + "id": "app.notification.body.text.group_message.generic", + "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}" + }, { "id": "app.notification.body.text.notification.full", "translation": "KANAŁ: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" @@ -3806,9 +3826,17 @@ "id": "app.notification.subject.direct.full", "translation": "Nowa wiadomość grupowa od {{ .SenderDisplayName}} {{.Month}} {{.Day}}, {{.Year}}" }, + { + "id": "app.notification.subject.group_message.full", + "translation": "Nowa wiadomość grupowa od {{ .SenderDisplayName}} {{.Month}} {{.Day}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "Nowe powiadomienie [{{.SiteName}}] z {{.Month}} {{.Day}}, {{.Year}}" + }, { "id": "app.notification.subject.notification.full", - "translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}" + "translation": "Nowa wiadomość grupowa od {{ .SenderDisplayName}} {{.Month}} {{.Day}}, {{.Year}}" }, { "id": "app.plugin.activate.app_error", @@ -5022,6 +5050,10 @@ "id": "model.config.is_valid.ldap_lastname", "translation": "Pole AD/LDAP \"Last Name Attribute\" jest wymagane." }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "Pole AD/LDAP \"ID Attribute\" jest wymagane." + }, { "id": "model.config.is_valid.ldap_max_page_size.app_error", "translation": "Nieprawidłowy maksymalny rozmiar strony." @@ -5922,10 +5954,6 @@ "id": "store.sql_channel.delete.channel.app_error", "translation": "Nie możemy usunąć kanału" }, - { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "Problem podczas aktualizowania daty ostatniej aktualizacji uczestników" - }, { "id": "store.sql_channel.get.existing.app_error", "translation": "Nie mogliśmy znaleźć istniejącego kanału" diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json index 290bea915..5b299756a 100644 --- a/i18n/pt-BR.json +++ b/i18n/pt-BR.json @@ -822,6 +822,10 @@ "id": "api.command_invite.permission.app_error", "translation": "Você não tem permissão suficiente para adicionar {{.User}} em {{.Channel}}." }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "Não foi possível encontrar o canal {{.Channel}}. Por favor utilize o identificador de canais para descobrir canais." + }, { "id": "api.command_invite.success", "translation": "{{.User}} adicionado ao canal {{.Channel}}." @@ -3772,11 +3776,19 @@ }, { "id": "app.notification.body.intro.direct.full", - "translation": "Você tem uma nova mensagem direta." + "translation": "Você tem uma nova Mensagem Direta." }, { "id": "app.notification.body.intro.direct.generic", - "translation": "Você tem uma nova mensagem direta de {{.SenderName}}" + "translation": "Você tem uma nova Mensagem Direta de @{{.SenderName}}" + }, + { + "id": "app.notification.body.intro.group_message.full", + "translation": "Você tem uma nova Mensagem de Grupo." + }, + { + "id": "app.notification.body.intro.group_message.generic", + "translation": "Você tem uma nova Mensagem de Grupo de @{{.SenderName}}" }, { "id": "app.notification.body.intro.notification.full", @@ -3784,19 +3796,27 @@ }, { "id": "app.notification.body.intro.notification.generic", - "translation": "Você tem uma nova notificação de {{.SenderName}}" + "translation": "Você tem uma nova notificação de @{{.SenderName}}" }, { "id": "app.notification.body.text.direct.full", - "translation": "{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}" + "translation": "@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}" }, { "id": "app.notification.body.text.direct.generic", "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}} {{.Month}}" }, + { + "id": "app.notification.body.text.group_message.full", + "translation": "Canal: {{.ChannelName}}
@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}" + }, + { + "id": "app.notification.body.text.group_message.generic", + "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Day}} {{.Month}}" + }, { "id": "app.notification.body.text.notification.full", - "translation": "CANAL: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}" + "translation": "Canal: {{.ChannelName}}
@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Day}} {{.Month}}" }, { "id": "app.notification.body.text.notification.generic", @@ -3804,7 +3824,15 @@ }, { "id": "app.notification.subject.direct.full", - "translation": "[{{.SiteName}}] Nova Mensagem Direta de {{.SenderDisplayName}} em {{.Day}} {{.Month}}, {{.Year}}" + "translation": "[{{.SiteName}}] Nova Mensagem Direta de @{{.SenderDisplayName}} em {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{ .SiteName }}] Nova Mensagem do Grupo {{ .ChannelName}} em {{.Day}} {{.Month}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] Nova Mensagem de Grupo em {{.Day}} {{.Month}}, {{.Year}}" }, { "id": "app.notification.subject.notification.full", @@ -5022,6 +5050,10 @@ "id": "model.config.is_valid.ldap_lastname", "translation": "O campo \"Last Name Attribute\" do AD/LDAP é requerido." }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "O campo \"Login ID Attribute\" do AD/LDAP é obrigatório." + }, { "id": "model.config.is_valid.ldap_max_page_size.app_error", "translation": "Valor do tamanho de página máximo inválido." @@ -5922,10 +5954,6 @@ "id": "store.sql_channel.delete.channel.app_error", "translation": "Não foi possível deletar o canal" }, - { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "Problema ao atualizar membros na última atualização" - }, { "id": "store.sql_channel.get.existing.app_error", "translation": "Não foi possível encontrar o canal existente" diff --git a/i18n/ru.json b/i18n/ru.json index 445d4a9f9..e34d1b8b0 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -822,6 +822,10 @@ "id": "api.command_invite.permission.app_error", "translation": "У вас недостаточно прав для добавления {{.User}} в {{.Channel}}." }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels." + }, { "id": "api.command_invite.success", "translation": "{{.User}} добавлен в канал {{.Channel}}." @@ -3776,7 +3780,15 @@ }, { "id": "app.notification.body.intro.direct.generic", - "translation": "You have a new direct message from {{.SenderName}}" + "translation": "You have a new Direct Message from @{{.SenderName}}" + }, + { + "id": "app.notification.body.intro.group_message.full", + "translation": "У вас есть новое личное сообщение." + }, + { + "id": "app.notification.body.intro.group_message.generic", + "translation": "You have a new Group Message from @{{.SenderName}}" }, { "id": "app.notification.body.intro.notification.full", @@ -3784,7 +3796,7 @@ }, { "id": "app.notification.body.intro.notification.generic", - "translation": "You have a new notification from {{.SenderName}}" + "translation": "You have a new notification from @{{.SenderName}}" }, { "id": "app.notification.body.text.direct.full", @@ -3794,6 +3806,14 @@ "id": "app.notification.body.text.direct.generic", "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, + { + "id": "app.notification.body.text.group_message.full", + "translation": "КАНАЛ: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + }, + { + "id": "app.notification.body.text.group_message.generic", + "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + }, { "id": "app.notification.body.text.notification.full", "translation": "КАНАЛ: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" @@ -3806,9 +3826,17 @@ "id": "app.notification.subject.direct.full", "translation": "[{{.SiteName}}] Новое личное сообщение от {{.SenderDisplayName}} в {{.Month}} {{.Day}}, {{.Year}}" }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{.SiteName}}] Новое личное сообщение от {{.SenderDisplayName}} в {{.Month}} {{.Day}}, {{.Year}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] Новое уведомление за {{.Day}} {{.Month}}, {{.Year}}" + }, { "id": "app.notification.subject.notification.full", - "translation": "[{{ .SiteName }}] Notification in {{ .TeamName}} on {{.Month}} {{.Day}}, {{.Year}}" + "translation": "[{{.SiteName}}] Новое личное сообщение от {{.SenderDisplayName}} в {{.Month}} {{.Day}}, {{.Year}}" }, { "id": "app.plugin.activate.app_error", @@ -5022,6 +5050,10 @@ "id": "model.config.is_valid.ldap_lastname", "translation": "Требуется поле AD/LDAP \"Last Name Attribute\"." }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "Требуется поле AD/LDAP \"ID Attribute\"." + }, { "id": "model.config.is_valid.ldap_max_page_size.app_error", "translation": "Неверное значение максимального размера страницы." @@ -5922,10 +5954,6 @@ "id": "store.sql_channel.delete.channel.app_error", "translation": "Неудачная попытка удалить канал" }, - { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "Проблема с обновлением времени последнего входа участника" - }, { "id": "store.sql_channel.get.existing.app_error", "translation": "Не удалось найти существующий канал" diff --git a/i18n/tr.json b/i18n/tr.json index 852bebad2..091f358ef 100644 --- a/i18n/tr.json +++ b/i18n/tr.json @@ -788,7 +788,7 @@ }, { "id": "api.command_invite.channel.error", - "translation": "{{.Channel}} kanalı belirlenemedi. Lütfen kanalları belirtmek için [channel handle] kullanın (https://about.mattermost.com/default-channel-handle-documentation)." + "translation": "{{.Channel}} kanalı belirlenemedi. Lütfen kanalları belirtmek için [kanal kısaltması] kullanın (https://about.mattermost.com/default-channel-handle-documentation)." }, { "id": "api.command_invite.desc", @@ -822,6 +822,10 @@ "id": "api.command_invite.permission.app_error", "translation": "{{.User}} kullanıcısını {{.Channel}} kanalına eklemek için yeterli izinleriniz yok." }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "{{.Channel}} kanalı bulunamadı. Lütfen kanalları belirtmek için kanal kısaltması kullanın." + }, { "id": "api.command_invite.success", "translation": "{{.User}} kullanıcısı {{.Channel}} kanalına eklendi." @@ -948,7 +952,7 @@ }, { "id": "api.command_mute.error", - "translation": "{{.Channel}} kanalı belirlenemedi. Lütfen kanalları belirtmek için [channel handle](https://about.mattermost.com/default-channel-handle-documentation) kullanın." + "translation": "{{.Channel}} kanalı belirlenemedi. Lütfen kanalları belirtmek için [kanal kısaltması](https://about.mattermost.com/default-channel-handle-documentation) kullanın." }, { "id": "api.command_mute.hint", @@ -960,7 +964,7 @@ }, { "id": "api.command_mute.no_channel.error", - "translation": "Belirtilen kanal bulunamadı. Lütfen kanalları belirtmek için [channel handle] kullanın (https://about.mattermost.com/default-channel-handle-documentation)." + "translation": "Belirtilen kanal bulunamadı. Lütfen kanalları belirtmek için [kanal kısaltması] kullanın (https://about.mattermost.com/default-channel-handle-documentation)." }, { "id": "api.command_mute.not_member.error", @@ -3772,11 +3776,19 @@ }, { "id": "app.notification.body.intro.direct.full", - "translation": "Yeni bir doğrudan iletiniz var." + "translation": "Yeni bir Doğrudan İletiniz var." }, { "id": "app.notification.body.intro.direct.generic", - "translation": "{{.SenderName}} tarafından gönderilen yeni bir doğrudan iletiniz var." + "translation": "@{{.SenderName}} tarafından gönderilen yeni bir Doğrudan İletiniz var." + }, + { + "id": "app.notification.body.intro.group_message.full", + "translation": "Yeni bir Grup İletiniz var." + }, + { + "id": "app.notification.body.intro.group_message.generic", + "translation": "@{{.SenderName}} tarafından gönderilen yeni bir Grup İletiniz var." }, { "id": "app.notification.body.intro.notification.full", @@ -3784,19 +3796,27 @@ }, { "id": "app.notification.body.intro.notification.generic", - "translation": "{{.SenderName}} tarafından gönderilen yeni bir bildiriminiz var." + "translation": "@{{.SenderName}} tarafından gönderilen yeni bir bildiriminiz var." }, { "id": "app.notification.body.text.direct.full", - "translation": "{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + "translation": "@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { "id": "app.notification.body.text.direct.generic", "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, + { + "id": "app.notification.body.text.group_message.full", + "translation": "Kanal: {{.ChannelName}}
@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + }, + { + "id": "app.notification.body.text.group_message.generic", + "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + }, { "id": "app.notification.body.text.notification.full", - "translation": "KANAL: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + "translation": "Kanal: {{.ChannelName}}
@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { "id": "app.notification.body.text.notification.generic", @@ -3804,7 +3824,15 @@ }, { "id": "app.notification.subject.direct.full", - "translation": "[{{.SiteName}}] {{ .SenderDisplayName}} tarafından {{.Month}} {{.Day}}, {{.Year}} tarihinde yeni doğrudan ileti" + "translation": "[{{.SiteName}}] @{{ .SenderDisplayName}} tarafından {{.Month}} {{.Day}}, {{.Year}} tarihinde yeni bir Doğrudan İleti gönderildi" + }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{ .SiteName }}] {{ .ChannelName}} grubuna {{.Month}} {{.Day}}, {{.Year}} tarihinde yeni bir Grup İletisi gönderildi" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] sitesinde {{.Month}} {{.Day}}, {{.Year}} tarihinde yeni bir Grup İletisi gönderildi" }, { "id": "app.notification.subject.notification.full", @@ -5022,6 +5050,10 @@ "id": "model.config.is_valid.ldap_lastname", "translation": "\"Soyad Özniteliği\" AD/LDAP alanı zorunludur." }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "\"Oturum Açma Kodu Özniteliği\" AD/LDAP alanı zorunludur." + }, { "id": "model.config.is_valid.ldap_max_page_size.app_error", "translation": "En büyük sayfa boyutu değeri geçersiz." @@ -5922,10 +5954,6 @@ "id": "store.sql_channel.delete.channel.app_error", "translation": "Kanal silinemedi" }, - { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "Üyelerin son güncellenme zamanları güncellenirken sorun çıktı" - }, { "id": "store.sql_channel.get.existing.app_error", "translation": "Var olan kanal bulunamadı" diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json index 4c9a11deb..43825d869 100644 --- a/i18n/zh-CN.json +++ b/i18n/zh-CN.json @@ -822,6 +822,10 @@ "id": "api.command_invite.permission.app_error", "translation": "您没有足够的权限在 {{.Channel}} 添加 {{.User}}。" }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "无法找到频道 {{.Channel}}。请使用频道识别查找频道。" + }, { "id": "api.command_invite.success", "translation": "已添加 {{.User}} 到 {{.Channel}} 频道。" @@ -3772,11 +3776,19 @@ }, { "id": "app.notification.body.intro.direct.full", - "translation": "你有一条新消息。" + "translation": "你有一条新私信。" }, { "id": "app.notification.body.intro.direct.generic", - "translation": "您有来自 {{.SenderName}} 的新私信" + "translation": "您有来自 @{{.SenderName}} 的新私信" + }, + { + "id": "app.notification.body.intro.group_message.full", + "translation": "你有一条新团体消息。" + }, + { + "id": "app.notification.body.intro.group_message.generic", + "translation": "您有来自 @{{.SenderName}} 的新团体消息" }, { "id": "app.notification.body.intro.notification.full", @@ -3784,19 +3796,27 @@ }, { "id": "app.notification.body.intro.notification.generic", - "translation": "您有来自 {{.SenderName}} 的新通知" + "translation": "您有来自 @{{.SenderName}} 的新通知" }, { "id": "app.notification.body.text.direct.full", - "translation": "{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + "translation": "@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { "id": "app.notification.body.text.direct.generic", "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}" }, + { + "id": "app.notification.body.text.group_message.full", + "translation": "频道:{{.ChannelName}}
@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + }, + { + "id": "app.notification.body.text.group_message.generic", + "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}" + }, { "id": "app.notification.body.text.notification.full", - "translation": "频道: {{.ChannelName}}
{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + "translation": "频道:{{.ChannelName}}
@{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" }, { "id": "app.notification.body.text.notification.generic", @@ -3804,7 +3824,15 @@ }, { "id": "app.notification.subject.direct.full", - "translation": "[{{.SiteName}}] 来自 {{ .SenderDisplayName}} 的新私信消息于 {{.Month}} {{.Day}}, {{.Year}}" + "translation": "[{{.SiteName}}] 来自 @{{ .SenderDisplayName}} 于 {{.Month}} {{.Day}}, {{.Year}} 发送的新私信消息" + }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{ .SiteName }}] 在 [{{ .SiteName }}] 于 {{.Month}} {{.Day}}, {{.Year}} 的新团体消息" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] 于 {{.Month}} {{.Day}}, {{.Year}} 的新团体消息" }, { "id": "app.notification.subject.notification.full", @@ -5022,6 +5050,10 @@ "id": "model.config.is_valid.ldap_lastname", "translation": "AD/LDAP 栏 \"姓氏\" 为必填。" }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "AD/LDAP 栏 \"登入 ID 属性\" 为必填。" + }, { "id": "model.config.is_valid.ldap_max_page_size.app_error", "translation": "无效的最大页面值。" @@ -5196,7 +5228,7 @@ }, { "id": "model.config.is_valid.site_url.app_error", - "translation": "站点网址必须为有效URL并且以 http:// 或 https:// 开头" + "translation": "站点网址必须为有效的 URL 并以 http:// 或 https:// 开头" }, { "id": "model.config.is_valid.site_url_email_batching.app_error", @@ -5922,10 +5954,6 @@ "id": "store.sql_channel.delete.channel.app_error", "translation": "我们无法删除频道" }, - { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "更新成员上次更新时间出现问题" - }, { "id": "store.sql_channel.get.existing.app_error", "translation": "我们找不到现有的频道" diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json index c76f097ba..82ec3587e 100644 --- a/i18n/zh-TW.json +++ b/i18n/zh-TW.json @@ -822,6 +822,10 @@ "id": "api.command_invite.permission.app_error", "translation": "沒有足夠的權限將 {{.User}} 新增至 {{.Channel}}。" }, + { + "id": "api.command_invite.private_channel.app_error", + "translation": "Could not find the channel {{.Channel}}. Please use the channel handle to identify channels." + }, { "id": "api.command_invite.success", "translation": "已將 {{.User}} 新增至 {{.Channel}} 頻道。" @@ -3778,6 +3782,14 @@ "id": "app.notification.body.intro.direct.generic", "translation": "您有 1 筆新的直接傳訊,來自{{.SenderName}}。" }, + { + "id": "app.notification.body.intro.group_message.full", + "translation": "您有 1 筆新的直接傳訊。" + }, + { + "id": "app.notification.body.intro.group_message.generic", + "translation": "您有 1 筆新的直接傳訊,來自{{.SenderName}}。" + }, { "id": "app.notification.body.intro.notification.full", "translation": "您有一筆新通知。" @@ -3794,6 +3806,14 @@ "id": "app.notification.body.text.direct.generic", "translation": "{{.Month}} {{.Day}},{{.Hour}}:{{.Minute}} {{.Timezone}}" }, + { + "id": "app.notification.body.text.group_message.full", + "translation": "頻道:{{.ChannelName}}
{{.SenderName}} - {{.Month}} {{.Day}},{{.Hour}}:{{.Minute}} {{.Timezone}}" + }, + { + "id": "app.notification.body.text.group_message.generic", + "translation": "{{.Month}} {{.Day}},{{.Hour}}:{{.Minute}} {{.Timezone}}" + }, { "id": "app.notification.body.text.notification.full", "translation": "頻道:{{.ChannelName}}
{{.SenderName}} - {{.Month}} {{.Day}},{{.Hour}}:{{.Minute}} {{.Timezone}}" @@ -3806,6 +3826,14 @@ "id": "app.notification.subject.direct.full", "translation": "[{{.SiteName}}] 來自 {{.SenderDisplayName}} 的直接傳訊,發於 {{.Year}} {{.Month}} {{.Day}}" }, + { + "id": "app.notification.subject.group_message.full", + "translation": "[{{.SiteName}}] 來自 {{.TeamName}} 的通知,發於 {{.Year}} {{.Month}} {{.Day}}" + }, + { + "id": "app.notification.subject.group_message.generic", + "translation": "[{{.SiteName}}] {{.Year}} {{.Month}} {{.Day}} 的新通知[{{.SiteName}}] {{.Year}} {{.Month}} {{.Day}} 的新通知" + }, { "id": "app.notification.subject.notification.full", "translation": "[{{.SiteName}}] 來自 {{.TeamName}} 的通知,發於 {{.Year}} {{.Month}} {{.Day}}" @@ -5022,6 +5050,10 @@ "id": "model.config.is_valid.ldap_lastname", "translation": "AD/LDAP 欄位 \"姓氏屬性\" 為必須欄位。" }, + { + "id": "model.config.is_valid.ldap_login_id", + "translation": "AD/LDAP 欄位 \"ID 的屬性\" 為必須欄位。" + }, { "id": "model.config.is_valid.ldap_max_page_size.app_error", "translation": "無效的最大分頁大小。" @@ -5922,10 +5954,6 @@ "id": "store.sql_channel.delete.channel.app_error", "translation": "無法刪除頻道" }, - { - "id": "store.sql_channel.extra_updated.app_error", - "translation": "更新成員的最後更新時間時遇到問題" - }, { "id": "store.sql_channel.get.existing.app_error", "translation": "找不到現有的頻道" -- cgit v1.2.3-1-g7c22