Skip to content

Commit

Permalink
Multilingual Emails (geonetwork#8044)
Browse files Browse the repository at this point in the history
* Add settings fields for localized emails

* Add spring bean to initialize feedback email locales from settings field

* Modify settings manager to update feedback locales when the settings fields are saved

* Create classes for localized emails, their components, and the components' parameters

* Implement lang in getIndexField

* Localize workflow status emails

* Localize metadata publication emails

* Localize user feedback emails

* Localize RegisterApi emails

* Localize PasswordApi emails

* Localize WatchListNotifier emails

* Localize MailApi emails

* Update migration script to only insert settings fields if not present

* Add static enum imports for readability

* Log a warning when a locale is invalid or missing

* Update log modules and messages

* Trim language codes defined in settings to handle spaces after commas

* Rename translation follows label to translation follows text for consistency

* Add back resource bundle 'messages' that was unused before merging main

* Add logic to break from loop when email subject and text messages fail
  • Loading branch information
tylerjmchugh authored Jun 19, 2024
1 parent 1ece808 commit 1fd8b52
Show file tree
Hide file tree
Showing 25 changed files with 1,355 additions and 244 deletions.
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;
}
}

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

0 comments on commit 1fd8b52

Please sign in to comment.