Skip to content

Commit 73ff0e0

Browse files
authored
[JN-1637] allow overriding translations for study & portal names (#1526)
1 parent cfccf24 commit 73ff0e0

File tree

21 files changed

+361
-37
lines changed

21 files changed

+361
-37
lines changed

core/src/main/java/bio/terra/pearl/core/dao/i18n/LanguageTextDao.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,20 @@ public void deleteByLocalSite(UUID localSiteId) {
7676
public List<LanguageText> findByLocalSiteId(UUID id) {
7777
return findAllByProperty("localized_site_content_id", id);
7878
}
79+
80+
public Optional<LanguageText> findBySiteContentLanguageAndKey(UUID siteContentId, String language, String key) {
81+
return jdbi.withHandle(handle ->
82+
handle.createQuery("""
83+
select lt.* from language_text lt
84+
inner join localized_site_content lsc on lt.localized_site_content_id = lsc.id
85+
inner join site_content sc on lsc.site_content_id = sc.id
86+
where sc.id = :siteContentId and lsc.language = :language and lt.language = :language and lt.key_name = :key
87+
""")
88+
.bind("siteContentId", siteContentId)
89+
.bind("language", language)
90+
.bind("key", key)
91+
.mapTo(clazz)
92+
.findOne()
93+
);
94+
}
7995
}

core/src/main/java/bio/terra/pearl/core/service/i18n/LanguageTextService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,16 @@ public Optional<LanguageText> findSystemTextByKeyAndLanguage(String keyName, Str
4040
public void deleteByLocalSite(UUID localSiteId, Set<CascadeProperty> cascades) {
4141
languageTextDao.deleteByLocalSite(localSiteId);
4242
}
43+
44+
public Optional<LanguageText> findBySiteContentLanguageAndKey(UUID siteContentId, String language, String key) {
45+
return languageTextDao.findBySiteContentLanguageAndKey(siteContentId, language, key);
46+
}
47+
48+
public static String formatStudyNameTranslationKey(String portalShortcode, String studyShortcode) {
49+
return String.format("study:%s.%s", portalShortcode, studyShortcode);
50+
}
51+
52+
public static String formatPortalNameTranslationKey(String portalShortcode) {
53+
return String.format("portal:%s", portalShortcode);
54+
}
4355
}

core/src/main/java/bio/terra/pearl/core/service/notification/email/EnrolleeEmailService.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import bio.terra.pearl.core.model.portal.PortalEnvironment;
66
import bio.terra.pearl.core.model.study.Study;
77
import bio.terra.pearl.core.model.workflow.TaskType;
8+
import bio.terra.pearl.core.service.i18n.LanguageTextService;
89
import bio.terra.pearl.core.service.notification.NotificationContextInfo;
910
import bio.terra.pearl.core.service.notification.NotificationSender;
1011
import bio.terra.pearl.core.service.notification.NotificationService;
@@ -35,18 +36,20 @@ public class EnrolleeEmailService implements NotificationSender {
3536
private final EmailTemplateService emailTemplateService;
3637
private final ApplicationRoutingPaths routingPaths;
3738
private final SendgridClient sendgridClient;
39+
private final LanguageTextService languageTextService;
3840

3941
public EnrolleeEmailService(NotificationService notificationService,
4042
PortalEnvironmentService portalEnvService, PortalService portalService,
4143
StudyService studyService, EmailTemplateService emailTemplateService,
42-
ApplicationRoutingPaths routingPaths, SendgridClient sendgridClient) {
44+
ApplicationRoutingPaths routingPaths, SendgridClient sendgridClient, LanguageTextService languageTextService) {
4345
this.notificationService = notificationService;
4446
this.portalEnvService = portalEnvService;
4547
this.portalService = portalService;
4648
this.studyService = studyService;
4749
this.emailTemplateService = emailTemplateService;
4850
this.routingPaths = routingPaths;
4951
this.sendgridClient = sendgridClient;
52+
this.languageTextService = languageTextService;
5053
}
5154

5255
@Async
@@ -131,6 +134,28 @@ protected Mail buildEmail(NotificationContextInfo contextInfo, EnrolleeContext r
131134
// if this portal environment hasn't been configured with a specific email, just send from the support address
132135
fromAddress = routingPaths.getSupportEmailAddress();
133136
}
137+
138+
if (contextInfo.study() != null) {
139+
// makes sure study.name returns the name of the study in the preferred language
140+
languageTextService
141+
.findBySiteContentLanguageAndKey(
142+
contextInfo.portalEnv().getSiteContentId(),
143+
preferredLanguage,
144+
LanguageTextService.formatStudyNameTranslationKey(contextInfo.portal().getShortcode(), contextInfo.study().getShortcode()))
145+
.ifPresent(studyNameText -> contextInfo.study().setName(studyNameText.getText()));
146+
}
147+
148+
if (contextInfo.portal() != null) {
149+
// makes sure portal.name returns the name of the portal in the preferred language
150+
languageTextService
151+
.findBySiteContentLanguageAndKey(
152+
contextInfo.portalEnv().getSiteContentId(),
153+
preferredLanguage,
154+
LanguageTextService.formatPortalNameTranslationKey(contextInfo.portal().getShortcode()))
155+
.ifPresent(portalNameText -> contextInfo.portal().setName(portalNameText.getText()));
156+
}
157+
158+
134159
String fromName = "Juniper";
135160
if (contextInfo.portal().getName() != null) {
136161
fromName = contextInfo.portal().getName();
@@ -140,6 +165,7 @@ protected Mail buildEmail(NotificationContextInfo contextInfo, EnrolleeContext r
140165
fromName += " (%s)".formatted(contextInfo.portalEnv().getEnvironmentName());
141166
}
142167

168+
143169
Mail mail = sendgridClient.buildEmail(
144170
localizedEmailTemplate,
145171
ruleData.getProfile().getContactEmail(),

core/src/main/java/bio/terra/pearl/core/service/notification/substitutors/EnrolleeEmailSubstitutor.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ public class EnrolleeEmailSubstitutor implements StringLookup {
3232
private NotificationContextInfo contextInfo;
3333
private final ApplicationRoutingPaths routingPaths;
3434

35-
protected EnrolleeEmailSubstitutor(EnrolleeContext ruleData, NotificationContextInfo contextInfo,
36-
ApplicationRoutingPaths routingPaths, Map<String, String> messages) {
35+
protected EnrolleeEmailSubstitutor(EnrolleeContext ruleData,
36+
NotificationContextInfo contextInfo,
37+
ApplicationRoutingPaths routingPaths,
38+
Map<String, String> messages) {
3739
this.enrolleeContext = ruleData;
3840
this.contextInfo = contextInfo;
3941
this.routingPaths = routingPaths;

core/src/test/java/bio/terra/pearl/core/service/i18n/LanguageTextServiceTests.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
import bio.terra.pearl.core.BaseSpringBootTest;
44
import bio.terra.pearl.core.factory.i18n.LanguageTextFactory;
55
import bio.terra.pearl.core.factory.portal.PortalFactory;
6+
import bio.terra.pearl.core.factory.site.SiteContentFactory;
67
import bio.terra.pearl.core.model.i18n.LanguageText;
78
import bio.terra.pearl.core.model.portal.Portal;
9+
import bio.terra.pearl.core.model.site.LocalizedSiteContent;
10+
import bio.terra.pearl.core.model.site.SiteContent;
11+
import bio.terra.pearl.core.service.site.LocalizedSiteContentService;
812
import org.junit.jupiter.api.Test;
913
import org.junit.jupiter.api.TestInfo;
1014
import org.springframework.beans.factory.annotation.Autowired;
@@ -27,6 +31,12 @@ public class LanguageTextServiceTests extends BaseSpringBootTest {
2731
@Autowired
2832
private PortalFactory portalFactory;
2933

34+
@Autowired
35+
private SiteContentFactory siteContentFactory;
36+
37+
@Autowired
38+
private LocalizedSiteContentService localizedSiteContentService;
39+
3040
@Test
3141
@Transactional
3242
public void testFindSystemTextByKeyAndLanguage(TestInfo testInfo) {
@@ -72,4 +82,57 @@ public void testGetLanguageTextMapForLanguage(TestInfo testInfo) {
7282
)));
7383
}
7484

85+
@Test
86+
@Transactional
87+
public void testGetLanguageTextBySiteContent(TestInfo info) {
88+
89+
SiteContent content1 = siteContentFactory.buildPersisted(getTestName(info));
90+
91+
LocalizedSiteContent localizedContent1Es = LocalizedSiteContent
92+
.builder()
93+
.siteContentId(content1.getId())
94+
.language("es")
95+
.build();
96+
97+
LocalizedSiteContent localizedContent1Dev = LocalizedSiteContent
98+
.builder()
99+
.siteContentId(content1.getId())
100+
.language("dev")
101+
.build();
102+
103+
104+
localizedContent1Es = localizedSiteContentService.create(localizedContent1Es);
105+
localizedContent1Dev = localizedSiteContentService.create(localizedContent1Dev);
106+
107+
SiteContent content2 = siteContentFactory.buildPersisted(getTestName(info));
108+
109+
LocalizedSiteContent localizedContent2Es = LocalizedSiteContent
110+
.builder()
111+
.siteContentId(content2.getId())
112+
.language("es")
113+
.build();
114+
115+
116+
localizedContent2Es = localizedSiteContentService.create(localizedContent2Es);
117+
118+
LanguageText languageTextEs = languageTextFactory.buildPersisted(getTestName(info), "testkey", "es", localizedContent1Es.getId());
119+
LanguageText languageTextDev = languageTextFactory.buildPersisted(getTestName(info), "testkey", "dev", localizedContent1Dev.getId());
120+
121+
122+
Optional<LanguageText> foundText = languageTextService.findBySiteContentLanguageAndKey(content1.getId(), "es", languageTextEs.getKeyName());
123+
assertThat(foundText.isPresent(), equalTo(true));
124+
assertThat(foundText.get().getText(), equalTo(languageTextEs.getText()));
125+
assertThat(foundText.get().getId(), equalTo(languageTextEs.getId()));
126+
127+
128+
foundText = languageTextService.findBySiteContentLanguageAndKey(content1.getId(), "dev", languageTextDev.getKeyName());
129+
assertThat(foundText.isPresent(), equalTo(true));
130+
assertThat(foundText.get().getText(), equalTo(languageTextDev.getText()));
131+
assertThat(foundText.get().getId(), equalTo(languageTextDev.getId()));
132+
133+
foundText = languageTextService.findBySiteContentLanguageAndKey(content2.getId(), "es", languageTextEs.getKeyName());
134+
assertThat(foundText.isPresent(), equalTo(false));
135+
136+
}
137+
75138
}

core/src/test/java/bio/terra/pearl/core/service/notification/email/EnrolleeEmailServiceTests.java

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,24 @@
66
import bio.terra.pearl.core.factory.notification.TriggerFactory;
77
import bio.terra.pearl.core.factory.participant.EnrolleeBundle;
88
import bio.terra.pearl.core.factory.participant.EnrolleeFactory;
9+
import bio.terra.pearl.core.factory.site.SiteContentFactory;
910
import bio.terra.pearl.core.model.EnvironmentName;
11+
import bio.terra.pearl.core.model.i18n.LanguageText;
1012
import bio.terra.pearl.core.model.notification.*;
1113
import bio.terra.pearl.core.model.participant.Enrollee;
1214
import bio.terra.pearl.core.model.participant.Profile;
1315
import bio.terra.pearl.core.model.portal.Portal;
1416
import bio.terra.pearl.core.model.portal.PortalEnvironment;
1517
import bio.terra.pearl.core.model.portal.PortalEnvironmentConfig;
18+
import bio.terra.pearl.core.model.site.LocalizedSiteContent;
19+
import bio.terra.pearl.core.model.site.SiteContent;
20+
import bio.terra.pearl.core.model.study.Study;
1621
import bio.terra.pearl.core.model.workflow.TaskType;
22+
import bio.terra.pearl.core.service.i18n.LanguageTextService;
1723
import bio.terra.pearl.core.service.notification.NotificationContextInfo;
1824
import bio.terra.pearl.core.service.notification.NotificationService;
1925
import bio.terra.pearl.core.service.rule.EnrolleeContext;
26+
import bio.terra.pearl.core.service.site.LocalizedSiteContentService;
2027
import bio.terra.pearl.core.service.study.StudyService;
2128
import bio.terra.pearl.core.shared.ApplicationRoutingPaths;
2229
import com.sendgrid.helpers.mail.Mail;
@@ -49,6 +56,12 @@ public class EnrolleeEmailServiceTests extends BaseSpringBootTest {
4956
private SendgridClient sendgridClient;
5057
@Autowired
5158
private EnrolleeEmailService enrolleeEmailService;
59+
@Autowired
60+
private SiteContentFactory siteContentFactory;
61+
@Autowired
62+
private LocalizedSiteContentService localizedSiteContentService;
63+
@Autowired
64+
private LanguageTextService languageTextService;
5265

5366

5467
@Test
@@ -153,6 +166,89 @@ public void testEmailBuildingWithMissingPreferredTemplate(TestInfo info) {
153166
assertThat(email.getSubject(), equalTo("Welcome given"));
154167
}
155168

169+
@Test
170+
@Transactional
171+
public void testEmailBuildingLocalizesStudyName(TestInfo info) {
172+
Profile profileEs = Profile.builder()
173+
.familyName("tester")
174+
.givenName("given")
175+
.contactEmail("[email protected]")
176+
.preferredLanguage("es")
177+
.build();
178+
Profile profileEn = Profile.builder()
179+
.familyName("tester")
180+
.givenName("given")
181+
.contactEmail("[email protected]")
182+
.preferredLanguage("en")
183+
.build();
184+
Enrollee enrollee = Enrollee.builder().build();
185+
186+
EnrolleeContext ruleDataEs = new EnrolleeContext(enrollee, profileEs, null, null);
187+
EnrolleeContext ruleDataEn = new EnrolleeContext(enrollee, profileEn, null, null);
188+
189+
SiteContent siteContent = siteContentFactory.buildPersisted(getTestName(info));
190+
191+
LocalizedSiteContent localizedSiteContentEs = LocalizedSiteContent.builder()
192+
.siteContentId(siteContent.getId())
193+
.language("es")
194+
.build();
195+
196+
localizedSiteContentEs = localizedSiteContentService.create(localizedSiteContentEs);
197+
198+
199+
LanguageText esStudyNameLanguageText = languageTextService.create(LanguageText
200+
.builder()
201+
.keyName("study:portal1.study1")
202+
.language("es")
203+
.text("spanish study name")
204+
.localizedSiteContentId(localizedSiteContentEs.getId())
205+
.build());
206+
207+
LanguageText esPortalNameLanguageText = languageTextService.create(LanguageText
208+
.builder()
209+
.keyName("portal:portal1")
210+
.language("es")
211+
.text("MiPortal")
212+
.localizedSiteContentId(localizedSiteContentEs.getId())
213+
.build());
214+
215+
216+
PortalEnvironmentConfig portalEnvConfig = PortalEnvironmentConfig.builder()
217+
.emailSourceAddress("[email protected]").build();
218+
PortalEnvironment portalEnv = PortalEnvironment.builder()
219+
.siteContentId(siteContent.getId())
220+
.environmentName(EnvironmentName.irb).portalEnvironmentConfig(portalEnvConfig).build();
221+
Portal portal = Portal.builder().shortcode("portal1").name("MyPortal").build();
222+
Study study = Study.builder().shortcode("study1").name("default study name").build();
223+
224+
LocalizedEmailTemplate englishTemplate = LocalizedEmailTemplate.builder()
225+
.body("study ${study.name}")
226+
.language("en")
227+
.subject("test").build();
228+
LocalizedEmailTemplate spanishTemplate = LocalizedEmailTemplate.builder()
229+
.body("estudio ${study.name}")
230+
.language("es")
231+
.subject("test").build();
232+
EmailTemplate emailTemplate = EmailTemplate.builder()
233+
.localizedEmailTemplates(List.of(englishTemplate, spanishTemplate)).build();
234+
235+
NotificationContextInfo contextInfo = new NotificationContextInfo(portal, portalEnv, portalEnvConfig, study, emailTemplate);
236+
Mail email = enrolleeEmailService.buildEmail(contextInfo, ruleDataEn, new Notification());
237+
assertThat(email.personalization.get(0).getTos().get(0).getEmail(), equalTo("[email protected]"));
238+
assertThat(email.content.get(0).getValue(), equalTo("study default study name"));
239+
assertThat(email.from.getEmail(), equalTo("[email protected]"));
240+
assertThat(email.from.getName(), equalTo("MyPortal (irb) (local)"));
241+
assertThat(email.getSubject(), equalTo("test"));
242+
243+
email = enrolleeEmailService.buildEmail(contextInfo, ruleDataEs, new Notification());
244+
245+
assertThat(email.personalization.get(0).getTos().get(0).getEmail(), equalTo("[email protected]"));
246+
assertThat(email.content.get(0).getValue(), equalTo("estudio " + esStudyNameLanguageText.getText()));
247+
assertThat(email.from.getEmail(), equalTo("[email protected]"));
248+
assertThat(email.from.getName(), equalTo("MiPortal (irb) (local)"));
249+
assertThat(email.getSubject(), equalTo("test"));
250+
}
251+
156252
@Test
157253
@Transactional
158254
public void testEmailSendOrSkip(TestInfo info) {

populate/src/main/resources/seed/portals/demo/siteContent/siteContent-2/siteContent.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,16 @@
3333
"populateFileName": "es/participation.json"
3434
}, {
3535
"populateFileName": "es/learnMore.json"
36-
}]
36+
}],
37+
"languageTextOverrides": [
38+
{
39+
"text": "Estudio de Demostración",
40+
"keyName": "study:demo.heartdemo"
41+
}, {
42+
"text": "Juniper Demostración",
43+
"keyName": "portal:demo"
44+
}
45+
]
3746
},{
3847
"language": "dev",
3948
"navLogoCleanFileName": "juniper-demo-logo.png",
@@ -51,6 +60,12 @@
5160
{
5261
"text": "DEV_Join Us",
5362
"keyName": "navbarJoin"
63+
}, {
64+
"text": "DEV_Heart Demo",
65+
"keyName": "study:demo.heartdemo"
66+
}, {
67+
"text": "DEV_Juniper Heart Demo",
68+
"keyName": "portal:demo"
5469
}
5570
]
5671
}]

ui-admin/src/portal/siteContent/LanguageTextOverridesEditor.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ import {
3434
import { basicTableLayout } from 'util/table/tableUtils'
3535
import { faTrashCan } from '@fortawesome/free-solid-svg-icons/faTrashCan'
3636
import { TextInput } from 'components/forms/TextInput'
37-
import Select from 'react-select'
3837
import Api from 'api/api'
3938
import { useLoadingEffect } from 'api/api-utils'
4039
import LoadingSpinner from 'util/LoadingSpinner'
4140
import { PortalEnvContext } from 'portal/PortalRouter'
41+
import Creatable from 'react-select/creatable'
4242

4343

4444
type EditableLanguageText = Partial<LanguageText> & { isEditing: boolean }
@@ -124,6 +124,8 @@ export default function LanguageTextOverridesEditor(
124124
const onChangeNewOverrideKey = (key: string) => {
125125
if (languageKeys.includes(key)) {
126126
setNewOverride({ ...newOverride, keyName: key, text: languageTexts[key] })
127+
} else {
128+
setNewOverride({ ...newOverride, keyName: key, text: '' })
127129
}
128130
}
129131

@@ -142,7 +144,7 @@ export default function LanguageTextOverridesEditor(
142144
cell: ({ row }) => {
143145
const value = row.original.keyName
144146
if (isEditable(row.original)) {
145-
return row.original.isEditing && <Select
147+
return row.original.isEditing && <Creatable
146148
aria-label={'New language text override key'}
147149
options={languageKeys.map(key => {
148150
return {

0 commit comments

Comments
 (0)