Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multilingual Emails #8044

Merged
merged 25 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
146f184
Add settings fields for localized emails
tylerjmchugh May 15, 2024
c7c4991
Add spring bean to initialize feedback email locales from settings field
tylerjmchugh May 15, 2024
80a7d26
Modify settings manager to update feedback locales when the settings …
tylerjmchugh May 15, 2024
acf4147
Create classes for localized emails, their components, and the compon…
tylerjmchugh May 15, 2024
88f2068
Implement lang in getIndexField
tylerjmchugh May 15, 2024
3fab7c3
Localize workflow status emails
tylerjmchugh May 15, 2024
da0f545
Localize metadata publication emails
tylerjmchugh May 15, 2024
cc83101
Localize user feedback emails
tylerjmchugh May 15, 2024
3d6b6ce
Localize RegisterApi emails
tylerjmchugh May 15, 2024
9988d7d
Localize PasswordApi emails
tylerjmchugh May 15, 2024
c94dd7b
Localize WatchListNotifier emails
tylerjmchugh May 15, 2024
461e4c5
Localize MailApi emails
tylerjmchugh May 15, 2024
a8f4d11
Update migration script to only insert settings fields if not present
tylerjmchugh May 16, 2024
bd1dc23
Add static enum imports for readability
tylerjmchugh May 16, 2024
3f5a3af
Log a warning when a locale is invalid or missing
tylerjmchugh May 16, 2024
2a56de3
Update log modules and messages
tylerjmchugh May 16, 2024
4319bb6
Merge main into main.multilingual-emails
tylerjmchugh May 16, 2024
ae96904
Merge remote-tracking branch 'refs/remotes/fork/main' into main.multi…
tylerjmchugh May 17, 2024
6c4f653
Trim language codes defined in settings to handle spaces after commas
tylerjmchugh May 17, 2024
cbcdf2d
Rename translation follows label to translation follows text for cons…
tylerjmchugh May 17, 2024
b3c4547
Merge main into main.multilingual-emails and resolve conflicts
tylerjmchugh Jun 3, 2024
5471960
Add back resource bundle 'messages' that was unused before merging main
tylerjmchugh Jun 3, 2024
c2599a4
Add logic to break from loop when email subject and text messages fail
tylerjmchugh Jun 4, 2024
5e8d4b5
Merge main into main.multilingual-emails
tylerjmchugh Jun 4, 2024
4d6825f
Merge main into main.multilingual-emails
tylerjmchugh Jun 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 54 additions & 34 deletions core/src/main/java/org/fao/geonet/kernel/WatchListNotifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@
import org.fao.geonet.domain.Selection;
import org.fao.geonet.domain.User;
import org.fao.geonet.kernel.setting.SettingManager;
import org.fao.geonet.languages.FeedbackLanguages;
import org.fao.geonet.repository.SelectionRepository;
import org.fao.geonet.repository.UserRepository;
import org.fao.geonet.repository.UserSavedSelectionRepository;
import org.fao.geonet.util.LocalizedEmail;
import org.fao.geonet.util.LocalizedEmailParameter;
import org.fao.geonet.util.LocalizedEmailComponent;
import org.fao.geonet.util.MailUtil;
import org.fao.geonet.utils.Log;
import org.quartz.JobExecutionContext;
Expand All @@ -44,6 +48,10 @@
import java.util.*;

import static org.fao.geonet.kernel.setting.Settings.SYSTEM_USER_LASTNOTIFICATIONDATE;
import static org.fao.geonet.util.LocalizedEmailComponent.ComponentType.*;
import static org.fao.geonet.util.LocalizedEmailComponent.KeyType;
import static org.fao.geonet.util.LocalizedEmailComponent.ReplacementType.*;
import static org.fao.geonet.util.LocalizedEmailParameter.ParameterType;

/**
* Task checking on a regular basis the list of records
Expand All @@ -53,15 +61,13 @@ public class WatchListNotifier extends QuartzJobBean {

private String lastNotificationDate;
private String nextLastNotificationDate;
private String subject;
private String message;
private String recordMessage;
private String updatedRecordPermalink;
private String language = "eng";
private SettingManager settingManager;
private ApplicationContext appContext;
private UserSavedSelectionRepository userSavedSelectionRepository;
private UserRepository userRepository;
private FeedbackLanguages feedbackLanguages;

@Value("${usersavedselection.watchlist.searchurl}")
private String permalinkApp = "catalog.search#/search?_uuid={{filter}}";
Expand Down Expand Up @@ -92,20 +98,7 @@ public WatchListNotifier() {
protected void executeInternal(JobExecutionContext jobContext) throws JobExecutionException {
appContext = ApplicationContextHolder.get();
settingManager = appContext.getBean(SettingManager.class);

ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages",
new Locale(
language
));

try {
subject = messages.getString("user_watchlist_subject");
message = messages.getString("user_watchlist_message");
recordMessage = messages.getString("user_watchlist_message_record").
replace("{{link}}",
settingManager.getNodeURL() + permalinkRecordApp);
} catch (Exception e) {
}
feedbackLanguages = appContext.getBean(FeedbackLanguages.class);

updatedRecordPermalink = settingManager.getSiteURL(language);

Expand Down Expand Up @@ -166,6 +159,9 @@ protected void executeInternal(JobExecutionContext jobContext) throws JobExecuti
}

private void notify(Integer selectionId, Integer userId) {

Locale[] feedbackLocales = feedbackLanguages.getLocales(new Locale(language));

// Get metadata with changes since last notification
// TODO: Could be relevant to get versionning system info once available
// and report deleted records too.
Expand All @@ -188,27 +184,51 @@ private void notify(Integer selectionId, Integer userId) {
// TODO: We should send email depending on user language
Optional<User> user = userRepository.findById(userId);
if (user.isPresent() && StringUtils.isNotEmpty(user.get().getEmail())) {
String url = updatedRecordPermalink +
permalinkApp.replace("{{filter}}", String.join(" or ", updatedRecords));

// Build message
StringBuffer listOfUpdateMessage = new StringBuffer();
for (String record : updatedRecords) {
try {
listOfUpdateMessage.append(
MailUtil.compileMessageWithIndexFields(recordMessage, record, this.language)
);
} catch (Exception e) {
Log.error(Geonet.USER_WATCHLIST, e.getMessage(), e);
LocalizedEmailComponent emailSubjectComponent = new LocalizedEmailComponent(SUBJECT, "user_watchlist_subject", KeyType.MESSAGE_KEY, POSITIONAL_FORMAT);
LocalizedEmailComponent emailMessageComponent = new LocalizedEmailComponent(MESSAGE, "user_watchlist_message", KeyType.MESSAGE_KEY, POSITIONAL_FORMAT);

for (Locale feedbackLocale : feedbackLocales) {

// Build message
StringBuffer listOfUpdateMessage = new StringBuffer();
for (String record : updatedRecords) {
LocalizedEmailComponent recordMessageComponent = new LocalizedEmailComponent(NESTED, "user_watchlist_message_record", KeyType.MESSAGE_KEY, NAMED_FORMAT);
recordMessageComponent.enableCompileWithIndexFields(record);
recordMessageComponent.enableReplaceLinks(true);
try {
listOfUpdateMessage.append(
recordMessageComponent.parseMessage(feedbackLocale)
);
} catch (Exception e) {
Log.error(Geonet.USER_WATCHLIST, e.getMessage(), e);
}
}

emailSubjectComponent.addParameters(
feedbackLocale,
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 1, settingManager.getSiteName()),
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 2, updatedRecords.size()),
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 3, lastNotificationDate)
);

emailMessageComponent.addParameters(
feedbackLocale,
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 1, listOfUpdateMessage.toString()),
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 2, lastNotificationDate),
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 3, url),
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 4, url)
);

}

String url = updatedRecordPermalink +
permalinkApp.replace("{{filter}}", String.join(" or ", updatedRecords));
String mailSubject = String.format(subject,
settingManager.getSiteName(), updatedRecords.size(), lastNotificationDate);
String htmlMessage = String.format(message,
listOfUpdateMessage.toString(),
lastNotificationDate,
url, url);
LocalizedEmail localizedEmail = new LocalizedEmail(true);
localizedEmail.addComponents(emailSubjectComponent, emailMessageComponent);

String mailSubject = localizedEmail.getParsedSubject(feedbackLocales);
String htmlMessage = localizedEmail.getParsedMessage(feedbackLocales);

if (Log.isDebugEnabled(Geonet.USER_WATCHLIST)) {
Log.debug(Geonet.USER_WATCHLIST, String.format(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,22 @@
import org.fao.geonet.kernel.setting.Settings;
import org.fao.geonet.repository.*;
import org.fao.geonet.repository.specification.GroupSpecs;
import org.fao.geonet.util.LocalizedEmail;
import org.fao.geonet.util.LocalizedEmailParameter;
import org.fao.geonet.util.LocalizedEmailComponent;
import org.fao.geonet.languages.FeedbackLanguages;
import org.fao.geonet.util.MailUtil;
import org.fao.geonet.utils.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;

import java.text.MessageFormat;
import java.util.*;

import static org.fao.geonet.kernel.setting.Settings.SYSTEM_FEEDBACK_EMAIL;
import static org.fao.geonet.util.LocalizedEmailComponent.ComponentType.*;
import static org.fao.geonet.util.LocalizedEmailComponent.KeyType;
import static org.fao.geonet.util.LocalizedEmailComponent.ReplacementType.*;
import static org.fao.geonet.util.LocalizedEmailParameter.ParameterType;

public class DefaultStatusActions implements StatusActions {

Expand Down Expand Up @@ -240,61 +247,106 @@ protected void notify(List<User> userToNotify, MetadataStatus status) throws Exc
return;
}

ResourceBundle messages = ResourceBundle.getBundle("org.fao.geonet.api.Messages", new Locale(this.language));
ApplicationContext applicationContext = ApplicationContextHolder.get();
FeedbackLanguages feedbackLanguages = applicationContext.getBean(FeedbackLanguages.class);

String translatedStatusName = getTranslatedStatusName(status.getStatusValue().getId());
// TODO: Refactor to allow custom messages based on the type of status
String subjectTemplate = "";
try {
subjectTemplate = messages
.getString("status_change_" + status.getStatusValue().getName() + "_email_subject");
} catch (MissingResourceException e) {
subjectTemplate = messages.getString("status_change_default_email_subject");
}
String subject = MessageFormat.format(subjectTemplate, siteName, translatedStatusName, replyToDescr // Author of the change
);
Locale[] feedbackLocales = feedbackLanguages.getLocales(new Locale(this.language));

Set<Integer> listOfId = new HashSet<>(1);
listOfId.add(status.getMetadataId());

String textTemplate = "";
try {
textTemplate = messages.getString("status_change_" + status.getStatusValue().getName() + "_email_text");
} catch (MissingResourceException e) {
textTemplate = messages.getString("status_change_default_email_text");
}

// Replace link in message
ApplicationContext applicationContext = ApplicationContextHolder.get();
SettingManager sm = applicationContext.getBean(SettingManager.class);
textTemplate = textTemplate.replace("{{link}}", sm.getNodeURL()+ "api/records/'{{'index:uuid'}}'");

UserRepository userRepository = context.getBean(UserRepository.class);
User owner = userRepository.findById(status.getOwner()).orElse(null);

IMetadataUtils metadataRepository = ApplicationContextHolder.get().getBean(IMetadataUtils.class);
AbstractMetadata metadata = metadataRepository.findOne(status.getMetadataId());

String metadataUrl = metadataUtils.getDefaultUrl(metadata.getUuid(), this.language);
String subjectTemplateKey = "";
String textTemplateKey = "";
boolean failedToFindASpecificSubjectTemplate = false;
boolean failedToFindASpecificTextTemplate = false;

for (Locale feedbackLocale: feedbackLocales) {
ResourceBundle resourceBundle = ResourceBundle.getBundle("org.fao.geonet.api.Messages", feedbackLocale);

if (!failedToFindASpecificSubjectTemplate) {
try {
subjectTemplateKey = "status_change_" + status.getStatusValue().getName() + "_email_subject";
resourceBundle.getString(subjectTemplateKey);
} catch (MissingResourceException e) {
failedToFindASpecificSubjectTemplate = true;
}
}

if (!failedToFindASpecificTextTemplate) {
try {
textTemplateKey = "status_change_" + status.getStatusValue().getName() + "_email_text";
resourceBundle.getString(textTemplateKey);
} catch (MissingResourceException e) {
failedToFindASpecificTextTemplate = true;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}
if ((failedToFindASpecificSubjectTemplate) && (failedToFindASpecificTextTemplate)) break;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added loop breaking logic in latest push


if ((failedToFindASpecificSubjectTemplate) && (failedToFindASpecificTextTemplate)) break;
}

if (failedToFindASpecificSubjectTemplate) {
subjectTemplateKey = "status_change_default_email_subject";
}

if (failedToFindASpecificTextTemplate) {
textTemplateKey = "status_change_default_email_text";
}

LocalizedEmailComponent emailSubjectComponent = new LocalizedEmailComponent(SUBJECT, subjectTemplateKey, KeyType.MESSAGE_KEY, NUMERIC_FORMAT);
emailSubjectComponent.enableCompileWithIndexFields(metadata.getUuid());

LocalizedEmailComponent emailMessageComponent = new LocalizedEmailComponent(MESSAGE, textTemplateKey, KeyType.MESSAGE_KEY, NUMERIC_FORMAT);
emailMessageComponent.enableCompileWithIndexFields(metadata.getUuid());
emailMessageComponent.enableReplaceLinks(false);

LocalizedEmailComponent emailSalutationComponent = new LocalizedEmailComponent(SALUTATION, "{{userName}},\n\n", KeyType.RAW_VALUE, NONE);

for (Locale feedbackLocale : feedbackLocales) {
// TODO: Refactor to allow custom messages based on the type of status

emailSubjectComponent.addParameters(
feedbackLocale,
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 1, siteName),
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 2, getTranslatedStatusName(status.getStatusValue().getId(), feedbackLocale)),
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 3, replyToDescr)
);

emailMessageComponent.addParameters(
feedbackLocale,
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 1, replyToDescr),
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 2, status.getChangeMessage()),
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 3, getTranslatedStatusName(status.getStatusValue().getId(), feedbackLocale)),
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 4, status.getChangeDate()),
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 5, status.getDueDate()),
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 6, status.getCloseDate()),
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 7, owner == null ? "" : Joiner.on(" ").skipNulls().join(owner.getName(), owner.getSurname())),
new LocalizedEmailParameter(ParameterType.RAW_VALUE, 8, metadataUtils.getDefaultUrl(metadata.getUuid(), feedbackLocale.getISO3Language()))
);
}

String message = MessageFormat.format(textTemplate, replyToDescr, // Author of the change
status.getChangeMessage(), translatedStatusName, status.getChangeDate(), status.getDueDate(),
status.getCloseDate(),
owner == null ? "" : Joiner.on(" ").skipNulls().join(owner.getName(), owner.getSurname()),
metadataUrl);
LocalizedEmail localizedEmail = new LocalizedEmail(false);
localizedEmail.addComponents(emailSubjectComponent, emailMessageComponent, emailSalutationComponent);

String subject = localizedEmail.getParsedSubject(feedbackLocales);

subject = MailUtil.compileMessageWithIndexFields(subject, metadata.getUuid(), this.language);
message = MailUtil.compileMessageWithIndexFields(message, metadata.getUuid(), this.language);
for (User user : userToNotify) {
String salutation = Joiner.on(" ").skipNulls().join(user.getName(), user.getSurname());
//If we have a salutation then end it with a ","
if (StringUtils.isEmpty(salutation)) {
salutation = "";
String userName = Joiner.on(" ").skipNulls().join(user.getName(), user.getSurname());
//If we have a userName add the salutation
String message;
if (StringUtils.isEmpty(userName)) {
message = localizedEmail.getParsedMessage(feedbackLocales);
} else {
salutation += ",\n\n";
Map<String, String> replacements = new HashMap<>();
replacements.put("{{userName}}", userName);
message = localizedEmail.getParsedMessage(feedbackLocales, replacements);
}
sendEmail(user.getEmail(), subject, salutation + message);
sendEmail(user.getEmail(), subject, message);
}
}

Expand Down Expand Up @@ -408,14 +460,14 @@ protected void unsetAllOperations(int mdId) throws Exception {
}
}

private String getTranslatedStatusName(int statusValueId) {
private String getTranslatedStatusName(int statusValueId, Locale locale) {
String translatedStatusName = "";
StatusValue s = statusValueRepository.findOneById(statusValueId);
if (s == null) {
translatedStatusName = statusValueId
+ " (Status not found in database translation table. Check the content of the StatusValueDes table.)";
} else {
translatedStatusName = s.getLabel(this.language);
translatedStatusName = s.getLabel(locale.getISO3Language());
}
return translatedStatusName;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.fao.geonet.domain.Setting;
import org.fao.geonet.domain.SettingDataType;
import org.fao.geonet.domain.Setting_;
import org.fao.geonet.languages.FeedbackLanguages;
import org.fao.geonet.repository.SettingRepository;
import org.fao.geonet.repository.SortUtils;
import org.fao.geonet.repository.SourceRepository;
Expand Down Expand Up @@ -94,6 +95,9 @@ public class SettingManager {
@Autowired
DefaultLanguage defaultLanguage;

@Autowired
FeedbackLanguages feedbackLanguages;

@PostConstruct
private void init() {
this.pathFinder = new ServletPathFinder(servletContext);
Expand Down Expand Up @@ -343,6 +347,12 @@ public boolean setValue(String key, String value) {

repo.save(setting);

if (key.equals("system/feedback/languages")) {
feedbackLanguages.updateSupportedLocales();
} else if (key.equals("system/feedback/translationFollowsText")) {
feedbackLanguages.updateTranslationFollowsText();
}

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ public class Settings {
public static final String SYSTEM_USERS_IDENTICON = "system/users/identicon";
public static final String SYSTEM_SEARCHSTATS = "system/searchStats/enable";
public static final String SYSTEM_FEEDBACK_EMAIL = "system/feedback/email";
public static final String SYSTEM_FEEDBACK_LANGUAGES = "system/feedback/languages";
public static final String SYSTEM_FEEDBACK_TRANSLATION_FOLLOWS_TEXT = "system/feedback/translationFollowsText";
public static final String SYSTEM_FEEDBACK_MAILSERVER_HOST = "system/feedback/mailServer/host";
public static final String SYSTEM_FEEDBACK_MAILSERVER_PORT = "system/feedback/mailServer/port";
public static final String SYSTEM_FEEDBACK_MAILSERVER_USERNAME = "system/feedback/mailServer/username";
Expand Down
Loading
Loading