Skip to content

Commit e5c8e6c

Browse files
kimurakazuhiro-cs35560claude
authored
chore(subscription): localize slack notification messages (#2778)
* chore: translate the slack notification The feature stale, experiment running and MAU count notifications were sent in Japanese regardless of the recipient's language setting. Only the domain event notification honored it. Move the hardcoded Japanese strings into the locale bundle and pass the existing localizer (built from Recipient.Language in notify()) down to the three remaining attachment builders. This also covers the days-left line of the experiment list, which was inlined in slack.go rather than in message.go. message.go is removed since localizedMessage() has no callers left. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(locale): add Template suffix to templated notification constants The Template suffix marks message IDs that must be resolved with MustLocalizeWithTemplate. Every other templated constant in this file follows it, so align the three subscription notification keys that take a template field. NotificationExperimentRunning keeps no suffix since it has no placeholder and is resolved with MustLocalize. Only the Go identifiers change; the message IDs and the localized data are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(locale): use count-neutral wording for the experiment days-left message Address review comments on PR #2778: - "{{ .Field_1 }} days left" rendered "`1` days left" for an experiment with exactly one day remaining. Reword it as "Days left: {{ .Field_1 }}" so the English notification is grammatical for singular, plural, zero and negative values, and update the test expectation accordingly. The Japanese message is already count-neutral and stays unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: s35560 <s35560@cyberagent.email> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 4b42fc6 commit e5c8e6c

6 files changed

Lines changed: 193 additions & 87 deletions

File tree

pkg/locale/localizedata/en.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,13 @@ ValueUpdated: "{{ .Field_1 }} value has been updated"
149149
Cancelled: "{{ .Field_1 }} has been cancelled"
150150
Failed: "{{ .Field_1 }} has failed"
151151
Skipped: "{{ .Field_1 }} has been skipped"
152-
AppliedNow: "{{ .Field_1 }} has been applied immediately"
152+
AppliedNow: "{{ .Field_1 }} has been applied immediately"
153+
154+
#############################
155+
# Subscription Notifications
156+
#############################
157+
158+
NotificationFeatureStale: "There are feature flags that have not been used for more than {{ .Field_1 }} days."
159+
NotificationExperimentRunning: "There are running experiments."
160+
NotificationExperimentDaysLeft: "Days left: {{ .Field_1 }}"
161+
NotificationMAUCount: "This is the MAU for month {{ .Field_1 }}."

pkg/locale/localizedata/ja.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,13 @@ ValueUpdated: "{{ .Field_1 }}の値が更新されました"
149149
Cancelled: "{{ .Field_1 }}がキャンセルされました"
150150
Failed: "{{ .Field_1 }}が失敗しました"
151151
Skipped: "{{ .Field_1 }}がスキップされました"
152-
AppliedNow: "{{ .Field_1 }}が即時適用されました"
152+
AppliedNow: "{{ .Field_1 }}が即時適用されました"
153+
154+
#############################
155+
# Subscription Notifications
156+
#############################
157+
158+
NotificationFeatureStale: "{{ .Field_1 }}日以上使用されていないフィーチャーフラグがあります。"
159+
NotificationExperimentRunning: "実行中のエクスペリメントがあります。"
160+
NotificationExperimentDaysLeft: "残り {{ .Field_1 }} 日"
161+
NotificationMAUCount: "{{ .Field_1 }}月のMAUです。"

pkg/locale/localizer.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ const (
173173
AppliedNowTemplate = "AppliedNow"
174174
)
175175

176+
// subscription notifications
177+
const (
178+
NotificationFeatureStaleTemplate = "NotificationFeatureStale"
179+
NotificationExperimentRunning = "NotificationExperimentRunning"
180+
NotificationExperimentDaysLeftTemplate = "NotificationExperimentDaysLeft"
181+
NotificationMAUCountTemplate = "NotificationMAUCount"
182+
)
183+
176184
func init() {
177185
bundle = i18n.NewBundle(language.English)
178186
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)

pkg/subscription/sender/notifier/message.go

Lines changed: 0 additions & 62 deletions
This file was deleted.

pkg/subscription/sender/notifier/slack.go

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"encoding/json"
2020
"errors"
2121
"fmt"
22+
"strconv"
2223
"time"
2324

2425
"github.com/slack-go/slack"
@@ -174,11 +175,11 @@ func (n *slackNotifier) createAttachment(
174175
case senderproto.Notification_DomainEvent:
175176
return n.createDomainEventAttachment(notification.DomainEventNotification, localizer)
176177
case senderproto.Notification_FeatureStale:
177-
return n.createFeatureStaleAttachment(notification.FeatureStaleNotification)
178+
return n.createFeatureStaleAttachment(notification.FeatureStaleNotification, localizer)
178179
case senderproto.Notification_ExperimentRunning:
179-
return n.createExperimentRunningAttachment(notification.ExperimentRunningNotification)
180+
return n.createExperimentRunningAttachment(notification.ExperimentRunningNotification, localizer)
180181
case senderproto.Notification_MauCount:
181-
return n.createMAUCountAttachment(notification.MauCountNotification)
182+
return n.createMAUCountAttachment(notification.MauCountNotification, localizer)
182183
case senderproto.Notification_DemoOrganizationCreation:
183184
return n.createDemoOrganizationCreationAttachment(notification.DemoOrganizationCreationNotification)
184185
}
@@ -257,6 +258,7 @@ func (n *slackNotifier) createDomainEventAttachment(
257258

258259
func (n *slackNotifier) createFeatureStaleAttachment(
259260
notification *senderproto.FeatureStaleNotification,
261+
localizer locale.Localizer,
260262
) (*slack.Attachment, error) {
261263
featureListMsg := ""
262264
for _, feature := range notification.Features {
@@ -272,16 +274,14 @@ func (n *slackNotifier) createFeatureStaleAttachment(
272274
newLine := "- ID: `" + feature.Id + "`, Name: *" + fmt.Sprintf(linkTemplate, url, feature.Name) + "*\n"
273275
featureListMsg = featureListMsg + newLine
274276
}
275-
// handle loc if multi-lang is necessary
276-
msg, err := localizedMessage(msgTypeFeatureStale, locale.Ja)
277-
if err != nil {
278-
return nil, err
279-
}
280-
replacedMsg := fmt.Sprintf(msg.Message, featuredomain.SecondsToStale/24/60/60)
277+
msg := localizer.MustLocalizeWithTemplate(
278+
locale.NotificationFeatureStaleTemplate,
279+
strconv.Itoa(featuredomain.SecondsToStale/24/60/60),
280+
)
281281
attachment := &slack.Attachment{
282282
Color: "#F4D03F",
283283
MarkdownIn: []string{"text"},
284-
Text: replacedMsg + "\n\n" +
284+
Text: msg + "\n\n" +
285285
"Environment: " + notification.EnvironmentName + "\n\n" +
286286
"Feature flags: \n\n" +
287287
featureListMsg,
@@ -291,6 +291,7 @@ func (n *slackNotifier) createFeatureStaleAttachment(
291291

292292
func (n *slackNotifier) createExperimentRunningAttachment(
293293
notification *senderproto.ExperimentRunningNotification,
294+
localizer locale.Localizer,
294295
) (*slack.Attachment, error) {
295296
listMsg := ""
296297
now := time.Now()
@@ -305,18 +306,18 @@ func (n *slackNotifier) createExperimentRunningAttachment(
305306
return nil, err
306307
}
307308
nameLink := fmt.Sprintf(linkTemplate, url, e.Name)
308-
newLine := fmt.Sprintf("- 残り `%d` 日, Name: *%s*\n", lastDays(now, time.Unix(e.StopAt, 0)), nameLink)
309+
daysLeft := localizer.MustLocalizeWithTemplate(
310+
locale.NotificationExperimentDaysLeftTemplate,
311+
fmt.Sprintf("`%d`", lastDays(now, time.Unix(e.StopAt, 0))),
312+
)
313+
newLine := fmt.Sprintf("- %s, Name: *%s*\n", daysLeft, nameLink)
309314
listMsg = listMsg + newLine
310315
}
311-
// handle loc if multi-lang is necessary
312-
msg, err := localizedMessage(msgTypeExperimentResult, locale.Ja)
313-
if err != nil {
314-
return nil, err
315-
}
316+
msg := localizer.MustLocalize(locale.NotificationExperimentRunning)
316317
attachment := &slack.Attachment{
317318
Color: "#3498DB",
318319
MarkdownIn: []string{"text"},
319-
Text: msg.Message + "\n\n" +
320+
Text: msg + "\n\n" +
320321
"Environment: " + notification.EnvironmentName + "\n\n" +
321322
"Experiments: \n\n" +
322323
listMsg,
@@ -326,17 +327,17 @@ func (n *slackNotifier) createExperimentRunningAttachment(
326327

327328
func (n *slackNotifier) createMAUCountAttachment(
328329
notification *senderproto.MauCountNotification,
330+
localizer locale.Localizer,
329331
) (*slack.Attachment, error) {
330-
msg, err := localizedMessage(msgTypeMAUCount, locale.Ja)
331-
if err != nil {
332-
return nil, err
333-
}
334-
replacedMsg := fmt.Sprintf(msg.Message, notification.Month)
332+
msg := localizer.MustLocalizeWithTemplate(
333+
locale.NotificationMAUCountTemplate,
334+
strconv.Itoa(int(notification.Month)),
335+
)
335336
p := message.NewPrinter(language.English)
336337
attachment := &slack.Attachment{
337338
Color: "#3498DB",
338339
MarkdownIn: []string{"text"},
339-
Text: replacedMsg + "\n\n" +
340+
Text: msg + "\n\n" +
340341
"Environment: " + notification.EnvironmentName + "\n" +
341342
p.Sprintf("Event count: %d", notification.EventCount) + "\n" +
342343
p.Sprintf("User count: %d", notification.UserCount),

pkg/subscription/sender/notifier/slack_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,19 @@ package notifier
1616

1717
import (
1818
"context"
19+
"strconv"
1920
"testing"
2021
"time"
2122

2223
"github.com/stretchr/testify/assert"
2324
"go.uber.org/zap"
2425
"google.golang.org/grpc/metadata"
2526

27+
featuredomain "github.com/bucketeer-io/bucketeer/v2/pkg/feature/domain"
2628
"github.com/bucketeer-io/bucketeer/v2/pkg/locale"
2729
domainproto "github.com/bucketeer-io/bucketeer/v2/proto/event/domain"
30+
experimentproto "github.com/bucketeer-io/bucketeer/v2/proto/experiment"
31+
featureproto "github.com/bucketeer-io/bucketeer/v2/proto/feature"
2832
senderproto "github.com/bucketeer-io/bucketeer/v2/proto/subscription/sender"
2933
)
3034

@@ -136,3 +140,140 @@ func TestCreateDomainEventAttachment(t *testing.T) {
136140
})
137141
}
138142
}
143+
144+
func newTestLocalizer(lang string) locale.Localizer {
145+
md := metadata.New(map[string]string{"accept-language": lang})
146+
return locale.NewLocalizer(metadata.NewIncomingContext(context.Background(), md))
147+
}
148+
149+
func TestCreateFeatureStaleAttachment(t *testing.T) {
150+
t.Parallel()
151+
152+
staleDays := strconv.Itoa(featuredomain.SecondsToStale / 24 / 60 / 60)
153+
154+
patterns := []struct {
155+
desc string
156+
lang string
157+
expectedText string
158+
}{
159+
{
160+
desc: "english",
161+
lang: locale.En,
162+
expectedText: "There are feature flags that have not been used for more than " + staleDays + " days.",
163+
},
164+
{
165+
desc: "japanese",
166+
lang: locale.Ja,
167+
expectedText: staleDays + "日以上使用されていないフィーチャーフラグがあります。",
168+
},
169+
}
170+
171+
for _, p := range patterns {
172+
t.Run(p.desc, func(t *testing.T) {
173+
t.Parallel()
174+
notifier := &slackNotifier{webURL: "https://example.com", logger: zap.NewNop()}
175+
attachment, err := notifier.createFeatureStaleAttachment(
176+
&senderproto.FeatureStaleNotification{
177+
EnvironmentName: "test-env",
178+
EnvironmentUrlCode: "test",
179+
Features: []*featureproto.Feature{
180+
{Id: "feature-id-1", Name: "feature-name-1"},
181+
},
182+
},
183+
newTestLocalizer(p.lang),
184+
)
185+
assert.NoError(t, err)
186+
assert.Contains(t, attachment.Text, p.expectedText)
187+
assert.Contains(t, attachment.Text, "https://example.com/test/features/feature-id-1")
188+
})
189+
}
190+
}
191+
192+
func TestCreateExperimentRunningAttachment(t *testing.T) {
193+
t.Parallel()
194+
195+
patterns := []struct {
196+
desc string
197+
lang string
198+
expectedText string
199+
expectedDaysMsg string
200+
}{
201+
{
202+
desc: "english",
203+
lang: locale.En,
204+
expectedText: "There are running experiments.",
205+
expectedDaysMsg: "- Days left: `1`, Name: *",
206+
},
207+
{
208+
desc: "japanese",
209+
lang: locale.Ja,
210+
expectedText: "実行中のエクスペリメントがあります。",
211+
expectedDaysMsg: "- 残り `1` 日, Name: *",
212+
},
213+
}
214+
215+
for _, p := range patterns {
216+
t.Run(p.desc, func(t *testing.T) {
217+
t.Parallel()
218+
notifier := &slackNotifier{webURL: "https://example.com", logger: zap.NewNop()}
219+
attachment, err := notifier.createExperimentRunningAttachment(
220+
&senderproto.ExperimentRunningNotification{
221+
EnvironmentName: "test-env",
222+
EnvironmentUrlCode: "test",
223+
Experiments: []*experimentproto.Experiment{
224+
{
225+
Id: "experiment-id-1",
226+
Name: "experiment-name-1",
227+
StopAt: time.Now().Add(25 * time.Hour).Unix(),
228+
},
229+
},
230+
},
231+
newTestLocalizer(p.lang),
232+
)
233+
assert.NoError(t, err)
234+
assert.Contains(t, attachment.Text, p.expectedText)
235+
assert.Contains(t, attachment.Text, p.expectedDaysMsg)
236+
})
237+
}
238+
}
239+
240+
func TestCreateMAUCountAttachment(t *testing.T) {
241+
t.Parallel()
242+
243+
patterns := []struct {
244+
desc string
245+
lang string
246+
expectedText string
247+
}{
248+
{
249+
desc: "english",
250+
lang: locale.En,
251+
expectedText: "This is the MAU for month 4.",
252+
},
253+
{
254+
desc: "japanese",
255+
lang: locale.Ja,
256+
expectedText: "4月のMAUです。",
257+
},
258+
}
259+
260+
for _, p := range patterns {
261+
t.Run(p.desc, func(t *testing.T) {
262+
t.Parallel()
263+
notifier := &slackNotifier{webURL: "https://example.com", logger: zap.NewNop()}
264+
attachment, err := notifier.createMAUCountAttachment(
265+
&senderproto.MauCountNotification{
266+
EnvironmentName: "test-env",
267+
EventCount: 1234,
268+
UserCount: 567,
269+
Month: 4,
270+
},
271+
newTestLocalizer(p.lang),
272+
)
273+
assert.NoError(t, err)
274+
assert.Contains(t, attachment.Text, p.expectedText)
275+
assert.Contains(t, attachment.Text, "Event count: 1,234")
276+
assert.Contains(t, attachment.Text, "User count: 567")
277+
})
278+
}
279+
}

0 commit comments

Comments
 (0)