Yi EungJun 2014-02-24
Send notification mail in user's preferred language.
Add 'lang' field in User model and store user's preferred language every
time user send a request which has Accept-Language HTTP field(s) and/or
language preference in a cookie.

When Yobi send notification emails, it groups recipients by preferred
language and send the email to each group separately in the preferred
language.
@d1e1022c6fe2361d31246d14dbff4831b2af3ef5
app/Global.java
--- app/Global.java
+++ app/Global.java
@@ -212,7 +212,10 @@
             public Result call(Http.Context ctx) throws Throwable {
                 if (ctx.session().get(UserApp.SESSION_USERID) == null) {
                     UserApp.isRememberMe();
+                } else {
+                    UserApp.updatePreferredLanguage();
                 }
+
                 ctx.response().setHeader("Date", DateUtils.formatDate(new Date()));
                 ctx.response().setHeader("Cache-Control", "no-cache");
                 Result result = delegate.call(ctx);
app/controllers/UserApp.java
--- app/controllers/UserApp.java
+++ app/controllers/UserApp.java
@@ -20,36 +20,30 @@
 package controllers;
 
 import actions.AnonymousCheckAction;
-
 import com.avaje.ebean.ExpressionList;
 import com.avaje.ebean.annotation.Transactional;
-
 import models.*;
 import models.enumeration.Operation;
 import models.enumeration.UserState;
-
 import org.apache.commons.lang.StringUtils;
 import org.apache.shiro.crypto.RandomNumberGenerator;
 import org.apache.shiro.crypto.SecureRandomNumberGenerator;
 import org.apache.shiro.crypto.hash.Sha256Hash;
 import org.apache.shiro.util.ByteSource;
-
+import org.codehaus.jackson.node.ObjectNode;
 import play.Configuration;
 import play.Logger;
+import play.Play;
 import play.data.Form;
 import play.i18n.Messages;
+import play.libs.Json;
 import play.mvc.*;
 import play.mvc.Http.Cookie;
-import utils.AccessControl;
-import utils.Constants;
-import utils.HttpUtil;
-import utils.ReservedWordsValidator;
-import utils.ErrorViews;
-import views.html.user.*;
-
-import org.codehaus.jackson.node.ObjectNode;
-
-import play.libs.Json;
+import utils.*;
+import views.html.user.edit;
+import views.html.user.login;
+import views.html.user.signup;
+import views.html.user.view;
 
 import java.util.*;
 
@@ -183,6 +177,9 @@
             if (sourceUser.rememberMe) {
                 setupRememberMe(authenticate);
             }
+
+            authenticate.lang = play.mvc.Http.Context.current().lang().code();
+            authenticate.update();
 
             if (StringUtils.isEmpty(redirectUrl)) {
                 return redirect(routes.Application.index());
@@ -849,4 +846,31 @@
         session(SESSION_LOGINID, user.loginId);
         session(SESSION_USERNAME, user.name);
     }
+
+    /**
+     * 현재 사용자가 선호하는 언어를 갱신한다.
+     *
+     * 쿠키나 Accept-Language HTTP 헤더에 선호하는 언어가 설정되어 있는 경우, 그것을 현재 로그인한 사용자가
+     * 선호하는 언어로 설정한다.
+     */
+    public static void updatePreferredLanguage() {
+        Http.Request request = Http.Context.current().request();
+        User user = UserApp.currentUser();
+
+        if (user.isAnonymous()) {
+            return;
+        }
+
+        if (request.acceptLanguages().isEmpty() &&
+                request.cookie(Play.langCookieName()) == null) {
+            return;
+        }
+
+        String code = StringUtils.left(Http.Context.current().lang().code(), 255);
+
+        if (!code.equals(user.lang)) {
+            user.lang = code;
+            user.update();
+        }
+    }
 }
app/models/NotificationEvent.java
--- app/models/NotificationEvent.java
+++ app/models/NotificationEvent.java
@@ -13,21 +13,22 @@
 import org.joda.time.DateTime;
 import org.tmatesoft.svn.core.SVNException;
 import play.Configuration;
+import play.api.i18n.Lang;
 import play.api.mvc.Call;
 import play.db.ebean.Model;
 import play.i18n.Messages;
 import play.libs.Akka;
-import playRepository.*;
+import playRepository.Commit;
+import playRepository.GitCommit;
+import playRepository.GitConflicts;
+import playRepository.RepositoryService;
 import scala.concurrent.duration.Duration;
 import utils.RouteUtil;
 
 import javax.persistence.*;
 import javax.servlet.ServletException;
 import java.io.IOException;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -86,6 +87,11 @@
 
     @Transient
     public String getMessage() {
+        return getMessage(Lang.defaultLang());
+    }
+
+    @Transient
+    public String getMessage(Lang lang) {
         if (message != null) {
             return message;
         }
@@ -93,15 +99,15 @@
         switch (eventType) {
             case ISSUE_STATE_CHANGED:
                 if (newValue.equals(State.CLOSED.state())) {
-                    return Messages.get("notification.issue.closed");
+                    return Messages.get(lang, "notification.issue.closed");
                 } else {
-                    return Messages.get("notification.issue.reopened");
+                    return Messages.get(lang, "notification.issue.reopened");
                 }
             case ISSUE_ASSIGNEE_CHANGED:
                 if (newValue == null) {
-                    return Messages.get("notification.issue.unassigned");
+                    return Messages.get(lang, "notification.issue.unassigned");
                 } else {
-                    return Messages.get("notification.issue.assigned", newValue);
+                    return Messages.get(lang, "notification.issue.assigned", newValue);
                 }
             case NEW_ISSUE:
             case NEW_POSTING:
@@ -113,26 +119,26 @@
                 return newValue;
             case PULL_REQUEST_STATE_CHANGED:
                 if (State.OPEN.state().equals(newValue)) {
-                    return Messages.get("notification.pullrequest.reopened");
+                    return Messages.get(lang, "notification.pullrequest.reopened");
                 } else {
-                    return Messages.get("notification.pullrequest." + newValue);
+                    return Messages.get(lang, "notification.pullrequest." + newValue);
                 }
             case PULL_REQUEST_COMMIT_CHANGED:
                 return newValue;
             case PULL_REQUEST_MERGED:
-                return Messages.get("notification.type.pullrequest.merged." + newValue) + "\n" + StringUtils.defaultString(oldValue, StringUtils.EMPTY);
+                return Messages.get(lang, "notification.type.pullrequest.merged." + newValue) + "\n" + StringUtils.defaultString(oldValue, StringUtils.EMPTY);
             case MEMBER_ENROLL_REQUEST:
                 if (RequestState.REQUEST.name().equals(newValue)) {
-                    return Messages.get("notification.member.enroll.request");
+                    return Messages.get(lang, "notification.member.enroll.request");
                 } else  if (RequestState.ACCEPT.name().equals(newValue)) {
-                    return Messages.get("notification.member.enroll.accept");
+                    return Messages.get(lang, "notification.member.enroll.accept");
                 } else {
-                    return Messages.get("notification.member.enroll.cancel");
+                    return Messages.get(lang, "notification.member.enroll.cancel");
                 }
             case PULL_REQUEST_REVIEWED:
-                return Messages.get("notification.pullrequest.reviewed", newValue);
+                return Messages.get(lang, "notification.pullrequest.reviewed", newValue);
             case PULL_REQUEST_UNREVIEWED:
-                return Messages.get("notification.pullrequest.unreviewed", newValue);
+                return Messages.get(lang, "notification.pullrequest.unreviewed", newValue);
             default:
                 return null;
         }
app/models/NotificationMail.java
--- app/models/NotificationMail.java
+++ app/models/NotificationMail.java
@@ -11,7 +11,9 @@
 import org.jsoup.select.Elements;
 import play.Configuration;
 import play.Logger;
+import play.api.i18n.Lang;
 import play.db.ebean.Model;
+import play.i18n.Messages;
 import play.libs.Akka;
 import scala.concurrent.duration.Duration;
 import utils.Config;
@@ -23,10 +25,7 @@
 import javax.persistence.OneToOne;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.Date;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
 
 @Entity
@@ -121,37 +120,57 @@
             return;
         }
 
-        final HtmlEmail email = new HtmlEmail();
+        HashMap<String, List<User>> usersByLang = new HashMap<>();
 
-        try {
-            email.setFrom(Config.getEmailFromSmtp(), event.getSender().name);
-            email.addTo(Config.getEmailFromSmtp(), utils.Config.getSiteName());
+        for (User receiver : receivers) {
+            String lang = receiver.lang;
 
-            for (User receiver : receivers) {
-                email.addBcc(receiver.email, receiver.name);
+            if (lang == null) {
+                lang = Locale.getDefault().getLanguage();
             }
 
-            String message = event.getMessage();
-            String urlToView = Url.create(event.getUrlToView());
-            String reference = Url.removeFragment(event.getUrlToView());
+            if (usersByLang.containsKey(lang)) {
+                usersByLang.get(lang).add(receiver);
+            } else {
+                usersByLang.put(lang, new ArrayList<User>(Arrays.asList(receiver)));
+            }
+        }
 
-            email.setSubject(event.title);
-            email.setHtmlMsg(getHtmlMessage(message, urlToView));
-            email.setTextMsg(getPlainMessage(message, urlToView));
-            email.setCharset("utf-8");
-            email.addHeader("References", "<" + reference + "@" + Config.getHostname() + ">");
-            email.setSentDate(event.created);
-            Mailer.send(email);
-            String escapedTitle = email.getSubject().replace("\"", "\\\"");
-            String logEntry = String.format("\"%s\" %s", escapedTitle, email.getBccAddresses());
-            play.Logger.of("mail").info(logEntry);
-        } catch (Exception e) {
-            Logger.warn("Failed to send a notification: "
-                    + email + "\n" + ExceptionUtils.getStackTrace(e));
+        for (String langCode : usersByLang.keySet()) {
+            final HtmlEmail email = new HtmlEmail();
+
+            try {
+                email.setFrom(Config.getEmailFromSmtp(), event.getSender().name);
+                email.addTo(Config.getEmailFromSmtp(), utils.Config.getSiteName()); 
+
+                for (User receiver : usersByLang.get(langCode)) {
+                    email.addBcc(receiver.email, receiver.name);
+                }
+
+                Lang lang = Lang.apply(langCode);
+
+                String message = event.getMessage(lang);
+                String urlToView = Url.create(event.getUrlToView());
+                String reference = Url.removeFragment(event.getUrlToView());
+
+                email.setSubject(event.title);
+                email.setHtmlMsg(getHtmlMessage(lang, message, urlToView));
+                email.setTextMsg(getPlainMessage(lang, message, urlToView));
+                email.setCharset("utf-8");
+                email.addHeader("References", "<" + reference + "@" + Config.getHostname() + ">");
+                email.setSentDate(event.created);
+                Mailer.send(email);
+                String escapedTitle = email.getSubject().replace("\"", "\\\"");
+                String logEntry = String.format("\"%s\" %s", escapedTitle, email.getBccAddresses());
+                play.Logger.of("mail").info(logEntry);
+            } catch (Exception e) {
+                Logger.warn("Failed to send a notification: "
+                        + email + "\n" + ExceptionUtils.getStackTrace(e));
+            }
         }
     }
 
-    private static String getHtmlMessage(String message, String urlToView) {
+    private static String getHtmlMessage(Lang lang, String message, String urlToView) {
         Document doc = Jsoup.parse(Markdown.render(message));
 
         String[] attrNames = {"src", "href"};
@@ -171,18 +190,18 @@
 
         if (urlToView != null) {
             doc.body().append(String.format("<hr><a href=\"%s\">%s</a>", urlToView,
-                    "View it on " + utils.Config.getSiteName()));
+                    Messages.get(lang, "notification.linkToView", utils.Config.getSiteName())));
         }
 
         return doc.html();
     }
 
-    private static String getPlainMessage(String message, String urlToView) {
+    private static String getPlainMessage(Lang lang, String message, String urlToView) {
         String msg = message;
         String url = urlToView;
 
         if (url != null) {
-            msg += String.format("\n\n--\nView it on %s", url);
+            msg += String.format("\n\n--\n" + Messages.get(lang, "notification.linkToView", url));
         }
 
         return msg;
app/models/User.java
--- app/models/User.java
+++ app/models/User.java
@@ -146,6 +146,12 @@
     @OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
     public RecentlyVisitedProjects recentlyVisitedProjects;
 
+    /**
+     * The user's preferred language code which can be recognized by {@link play.api.i18n.Lang#get},
+     * such as "ko", "en-US" or "ja". This field is used as a language for notification mail.
+     */
+    public String lang;
+
     public User() {
     }
 
 
conf/evolutions/default/65.sql (added)
+++ conf/evolutions/default/65.sql
@@ -0,0 +1,7 @@
+# --- !Ups
+
+ALTER TABLE n4user ADD COLUMN lang varchar(255);
+
+# --- !Downs
+
+ALTER TABLE n4user DROP COLUMN lang;
conf/messages
--- conf/messages
+++ conf/messages
@@ -295,6 +295,7 @@
 notification.issue.closed  = Issue has been closed
 notification.issue.reopened = Issue has been reopened
 notification.issue.unassigned = Issue has been unassigned
+notification.linkToView = View it on {0}
 notification.member.enroll.accept = Joined as a member.
 notification.member.enroll.cancel = The request for joining your project has been canceled.
 notification.member.enroll.request = Received a request for joining your project.
conf/messages.ko
--- conf/messages.ko
+++ conf/messages.ko
@@ -295,6 +295,7 @@
 notification.issue.closed  = 이슈 닫힘
 notification.issue.reopened = 이슈 다시 열림
 notification.issue.unassigned = 이슈 담당자 없음
+notification.linkToView = {0}에서 보기
 notification.member.enroll.accept = 멤버로 등록됨
 notification.member.enroll.cancel = 멤버 등록 요청이 취소됨
 notification.member.enroll.request = 멤버 등록 요청을 받음
Add a comment
List