Trong bài viết trước mình đã giới thiệu các bạn cách ứng dụng Javalin để làm website thư viện sách, tiếp nối ví dụ trên hôm nay mình sẽ tiếp tục giới thiệu tiếp đến các bạn cách xử lý đa ngôn ngữ trong Javalin. Chúng ta sẽ sử dụng lại phần source code thư viện sách đã thảo luận hôm trước. Trong ví dụ gốc của Javalin tác giả đã gom 2 ví dụ này thành một project, điều này sẽ làm cho các bạn chưa tiếp xúc nhiều với Javalin sẽ gặp khó khăn để tiếp cận một cách rõ ràng mạch lạc hơn là copy code đơn thuần.

OK, Chúng ta bắt đầu

Cấu trúc project hoàn chỉnh của chúng ta sẽ như sau:

 

Trong folder resource các bạn thêm folder localization và 2 file properties message.properties (Tiếng Anh) và message_de.properties (Tiếng Đức).

Mình sẽ tóm tắt các bước trước khi đi chi tiết để các bạn có thể hình dung trình tự từ khi nhận request từ người dùng Javalin sẽ xử lý như thế nào ở Backend và cũng như cách thức trả về các message ngôn ngữ cho người dùng.

  1. Người dùng click vào button request chuyển ngôn ngữ, các bạn xem đoạn code dưới đây:
    ## thay đổi sang Tiếng Đức
    <button name="locale" value="de"></button>
    
    ## thay đổi sang Tiếng Anh
    <button name="locale" value="en"></button>

    Chúng ta sẽ set giá trị name='locale' để Backend nhận dạng đây là yêu cầu chuyển ngô ngữ. Và giá trị value='de' là ngôn ngữ yêu cầu.

  2. Backend nhận yêu cầu ở Filtersset session attribute "locale" thành ngôn ngữ yêu cầu (vd: de, en)
  3. Filter chuyển tiếp request đến tầng Controller, tại đây class MessageBundle sẽ load các text từ file ngôn ngữ theo yêu cầu của người dùng trong folder /resources, vd: messages_de.properties vào object ResourceBundle của Javalin và trả về cho Front-end dưới dạng 1 model attribute (vd: msg). Các bạn xem đoạn code sau:
    package app.util;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import io.javalin.Context;
    import io.javalin.ErrorHandler;
    
    import static app.util.RequestUtil.*;
    
    public class ViewUtil {
    
        public static Map<String, Object> baseModel(Context ctx) {
            Map<String, Object> model = new HashMap<>();
            model.put("msg", new MessageBundle(getSessionLocale(ctx)));
            model.put("currentUser", getSessionCurrentUser(ctx));
            return model;
        }
    
        public static ErrorHandler notFound = ctx -> {
            ctx.render(Path.Template.NOT_FOUND, baseModel(ctx));
        };
    
    }
    

     

  4. Trên UI, file velocity sẽ đọc các message từ model attribute (msg) để load text ngôn ngữ.

Chúng ta đi tuần tự từng bước, và việc đầu tiên là phải chuẩn bị các message ngôn ngữ trước, trong ví dụ này sẽ hỗ trợ 2 ngôn ngữ là Tiếng Anh và Tiếng Đức.

message.properties (hoặc message_en.properties). Với Javalin locale mặc định được set là EN (English)

## Common
COMMON_TITLE=Javalin Library
COMMON_FOOTER_TEXT=This Application uses <a href="https://openlibrary.org/" target="_blank">OpenLibrary</a> for images.
COMMON_NAV_ALLBOOKS=View all books
COMMON_NAV_LOGIN=Log in
COMMON_NAV_LOGOUT=Log out
ERROR_404_NOT_FOUND=We can't find the page you're looking for <small>(error 404)</small>

## Index
INDEX_HEADING=Welcome to the Javalin Library
INDEX_REGISTERED_USERS=There are currently {0} users registered:
INDEX_PASSWORD_INFO=It seems they've all chosen the password "password" for some reason. How silly.
INDEX_BOOK_OF_THE_DAY_TEXT=The book of the day is:
INDEX_BOOK_OF_THE_DAY_LINK=<strong>{0} by {1}</strong>

## Login
LOGIN_HEADING=Login
LOGIN_INSTRUCTIONS=Please enter your username and password. <small><br>(See the <a href="{0}">index page</a> if you need a hint)</small>
LOGIN_AUTH_SUCCEEDED=You''re logged in as ''{0}''.
LOGIN_AUTH_FAILED=The login information you supplied was incorrect.
LOGIN_LOGGED_OUT=You have been logged out.
LOGIN_LABEL_USERNAME=Username
LOGIN_LABEL_PASSWORD=Password
LOGIN_BUTTON_LOGIN=Log in

## Books
BOOKS_HEADING_ALL=All books
BOOKS_CAPTION=<strong>{0}</strong> <br>by {1}
BOOKS_BOOK_NOT_FOUND=Book not found

message_de.properties

## Common
COMMON_TITLE=Ze Javalin Library
COMMON_FOOTER_TEXT=Ze application uses <a href="https://openlibrary.org/" target="_blank">OpenLibrary</a> vor images.
COMMON_NAV_ALLBOOKS=View all ze books
COMMON_NAV_LOGIN=Innlogg
COMMON_NAV_LOGOUT=Auslogg
ERROR_404_NOT_FOUND=Ve cannot find ze page you are looking for <small>(error 404)</small>

## Index
INDEX_HEADING=Velcome to ze Javalin Library
INDEX_REGISTERED_USERS=Zere are currently {0} users registered:
INDEX_PASSWORD_INFO=It seems zey have all chosen ze password "password" for some reason. Hov silly.
INDEX_BOOK_OF_THE_DAY_TEXT=Ze book of ze day is:
INDEX_BOOK_OF_THE_DAY_LINK=<strong>{0} von {1}</strong>

## Login
LOGIN_HEADING=Innlogg
LOGIN_INSTRUCTIONS=Please enter dein username und password. <small><br>(Zee ze <a href="{0}">index page</a> if you need a hint)</small>
LOGIN_AUTH_SUCCEEDED=You''re logged in as ''{0}''.
LOGIN_AUTH_FAILED=Ze login informazion you zuplied vas incorrect.
LOGIN_LOGGED_OUT=You have been logged aus.
LOGIN_LABEL_USERNAME=Username
LOGIN_LABEL_PASSWORD=Password
LOGIN_BUTTON_LOGIN=Innlogg

## Books
BOOKS_HEADING_ALL=All ze books
BOOKS_CAPTION=<strong>{0}</strong> <br>von {1}
BOOKS_BOOK_NOT_FOUND=Book nicht found

 

Thêm Before Filter cho ngôn ngữ:

Chúng ta define class Filters với method handle locale khi nhận được request change locale từ người dùng.

Filters.java

package app.util;

import io.javalin.Handler;

import static app.util.RequestUtil.*;

public class Filters {

    // Locale change can be initiated from any page
    // The locale is extracted from the request and saved to the user's session
    public static Handler handleLocaleChange = ctx -> {
        if (getQueryLocale(ctx) != null) {
            ctx.sessionAttribute("locale", getQueryLocale(ctx));
            ctx.redirect(ctx.path());
        }
    };
}

Method handleLocaleChange() sẽ làm nhiệm vụ là set lại attribute "locale" của session khi đó của người dùng, điểu này có nghĩa là việc thay đổi ngôn ngữ sẽ chỉ có tác dụng trong session hiện tại của người dùng. Nếu như trình duyệt bị đóng hay session hiện tại bị time out thì ngôn ngữ hiển thị sẽ bị reset lại default,  trong trường hợp này là Tiếng Anh.

Trong file  Main.java, chúng ta thêm dòng code sau vào app.routes() để sử dụng class Filters

before(Filters.handleLocaleChange);
package app;

import app.book.BookController;
import app.book.BookDao;
import app.login.LoginController;
import app.user.UserDao;
import app.util.Filters;
import app.util.Path;
import app.util.ViewUtil;
import io.javalin.Javalin;

import static io.javalin.apibuilder.ApiBuilder.*;

public class Main {

    // Declare dependencies
    public static BookDao bookDao;
    public static UserDao userDao;

    public static void main(String[] args) {

        // Instantiate your dependencies
        bookDao = new BookDao();
        userDao = new UserDao();

        Javalin app = Javalin.create()
            .port(7000)
            .enableStaticFiles("/public")
            .enableRouteOverview("/routes")
            .start();

        app.routes(() -> {
            before(Filters.handleLocaleChange);
            before(LoginController.ensureLoginBeforeViewingBooks);
            get(Path.Web.BOOKS, BookController.fetchAllBooks);
            get(Path.Web.ONE_BOOK, BookController.fetchOneBook);
            get(Path.Web.LOGIN, LoginController.serveLoginPage);
            post(Path.Web.LOGIN, LoginController.handleLoginPost);
            post(Path.Web.LOGOUT, LoginController.handleLogoutPost);
        });

        app.error(404, ViewUtil.notFound);
    }
}

Load Bundle Message

Sau khi định nghĩa các text message ngôn ngữ vào các file tương ứng chúng ta sẽ cần một object (class) làm nhiệm vụ load chúng lên và trả về cho người dùng. Lưu ý method handleLocaleChange() chỉ làm nhiệm vụ set lại attribute session locale hay nói cách khác là chỉ đặt lại cờ ngôn ngữ cho session. Và chúng ta sẽ định nghĩa object load file ngôn ngữ tương ứng với cờ ngôn ngữ vừa đề cập.

MessageBundle.java

package app.util;

import java.text.MessageFormat;
import java.util.Locale;
import java.util.ResourceBundle;

public class MessageBundle {

    private ResourceBundle messages;

    public MessageBundle(String languageTag) {
        Locale locale = languageTag != null ? new Locale(languageTag) : Locale.ENGLISH;
        this.messages = ResourceBundle.getBundle("localization/messages", locale);
    }

    public String get(String message) {
        return messages.getString(message);
    }

    public final String get(final String key, final Object... args) {
        return MessageFormat.format(get(key), args);
    }

}

MessageBundle.java sẽ hỗ trợ 2 phương thức get message, message thuần (ví dụ như: login, logout,...) và message có kèm theo paramter (ví dụ welcome {0} - với {0} là username của người dùng)). Ngoài ra trong MessageBundle cũng chỉ rõ ra rằng chúng ta sử dụng ngôn ngữ Tiếng Anh làm ngôn ngữ mặc định nếu người dùng chưa chỉ định ngôn ngữ.

Các message ngôn ngữ được load từ file và được lưu vào object ResourceBundle.

Bind Bundle Message to View

Bước sau cùng, các Controller (vd: LoginController, BookController sẽ gọi hàm baseModel() để lấy MessageBundle, ví dụ như sau cho LoginController: 

package app.login;

import java.util.Map;

import app.user.UserController;
import app.util.Path;
import app.util.ViewUtil;
import io.javalin.Handler;

import static app.util.RequestUtil.*;

public class LoginController {

    public static Handler serveLoginPage = ctx -> {
        Map<String, Object> model = ViewUtil.baseModel(ctx);
        model.put("loggedOut", removeSessionAttrLoggedOut(ctx));
        model.put("loginRedirect", removeSessionAttrLoginRedirect(ctx));
        ctx.render(Path.Template.LOGIN, model);
    };

    //more methods here

}

 


login.vm

Để bind được message lên giao diện cho người dùng, các bạn gọi tên của model chứa ResourceBundle, (trong ví dụ là msg). 

#parse("/velocity/layout.vm")
#@mainLayout()
<form id="loginForm" method="post">
    #if($authenticationFailed)
        <p class="bad notification">$msg.get("LOGIN_AUTH_FAILED")</p>
    #elseif($authenticationSucceeded)
        <p class="good notification">$msg.get("LOGIN_AUTH_SUCCEEDED", $currentUser)</p>
    #elseif($loggedOut)
        <p class="notification">$msg.get("LOGIN_LOGGED_OUT")</p>
    #end
    <h1>$msg.get("LOGIN_HEADING")</h1>
    <p>$msg.get("LOGIN_INSTRUCTIONS", "/index")</p>
    <label>$msg.get("LOGIN_LABEL_USERNAME")</label>
    <input type="text" name="username" placeholder="$msg.get("LOGIN_LABEL_USERNAME")" value="" required>
    <label>$msg.get("LOGIN_LABEL_PASSWORD")</label>
    <input type="password" name="password" placeholder="$msg.get("LOGIN_LABEL_PASSWORD")" value="" required>
    #if($loginRedirect)
        <input type="hidden" name="loginRedirect" value="$loginRedirect">
    #end
    <input type="submit" value="$msg.get("LOGIN_BUTTON_LOGIN")">
</form>
#end

Chúng ta thay thế các đoạn text fix cứng bằng model attribute msg và gọi message tương ứng bằng các key define trong các file properties.

Kết quả:

 

Tới đây mình đã hoàn thành hướng dẫn các bạn cách setup đa ngôn ngữ trong Javalin.

Chúc các bạn thành công.

AutoCode.VN

minhnhatict@gmail.com Javalin