Author Archives: Phở Code

JNI – Các kiểu dữ liệu cơ bản

Các kiểu dữ liệu tham số ở phía native cũng tương tự với kiểu dữ liệu của ngôn ngữ đó.

Đối với các kiểu dữ liệu dạng primitive như int, float, char…v.v thì khi chuyển sang JNI cũng gần như là tương tự nhau, chỉ cần thêm kí tự 'j' vào trước tên kiểu dữ liệu, như int trong Java thì chuyển sang jint trong JNI, float thành jfloat…v.v

Bảng dưới đây mô tả các kiểu dữ liệu tương ứng giữa Java và JNI:

Java

JNI

Ghi chú

boolean jboolean 8 bit không dấu
byte jbyte 8 bit có dấu
char jchar 16 bit không dấu
short jshort 16 bit có dấu
int jint 32 bit có dấu
long jlong 64 bit có dấu
float jfloat 32 bit
double jdouble 64 bit

 

JNI – Tham số JNI

Chúng ta sẽ tìm hiểu về các tham số trong các phương thức native.

Để làm việc này thì chúng ta sẽ viết hàm native trong Java, hàm này sẽ nhận vào một chuỗi kí tự, bên C++ sẽ nhận chuỗi kí tự này và in ra màn hình.

Đầu tiên chúng ta tạo file .java như sau:

public class Arg {
    static {
        System.loadLibrary("Arg");
    }
 
    private native void sendName(String name);
 
    public static void main(String args[]){
        String name = "Pho Code";
        Arg arg = new Arg();
        arg.sendName(name);
    }
}

Phương thức nativesendName(), chúng ta truyền vào với chuỗi là "Pho Code".

Sau đó dịch và tạo file header C++:

javac Arg.java
...
javah -classpath . -jni Arg
...

File Arg.h với phương thức native được chuyển sang C++ sẽ được sinh ra như thế này:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Arg */

#ifndef _Included_Arg
#define _Included_Arg
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class: Arg
 * Method: sendName
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_Arg_sendName
 (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

Trong đó JNIEXPORTJNICALL là các macro được định nghĩa trong file jni.h, các macro này cho biết là hàm đó có thể được gọi từ nơi khác (giống như từ khóa extern trong C). Bạn cũng có thể để ý tên phương thức được tạo nên từ Java_<tên_lớp>_<tên_phương_thức>.

Và trong phương thức native này bao giờ cũng sẽ có ít nhất là 2 tham số JNIEnv*jobject. Về cơ bản thì đối tượng JNIEnv* là một con trỏ trỏ tới một con trỏ, con trỏ này trỏ tới một mảng, mảng này lại chứa các con trỏ trỏ tới các phương thức của JNI để chúng ta có thể sử dụng.

Tham số jobject sẽ khác nhau tùy thuộc vào phương thức native ở bên phía Java như thế nào. Nếu phương thức native là phương thức static thì jobject sẽ đại diện cho lớp chứa phương thức đó, còn nếu không thì jobject sẽ là đối tượng được tạo ra từ lớp đó.

Ngoài 2 tham số bắt buộc phải có kia thì chúng ta có thể truyền thêm các tham số khác như bình thường.

Bây giờ chúng ta viết code cho phương thức native C++ như sau:

#include <iostream>
#include "Arg.h"

JNIEXPORT void JNICALL Java_Arg_sendName (JNIEnv *e, jobject obj, jstring name){
    const char *str;
    str = e->GetStringUTFChars(name, JNI_FALSE);
    if(str == NULL)
        return;
    std::cout << "Hello " << str;
    e->ReleaseStringUTFChars(name, str);
}

Tham số jstring ở bên Java là biến là một đối tượng java.lang.String, nhưng C++ thì lại không có kiểu dữ liệu nào như thế này. Do đó chúng ta phải chuyển kiểu dữ liệu, may mắn là trong số các phương thức JNI có phương thức để chuyển, ở đây chúng ta dùng phương thức GetStringUTFChars(), phương thức này sẽ chuyển jstring sang một mảng char, tham số đầu tiên là biến jstring, tham số thứ 2 là một giá trị bool, chúng ta có thể dùng JNI_FALSE hoặc JNI_TRUE, ý nghĩa của tham số thứ 2 là có muốn thực hiện copy hay không.

Sau khi chuyển xong chúng ta có thể in ra như thường, ngoài ra chúng ta cũng có thể kiểm tra xem việc chuyển đổi có thành công hay không bằng cách kiểm tra xem strNULL hay không.

Cuối cùng chúng ta gọi phương hức ReleaseStringUTFChars() để xóa biến này khỏi bộ nhớ.

Bây giờ chúng ta có thể dịch và build file thư viện và chạy:

g++ -I"C:\Program Files\Java\jdk1.8.0_65\include" 
-I"C:\Program Files\Java\jdk1.8.0_65\include\win32 -o Arg.o -c Arg.cpp
...
g++ -Wl,--add-stdcall-alias -shared -o Arg.dll Arg.o
...
java -classpath . -Djava.library.path=. Arg
Hello Pho Code

Ngoài 2 phương thức GetStringUTFChars()ReleaseStringUTFChars() ở trên thì còn rất nhiều phương thức khác như:

  • jstring NewString(JNIEnv *, const jchar *, jsize): tạo chuỗi java.lang.String
  • jsize GetStringLength(JNIEnv *, jstring): lấy độ dài của chuỗi jstring
  • const jchar * GetStringChars(JNIEnv *, jstring, jboolean *)tương tự GetStringUTFChars()
  • void ReleaseStringChars(JNIEnv *, jstring, const jchar *): tương tự ReleaseStringUTFChars()

Bạn có thể xem danh sách tại đây.

JNI – Dịch một chương trình JNI

Trước tiên chúng ta cần có các công cụ sau đây:

  • JDK 2 trở lên, phiên bản mới nhất tại thời điểm viết bài là JDK 8.
  • Trình biên dịch MinGW.

Bạn lên mạng tải và cài đặt 2 trình biên dịch trên, JDK có thể tải trực tiếp từ website của Oracle. MinGW có thể tải về từ mingw.org, lưu ý là các IDE phổ biến như DevC++ hoặc CodeBlocks cũng thường có sẵn MinGW khi tải về rồi, nếu bạn đã từng tải và cài 2 IDE trên mà có sử dụng bản đính kèm MinGW thì bạn không cần phải tải MinGW về nữa.

Lưu ý là mặc dù bạn có cài bằng IDE thì ở đây chúng ta sẽ biên dịch trên dòng lệnh hết, tức là sẽ không có bấm nút màu xanh lá cây hay bấm Ctrl + F9, F6… gì cả.

Sau khi đã tải và cài xong thì bạn thêm đường dẫn đến thư mục bin trong thư mục cài đặt của 2 trình biên dịch này vào biến môi trường PATH để tiện sử dụng. Ví dụ trên Windows 10 thì bạn vào My Computer → Systems properties → Advanced system settings → Environment Variables… Sau đó trong phần System variables bạn tìm biến PATH, bấm Edit… và thêm đường dẫn đến 2 thư mục trên vào.

Xong xuôi rồi thì bạn có thể kiểm tra bằng cách mở Command Prompt (cmd) lên và gõ lệnh java -version để xem phiên bản Java và g++ --version để xem phiên bản C++.

Kết quả ra tương tự như hình dưới đây là được:

Liên kết Java và C++ bằng JNI

Để có thể gọi code C++ từ code Java thì chúng ta làm như sau:

  • Bước 1: Viết phương thức native trong Java
  • Bước 2: Dùng lệnh javac để dịch code đó ra file .class
  • Bước 3: Dùng lệnh javah -jni để tạo file .h (Header trong C++)
  • Bước 4: Viết hàm/lớp/phương thức C++ mà sẽ được gọi từ phương thức native trong Java đã khai báo ở trên
  • Bước 5: Dịch hàm/lớp/phương thức C++ trên thành file thư viện liên kết động .dll (Windows) hoặc .so (Linux)
  • Bước 6: Load file thư viện đó trong main ở phía Java và sử dụng

Ví dụ

Chúng ta sẽ viết hàm in dòng chữ “Hello World” bằng C++, sau đó biên dịch hàm đó ra file .dll rồi load file đó vào trong Java, từ Java chúng ta sẽ gọi hàm in dòng chữ “Hello World” đó.

Đầu tiên chúng ta tạo một thư mục chứa project, ở đây mình tạo thư mục HelloWorld trong ổ đĩa C.

Bước 1: Viết code Java

Chúng ta tạo một file có tên HelloWorld.java trong thư mục HelloWorld có nội dung như sau:

public class HelloWorld {
    static {
        System.loadLibrary("HelloWorld");
    }
 
    private native void print();
 
    public static void main(String[] args) {
        new HelloWorld().print();
    } 
}

Đoạn code trên rất đơn giản, đầu tiên chúng ta gọi phương thức System.loadLibrary("HelloWorld") để chương trình nạp file HelloWorld.dll vào, file HelloWorld.dll sẽ được tạo sau.

Sau đó ở dưới chúng ta định nghĩa phương thức có tên print(), phương thức này là phương thức native, chúng ta dùng từ khóa native để khai báo một phương thức là native. Phương thức native là phương thức được gọi từ file thư viện .dll.

Cuối cùng là hàm main(), trong đó chúng ta tạo đối tượng HelloWorld rồi gọi phương thức print().

Bước 2: biên dịch code Java

Chúng ta dùng lệnh javac HelloWorld.java để dịch ra file HelloWorld.class

Bước 3: Tạo file header .h

Chúng ta dùng lệnh javah -jni -classpath . HelloWorld để tạo ra file HelloWorld.h.

Trong lệnh trên thì tham số -classpath có giá trị là đường dẫn đến file .class đã được tạo ra ở bước 2, ở đây chúng ta truyền vô giá trị là dấu chấm, tức là đường dẫn đến thư mục hiện tại trong Command Prompt.

File HelloWorld.h sẽ được tạo ra tự động có nội dung như sau:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class: HelloWorld
 * Method: print
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloWorld_print
 (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

Chúng ta sẽ không đụng chạm gì tới các file .h này, có một lưu ý là ở đầu file này có include file thư viện jni.h, file này nằm trong thư mục include trong thư mục cài đặt JDK, bạn có thể tìm và mở ra xem, trong đó lại có dòng include đến file jni_md.h nữa, và file này nằm trong thư mục include/win32 trong thư mục cài đặt JDK, chúng ta sẽ cần lưu ý đến 2 thư mục này.

Một lưu ý nữa là trong đoạn code trên có phần khai báo prototype cho hàm Java_HelloWorld_print(JNIEnv *, jobject), không có phần thân hàm {}. Nhiệm vụ của chúng ta là phải code hàm này ở đâu đó.

Bước 4: viết hàm native có trong file header

Do đó bây giờ chúng ta tạo một file có tên HelloWorld.cpp trong thư mục đó với nội dung như sau:

#include <iostream>
#include "HelloWorld.h"

using namespace std;

JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *e, jobject obj) {
    cout << "Hello World printed from C++" << endl;
}

Ở đây chúng ta viết giống như viết code C++ bình thường, chỉ khác là chúng ta dùng những từ khóa của JNI như JNIEXPORT, JNICALl, JNIEnv, jobject…v.v chúng ta sẽ tìm hiểu chúng sau, một điểm khác nữa là không có hàm main(), đơn giản là vì chúng ta đang viết thư viện chứ không viết một chương trình.

Bước 5: dịch hàm native C++ ra file thư viện

Nếu bạn đã từng dịch C++ bằng dòng lệnh thì bạn sẽ biết là chúng ta cần dịch ra file object trước, sau đó mới link các file object thành file exe, ở đây cũng vậy, chỉ khác là chúng ta link thành file .dll (hoặc .so).

Đầu tiên chúng ta chạy lệnh g++ để tạo file object, ở đây chúng ta chạy lệnh này với cú pháp như sau:

g++ -I"<đường_dẫn_thư_mục_header" -c <tên_file_cpp> -o <tên_file_object>

Trong đó -I là tham số nhận vào đường dẫn đến các thư mục chứa file .h được dùng thêm, tại vì ở đây chúng ta có dùng đến file jni.hjni_md.h trong thư mục include của JDK nên chúng ta phải truyền vào tham số -I. Tham số -c nhận vào tên file .cpp mà chúng ta đã viết ra ở bước 4, tham số -o nhận vào tên file .o sẽ được tạo ra sau khi chạy lệnh này.

Giả sử thư mục cài đặt JDK của mình nằm ở C:\Programs Files\Java\jdk1.8.0_65 thì mình sẽ gõ lệnh trên như hình sau:

Tiếp theo chúng ta tạo file thư viện .dll theo cú pháp như sau:

g++ -Wl,--add-stdcall-alias -shared -o <tên_file_dll> <tên_file_object>

Bạn cần đưa vào tham số -Wl,--add-stdcall-alias nếu bạn dùng hệ điều hành Windows. Tham số -shared cho biết chúng ta sẽ tạo file thư viện, tham số -o có giá trị là tên file .dll sẽ được tạo ra, cuối cùng là tên file .o đã được tạo ra ở trên.

Lệnh mình gõ sẽ tương tự như sau:

Bước 6: Run

Vậy là xong, chúng ta có thể chạy code Java được rồi, chúng ta dùng lệnh sau:

java -classpath . -Djava.library.path=<đường_dẫn_đến_file_dll> <tên_lớp_main>

Chúng ta dùng lệnh Java để chạy một file đã được dịch thành file .class, tuy nhiên ở đây chúng ta có dùng thêm thư viện nữa, mặc dù file thư viện .dll nằm chung thư mục với file .class (nếu bạn làm giống như những gì đã hướng dẫn ở trên) nhưng Java vẫn sẽ không tự nhận diện file đó, do đó chúng ta phải thêm đường dẫn đến thư mục chứa file .dll đó trong tham số –Djava.library.path.

Kết quả giống như hình dưới:

JNI – Giới thiệu

Chúng ta biết môi trường Java là một môi trường lập trình bao gồm 2 thứ: máy ảo Java (JVM – Java Virtual Machine) và các thư viện API. Khi dịch thì Java sẽ được dịch sang mã máy có thể chạy được trên JVM.

Một khái niệm khác là môi trường máy chủ, tức là hệ điều hành được cài trên máy tính đó, môi trường này cũng có các API riêng, tập lệnh CPU riêng, ngôn ngữ lập trình riêng (thường là C/C++, Assembly).

Môi trường Java được cài nằm trên môi trường máy chủ, ứng với mỗi hệ điều hành (môi trường máy chủ riêng) mà có một môi trường Java riêng, chẳng hạn trên Windows và Solaris thì môi trường Java này là Java Runtime Environment (JRE).

Vai trò của JNI

Khi một môi trường Java được cài trên một hệ điều hành, sẽ có trường hợp người lập trình muốn sử dụng các thư viện của riêng hệ điều hành đó. Lý do là vì dùng thư viện của riêng hệ điều hành sẽ nhanh hơn, hiệu suất cao hơn.

Ngoài ra đối với hệ điều hành Windows thì số lượng các thư viện do cộng đồng viết ra rất nhiều, và có một số thư viện cực kỳ đồ sộ, việc viết lại các thư viện này bằng ngôn ngữ Java sẽ mất nhiều thời gian hơn so với việc tìm cách sử dụng chúng từ Java.

JNI là một tính năng cực kỳ mạnh mẽ cho phép chúng ta sử dụng code từ các ngôn ngữ khác, JNI có tính chất 2 chiều, tức là code từ các ngôn ngữ khác cũng có thể gọi lại code từ Java nữa.

Nhược điểm của JNI

Chúng ta đã biết rằng Java là một ngôn ngữ viêt một lần-chạy mọi nơi, tức là chỉ cần viết code Java, sau đó biên dịch rồi đem lên một hệ điều hành có cài JVM là có thể chạy bình thường. Tuy nhiên khi chúng ta sử dụng JNI để “hợp tác” với code của hệ điều hành, thì lại không thể đem chương trình đó đi chạy trên máy có hệ điều hành khác được, do đó mất đi tính viết một lần-chạy mọi nơi.

Một điều nữa là Java có tính năng type-safe, tức là bạn khai báo kiểu dữ liệu gì thì chỉ được thao tác với kiểu dữ liệu đó, nhưng các ngôn ngữ hệ điều hành thì có thể không có tính năng type-safe, do đó khi viết code JNI bạn sẽ phải chú ý cẩn thận, chỉ cần khác kiểu dữ liệu cũng có thể crash chương trình.

Khi nào nên sử dụng JNI

Trước khi quyết định sử dụng JNI, bạn nên tìm hiểu xem kỹ càng xem có thư viện nào hỗ trợ yêu cầu của mình không, nếu không còn thì hãy dùng JNI, luôn dùng JNI làm giải pháp cuối cùng.

Rails – Initializers

Trong một project Rails có một thư mục tên là initializers nằm trong thư mục config, thư mục này dùng để chứa các file .rb, ý nghĩa của thư mục này là khởi tạo tài nguyên. Mỗi khi chúng ta chạy server, Rails load các file và module xong thì sẽ chạy các file code .rb được đặt trong thư mục này.

Chúng ta sẽ quy định project sử dụng ngôn ngữ tiếng Anh mặc định khi chạy project.

Chúng ta tạo một file có tên i18n.rb trong thư mục initializers như sau:

I18n.default_locale = :en

LANGUAGES = [
    ['English', 'en'],
    ['Tiếng Việt', 'vi']
]

Để thiết lập ngôn ngữ mặc định cho I18n thì chúng ta gán vào thuộc tính default_locale.

Ngoài ra ở đây chúng ta còn định nghĩa một mảng có tên là LANGUAGES chứa các phần tử là mảng khác, các mảng phần tử chứa tên các ngôn ngữ. Chúng ta tạo mảng này để tạo nút chọn ngôn ngữ cho người dùng sử dụng.

Bây giờ chúng ta sửa lại file layout như sau:

<!DOCTYPE html>
<html>
<head>
    <title>Books Store</title> 
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>
<body id="store">
    <div id="banner"> 
        <%= form_tag '/', :class => 'locale', :method => :get do %>
            <%= select_tag 'set_locale',
                options_for_select(LANGUAGES, I18n.locale.to_s),
                :onchange => 'this.form.submit()' %>         
        <% end %>
        <%= image_tag("logo.png") %>
        <%= @page_title || t('.title') %>
    </div>
    <div id="columns">
        <div id="side">
            <div id="cart"> 
                <%= hide_cart_if(current_cart.line_items.empty?, :id => "cart") do %>
                    <%= render current_cart %> 
                <% end %>
            </div>
 
            <a href="#"><%= t('.home') %></a><br />
            <a href="#"><%= t('.faq') %></a><br />
            <a href="#"><%= t('.news') %></a><br />
            <a href="#"><%= t('.contact') %></a><br />
 
            <% if session[:user_id] %>
                <br/>
                <%= link_to 'Orders', '/orders' %><br />
                <%= link_to 'Products', '/products' %><br />
                <%= link_to 'Users', '/users' %><br />
                <%= button_to 'Logout', '/logout', :method => :delete %>
                <br/>
            <% else %>
                <br/>
                <%= link_to 'Log In', login_path %><br />
            <% end %>
        </div>
        <div id="main">
            <%= yield %>
        </div>
    </div> 
</body>
</html>

Chúng ta tạo một form bằng phương thức form_tag, form này sẽ gửi về trang '/' với phương thức là GET, bên trong chúng ta tạo một nút combobox bằng phương thức select_tag, phương thức này sẽ tạo thẻ <select> trong HTML.

Tham số đầu tiên của phương thức này là 'set_locale', đây là tên tham số được truyền lên server, Tham số thứ 2 là phương thức options_for_select(LANGUAGES, I18n.locale.to_s), có nghĩa là lấy danh sách các phần tử cho trong biến LANGGUAGES (đã được định nghĩa ở trên) để làm các item, giá trị được select mặc định lấy trong từ I18n.locale.to_s. Tham số thứ 3 là :onchange, tức là tên phương thức được gọi khi người dùng chọn một item khác, ở đây phương thức đó là this.form.submit(), tức là sẽ gửi form về server.

Form này chúng ta định nghĩa class riêng là locale, bây giờ chúng ta định nghĩa class CSS đó như sau:

.
.
.
/* Language chooser style */
.locale {
    float: right;
    margin: -0.25em 0.1em;
}

Cuối cùng chúng ta sửa phương thức index trong lớp StoreController như sau:

class StoreController < ApplicationController
    skip_before_filter :authorize
    
    def index 
        if params[:set_locale]
            @redirectURL = '/' + params[:set_locale] 
            redirect_to @redirectURL
        else
            @products = Product.all
            @cart = current_cart
        end
    end
end

Chúng ta kiểm tra xem lệnh gửi lên có kèm theo tham số set_locale hay không, nếu có thì chúng ta redirect về lại trang này với tham số ngôn ngữ đi kèm.

Rails – Đa ngôn ngữ – Phần 3

Chúng ta sẽ dịch lỗi trên model.

Đầu tiên chúng ta “dịch” form nhập thông tin thanh toán, trước tiên là file new.html.erb trong thư mục app/views/orders:

<div class="depot_form">
    <fieldset>
        <legend><%= t('.legend') %></legend>
            <%= render 'form' %> 
    </fieldset>
</div>

Tiếp theo là file _form.html.erb:

<%= form_for(@order) do |f| %>
    <% if @order.errors.any? %>
        <div id="error_explanation">
            <h2><%= pluralize(@order.errors.count, "error") %> prohibited this order from being saved:</h2> 
            <ul>
            <% @order.errors.full_messages.each do |message| %>
                <li><%= message %></li>
            <% end %>
            </ul>
        </div>
    <% end %>

    <div class="field">
        <%= f.label t('.name') %><br>
        <%= f.text_field :name, :size => 40 %>
    </div>
    <div class="field">
        <%= f.label :address, t('.address') %><br>
        <%= f.text_area :address, :rows => 3, :cols => 40 %>
    </div>
    <div class="field">
        <%= f.label :email %><br>
        <%= f.text_field :email, :size => 40 %>
    </div>
    <div class="field">
        <%= f.label t('.pay_type') %><br>
        <%= f.select :pay_type, Order::PAYMENT_TYPES, :prompt => t('.pay_type_combo') %>
    </div>
 
    <div class="actions">
        <%= f.submit t('.submit') %>
    </div>
<% end %>

Kế tiếp là file ngôn ngữ:

English:

en:
  .
  .
  .
  orders:
    new:
      legend: "Enter your information"
    form:
      name: "Name"
      address: "Address"
      pay_type: "Payment Type" 
      pay_type_combo: "Select a payment method"
      submit: "Checkout" 

Tiếng Việt:

vi:
  .
  .
  .
  orders:
    new:
      legend: "Thông tin thanh toán"
    form:
      name: "Tên"
      address: "Địa chỉ"
      pay_type: "Thanh toán" 
      pay_type_combo: "Chọn phương thức thanh toán"
      submit: "Đặt hàng" 

Bây giờ chúng ta có thể đặt hàng với giao diện tiếng Việt, và khi bấm nút “Đặt hàng” mà không điền cái gì vào thì chúng ta sẽ được trang thông báo lỗi như sau:

Do đó bây giờ chúng ta cần khai báo bản dịch cho phần báo lỗi.

Để dịch thì chúng ta sẽ dịch trên đối tượng ActiveRecord chứ không phải là các chuỗi do chúng ta tự khai báo trong view nữa, do đó chúng ta phải khai báo các khóa do Rails quy định.

Chúng ta chỉ cần chỉnh sửa phần tiếng Việt thôi, không cần khai báo phần ngôn ngữ tiếng Anh. Chúng ta sửa lại file vi.yml như sau:

vi:
  .
  .
  .
  activerecord: 
    models:
      order: "Đơn hàng" 
    attributes:
      order:
        name: "Tên" 
        address: "Địa chỉ" 
        email: "Email"
        pay_type: "Phương thức thanh toán" 
    errors: 
      messages:
        blank: "không được để trống" 
        inclusion: "không thuộc danh sách cho trước"  
      template: 
        body: "Lỗi xảy ra ở các trường sau:" 
      header:
        one: "1 lỗi xảy ra" 
        other: "%{count} lỗi xảy ra" 

Để dịch các tên model thì chúng ta truyền theo cấu trúc activerecord → models → <tên_model>. 

Dịch tên các thuộc tính/trường thì theo cấu trúc activerecord  → attributes  <tên_model>  <tên_thuộc_tính>

Dịch các dòng báo lỗi thì theo cấu trúc: activerecord → errors → messages → <tên_loại_lỗi>. Ở đây tên loại lỗi là blank, tức là lỗi thuộc tính không được để trống, inclusion là thuộc tính không thuộc một tập giá trị cho trước. Ngoài ra còn có các lỗi như confirmation, accepted, present…v.v Bạn có thể xem tại đây.

Ngoài ra còn có một số khóa khác như template, header, body, one, other. Trong đó one là hiển thị câu thông báo có 1 lỗi và other là thông báo có nhiều lỗi. Lý do phải chia ra 2 loại là vì có một số ngôn ngữ hiển thị số ít và số nhiều khác nhau, như tiếng Anh thì phải thêm kí tự ‘s’ sau cùng. Ở đây khóa other nhận vào giá trị từ biến count, chúng ta phải truyền biến này vào khi gọi hàm t().

Do đó bây giờ chúng ta sửa lại file _form.html.erb như sau:

<%= form_for(@order) do |f| %>
    <% if @order.errors.any? %>
        <div id="error_explanation">
            <h2><%= t('activerecord.errors.template.header', 
                    :count => @order.errors.size) %>.</h2> 
                <p><%= t('activerecord.errors.template.body') %></p>
            <ul>
                <% @order.errors.full_messages.each do |message| %>
                <li><%= message %></li>
                <% end %>
            </ul>
        </div>
    <% end %>
    .
    .
    .
<% end %>

Vậy là các câu thông báo lỗi đã được dịch xong:

Chúng ta dịch thêm một thứ nữa là câu thông báo đặt hàng thành công:

Đầu tiên chúng ta sửa lại phương thức create trong lớp OrdersController như sau:

class OrdersController < ApplicationController
    .
    .
    .
    # POST /orders
    # POST /orders.json
    def create
        @order = Order.new(order_params)
        @order.add_line_items_from_cart(current_cart)
 
        respond_to do |format|
            if @order.save
                Cart.destroy(session[:cart_id])
                session[:cart_id] = nil
                Notifier.order_received(@order).deliver
    
                format.html { redirect_to '/', notice: I18n.t('.thanks') }
                format.json { render :show, status: :created, location: @order }
            else
                format.html { render :new }
                format.json { render json: @order.errors, status: :unprocessable_entity }
            end
        end
    end
    .
    .
    .
end

Ở đây chúng ta phải ghi rõ ra phương thức t() của module I18n.

Tiếp theo là các file ngôn ngữ:

English:

en:
  .
  .
  .
  thanks: "Thank you for your order"

Tiếng Việt:

vi:
  .
  .
  .
  thanks: "Cám ơn bạn đã đặt hàng"

Vậy là xong!

Rails – Đa ngôn ngữ – Phần 2

Trong phần này chúng ta sẽ “dịch” một số chuỗi ra ngôn ngữ tiếng Việt.

Bây giờ chúng ta sửa lại file layout chính như sau:

<!DOCTYPE html>
<html>
<head>
<title>Books Store</title> 
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>
<body id="store">
    <div id="banner">
        <%= image_tag("logo.png") %>
        <%= @page_title || t('.title') %>
    </div>
    <div id="columns">
        <div id="side">
            <div id="cart"> 
                <%= hide_cart_if(current_cart.line_items.empty?, :id => "cart") do %>
                    <%= render current_cart %> 
                <% end %>
            </div>
 
            <a href="#"><%= t('.home') %></a><br />
            <a href="#"><%= t('.faq') %></a><br />
            <a href="#"><%= t('.news') %></a><br />
            <a href="#"><%= t('.contact') %></a><br />
 
            <% if session[:user_id] %>
                <br/>
                <%= link_to 'Orders', '/orders' %><br />
                <%= link_to 'Products', '/products' %><br />
                <%= link_to 'Users', '/users' %><br />
                <%= button_to 'Logout', '/logout', :method => :delete %>
                <br/>
            <% else %>
                <br/>
                <%= link_to 'Log In', login_path %><br />
            <% end %>
        </div>
        <div id="main">
            <%= yield %>
        </div>
    </div> 
</body>
</html>

Ở đây chúng ta gọi hàm t() thay vì ghi các chuỗi cụ thể ra luôn. Hàm t() là hàm của module i18n, đây là tên viết tắt của hàm translate(), hàm này nhận vào tên khóa và lấy giá trị tương ứng trong file .yml ra để hiển thị.

 

Bây giờ chúng ta phải tạo các khóa đó, chúng ta có thể đặt các file .yml trong cùng thư mục với từng View, tức là các thư mục views/admin, views/cartsv.v hoặc đặt trong thư mục mặc định của Rails là config/locales, ở đây chúng ta sẽ dùng thư mục mặc định cho dễ tìm, chúng ta sửa lại file en.yml trong thư mục config/locales như sau:

en: 
  layouts:
    application:
      home: "Home" 
      title: "Books Store"
      faq: "FAQ" 
      news: "News" 
      contact: "Contact"

Chúng ta phải khai báo khóa layout:application: rồi mới tới các khóa kia.

Lưu ý là chúng ta dùng 2 kí tự cách để thụt đầu dòng chứ không dùng dấu Tab. Đây là cú pháp của ngôn ngữ YAML (.yml).

Với tiếng Việt thì chúng ta cũng làm tương tự là tạo file vi.yml trong thư mục config/locales như sau:

vi:
  layouts:
    application:
      home: "Trang chủ" 
      title: "Books Store"
      faq: "Hỏi đáp" 
      news: "Tin tức" 
      contact: "Liên hệ"

Bây giờ chúng ta có thể dùng URL http://localhost:3000/vi được rồi.

Bây giờ chúng ta sẽ “dịch” trang store/index và trang hiển thị giỏ hàng. Đầu tiên chúng ta định nghĩa các đoạn text như sau:

English:

en: 
  layouts:
    application: 
      home: "Home" 
      title: "Books Store"
      faq: "FAQ" 
      news: "News" 
      contact: "Contact"
 
  store:
    index:
      title: "Products List" 
      add: "Add to Cart"
  carts:
    cart:
      title: "Your cart" 
      empty_cart: "Empty cart" 
      checkout: "Checkout"

Tiếng Việt:

vi:
  layouts:
    application:
      home: "Trang chủ" 
      title: "Books Store"
      faq: "Hỏi đáp" 
      news: "Tin tức" 
      contact: "Liên hệ"

  store:
    index:
      title: "Sản phẩm"
      add: "Thêm vào giỏ hàng"
 
  carts:
    cart:
      title: "Giỏ hàng"
      empty_cart: "Xóa giỏ hàng"
      checkout: "Thanh toán"

Tiếp theo chúng ta sửa file view index.html.erb trong thư mục app/views/store như sau:

<% if notice %>
    <p id="notice"><%= notice %></p>
<% end %>
 
<h1><%= t('.title') %></h1>
 
<% @products.each do |product| %>
    <div class="entry">
        <%= image_tag(product.image_url) %>
        <h3><%= product.title %></h3> 
        <%= sanitize(product.description) %>
        <div class="price_line">
            <span class="price"><%= number_to_currency(product.price) %></span>
            <%= button_to t('.add'), line_items_path(:product_id => product),
                :remote => true %>
        </div>
    </div>
<% end %>

Kế tiếp là file _cart.html.erb trong thư mục app/views/carts:

<h2><%= t('.title') %></h2>
<table>
    <%= render cart.line_items %>
    <tr class="total_line">
        <td colspan="2">Total</td>
        <td class="total_cell"><%= number_to_currency(cart.total_price) %></td>
    </tr>
</table>
 
<%= button_to t('.checkout'), new_order_path, :method => :get %><br>
<%= button_to t('.empty_cart'), 
    cart, 
    :method => :delete,
    data: {:confirm => 'Are you sure?' } %>

Bây giờ chúng ta sẽ “dịch” chuỗi hiển thị tiền từ USD sang VNĐ. Ở đây chúng ta chỉ hiển thị cho khác đi thôi chứ không chuyển đổi tỉ giá. Nếu muốn bạn có thể tự chuyển.

Để chuyển đổi tiền tệ thì chúng ta khai báo trong file .yml như sau:

English:

en:
  .
  .
  .
  number:
    currency:
      format:
        unit: "$"
        precision: 2
        separator: "."
        delimiter: ","
        format: "%u%n" 

Một số lưu ý như format là %u%n, thì trong đó %u là hiển thị kí tự tiền tệ, %n là số tiền, precision là số lượng chữ số sau phần thập phân, separator là kí tự ngăn cách giữa phần thập phân và phần đơn vị, delimiter là kí tự ngăn cách phần ngàn.

Tiếng Việt:

vi:
  .
  .
  .
  number:
    currency:
      format:
        unit: đồng
        precision: 3
        separator: ","
        delimiter: "."
        format: "%n %u"

Hàm number_to_currency() sẽ tự động tìm các giá trị trong file .yml để hiển thị cho đúng.

Rails – Đa ngôn ngữ – Phần 1

Chúng ta sẽ dùng I18n để hiển thị website trên nhiều ngôn ngữ khác nhau.

Đa ngôn ngữ là một bài toán khó, do đó chúng ta sẽ không thực hiện bằng cách dịch bình thường, mà thay vào đó là các chuỗi text cố định sẽ được hiển thị khác nhau với từng ngôn ngữ khác nhau.

Mặc định thì các ứng dụng Rails đều sử dụng I18n rồi, và ngôn ngữ duy nhất được sử dụng là tiếng Anh, các file ngôn ngữ sẽ được đặt trong thư mục config/locale với đuôi .yml, mặc định thư mục này chứa 1 file là en.yml:

# Files in the config/locales directory are used for internationalization
# and are automatically loaded by Rails. If you want to use locales other
# than English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
# I18n.t 'hello'
#
# In views, this is aliased to just `t`:
#
# <%= t('hello') %>
#
# To use a different locale, set it with `I18n.locale`:
#
# I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# To learn more, please read the Rails Internationalization guide
# available at http://guides.rubyonrails.org/i18n.html.

en:
 hello: "Hello world"

Chúng ta sẽ làm việc với các file này sau.

Bây giờ chúng ta sẽ thêm chức năng hỗ trợ URL đa ngôn ngữ.

Chúng ta sẽ đưa tham số ngôn ngữ vào sau đuôi URL, ví dụ http://localhost:3000/en/products.

Đầu tiên chúng ta sửa lại file routes.rb trong thư mục config như sau:

Rails.application.routes.draw do
    get 'admin/index'
    get 'sessions/new'
    get 'sessions/create'
    get 'sessions/destroy' 
    get 'store/index' 
 
    get 'admin' => 'admin#index'
 
    controller :sessions do
        get 'login' => :new
        post 'login' => :create
        delete 'logout' => :destroy
    end
 
    scope '(:locale)' do
        resources :users
        resources :orders
        resources :line_items
        resources :carts
        resources :products do
            get :who_bought, :on => :member
        end
        root :to => 'store#index', :as => 'store'
    end
end

Chúng ta gom các đoạn routing cho model – tức là các phương thức resources vào trong phương thức scope.

Phương thức scope ':locale' sẽ nối chuỗi trong biến :locale vào trước URL, tức là /products thì sẽ là en/products, ở đây chúng ta bọc :locale trong cặp dấu ngoặc tròn (), tức là có thể dùng hoặc không dùng cũng được.

Tiếp theo chúng ta cần khai báo biến :locale đó, chúng ta sửa lại lớp ApplicationController như sau:

class ApplicationController < ActionController::Base
    # Prevent CSRF attacks by raising an exception.
    # For APIs, you may want to use :null_session instead.
    before_filter :authorize
    before_filter :set_i18n_locale
 
    protect_from_forgery with: :exception
 
  private
 
    def current_cart
        Cart.find(session[:cart_id])
    rescue ActiveRecord::RecordNotFound
        cart = Cart.create
        session[:cart_id] = cart.id
        cart
    end
 
    helper_method :current_cart
 
  protected
 
    def authorize
        @user = User.find_by_id(session[:user_id]) 
        if @user == nil
            redirect_to '/login', :notice => 'You must login first'
        end
    end
 
    def set_i18n_locale 
        if params[:locale] 
            if I18n.available_locales.include?(params[:locale].to_sym)
                I18n.locale = params[:locale] 
            else
                flash.now[:notice] = params[:locale] + ' is not supported'               
            end
        end
    end
 
    def default_url_options 
        { :locale => I18n.locale }
    end
end

Phương thức set_i18n_locale sẽ được dùng trong phương thức before_filter, ở đây phương thức này sẽ kiểm tra xem trong URL gửi lên có tham số nào là :locale hay không, nếu có thì kiểm tra xem tham số đó có trong danh sách ngôn ngữ của I18n không, nếu có thì gán giá trị của tham số đó vào thuộc tính I18n.locale, không thì hiển thị lỗi.

Phương thức default_url_options là phương thức có sẵn của lớp ActionController::Base, phương thức này làm nhiệm vụ thiết lập các tham số trong lệnh HTTP được gửi lên. Chúng ta override phương thức này, ở đây chúng ta khai báo biến :locale có giá trị là giá trị của thuộc tính I18n.locale, tham số này sẽ được dùng trong phương thức helper là url_for.

Chúng ta sẽ tiếp tục làm việc với các phương thức này sau. Bây giờ nếu chúng ta trỏ đến URL http://localhost:3000/en/products thì sẽ không có gì khác biệt, còn nếu truyền vào một giá trị khác như http://localhost:3000/vi/products thì sẽ có dòng thông báo như ‘vi is not supported’.

Rails – Callback

Trong quá trình chạy ứng dụng, một đối tượng có thể được tạo ra, được cập nhật hoặc bị hủy thông qua các thao tác CRUD. Rails cung cấp cơ chế callback để chúng ta có thể kiểm soát trạng thái của các đối tượng này.

Callback là các phương thức/hàm được gọi trước hoặc sau khi có sự thay đổi trạng thái (như tạo, lưu, xóa, cập nhật, validate…) của đối tượng.

Ví dụ

Chúng ta sẽ không cho thực hiện chức năng xóa user nếu trong bảng chỉ còn lại một user.

Đầu tiên chúng ta sửa lại file layout một tí như sau:

<!DOCTYPE html>
<html>
<head>
<title>Books Store</title> 
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>
<body id="store">
    <div id="banner">
        <%= image_tag("logo.png") %>
        <%= @page_title || "Books Store" %>
    </div>
    <div id="columns">
        <div id="side">
            <div id="cart"> 
                <%= hide_cart_if(current_cart.line_items.empty?, :id => "cart") do %>
                    <%= render current_cart %> 
                <% end %>
            </div>
 
            <a href="#">Home</a><br />
            <a href="#">FAQ</a><br />
            <a href="#">News</a><br />
            <a href="#">Contact</a><br />
 
            <% if session[:user_id] %>
                <br/>
                <%= link_to 'Orders', '/orders' %><br />
                <%= link_to 'Products', '/products' %><br />
                <%= link_to 'Users', '/users' %><br />
                <%= button_to 'Logout', '/logout', :method => :delete %>
                <br/>
            <% else %>
                <br/>
                <%= link_to 'Log In', login_path %><br />
            <% end %>
        </div>
        <div id="main">
            <%= yield %>
        </div>
    </div> 
</body>
</html>

Chúng ta tạo thêm mấy nút bấm dẫn đến trang /products, /users, /orders, /logout nếu người dùng đã đăng nhập, nếu chưa thì hiển thị nút dẫn đến trang /login.

Tiếp theo chúng ta sửa lại lớp User như sau:

require 'digest/sha2'

class User < ActiveRecord::Base
    validates :name, :presence => true, :uniqueness => true
    validates :password, :confirmation => true
    attr_accessor :password_confirmation
    attr_reader :password 
 
    validate :password_must_be_present
 
    def User.encrypt_password(password, salt) 
        Digest::SHA2.hexdigest(password + salt)
    end
 
    def password=(password) 
        @password = password
 
        if password.present?
            generate_salt
            self.hashed_password = self.class.encrypt_password(password, salt)
        end
    end
 
    def User.authenticate(name, password) 
        if user = find_by_name(name) 
            puts encrypt_password(password, user.salt)
            if user.hashed_password == encrypt_password(password, user.salt) 
                user
            end
        end
    end
 
    after_destroy :check_user_empty
 
    def check_user_empty
        if User.count.zero?
            raise "Can't delete last user"
        end
    end
 
  private
    def password_must_be_present 
        if hashed_password.present? == false
            errors.add(:password, "Missing password")
        end
    end
 
    def generate_salt
        self.salt = self.object_id.to_s + rand.to_s
    end
end

Chúng ta định nghĩa phương thức check_user_empty, phương thức này kiểm tra xem trong bảng User có rỗng hay không, nếu rỗng thì giải phóng một lỗi exception.

Sau đó ở trên chúng ta gọi phương thức after_destroy :check_user_empty. Phương thức after_destroy là một phương thức callback, phương thức này sẽ gọi phương thức :check_user_empty mỗi khi có một thao tác nào đó liên quan đến câu lệnh DELETE trong cơ sở dữ liệu xảy ra. Tức là ở đây nếu người dùng bấm nút ‘Destroy’ để xóa user thì phương thức check_user_exist sẽ được gọi, và nếu bảng users trong CSDL không còn bản ghi nào thì một lỗi exception sẽ được sinh ra.

Và nếu sau lời gọi hàm callback mà có lỗi exception nào đó thì lỗi này sẽ được gửi ngược về nơi đã gọi ra nó, tức là ở đây tương ứng với lời gọi @user.destroy trong phương thức destroy của lớp UsersController. Ngoài ra lỗi exception cũng sẽ bắt buộc Rails phải “đảo ngược” câu truy vấn vừa thực hiện, tức là nếu xóa user mà có lỗi exception đó thì user đó sẽ được phục hồi nguyên vẹn trong CSDL.

Bây giờ chúng ta sửa lại phương thức destroy trong lớp UsersController để bắt exception như sau:

class UsersController < ApplicationController
    .
    .
    .
    # DELETE /users/1
    # DELETE /users/1.json
    def destroy 
        begin
            @user.destroy
            flash[:notice] = "User #{@user.name} deleted"
        rescue Exception => e
            flash[:notice] = e.message
        end
        respond_to do |format|
            format.html { redirect_to users_url }
            format.json { head :no_content }
        end
    end
    .
    .
    .
end

Nếu có lỗi exception xảy ra thì chúng ta chỉ đơn giản là thêm câu thông báo vào biến flash.

Nếu bạn muốn Rails chỉ thực hiện phục hồi dữ liệu chứ không muốn tạo một đối tượng exception nào thì trong lớp User, chúng ta cho giải phóng một đối tượng ActiveRecord::Rollback là được.

Ngoài ra còn có rất nhiều phương thức callback khác như:

CREATE

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_create
  • around_create
  • after_create
  • after_save
  • after_commit/after_rollback

UPDATE

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_update
  • around_update
  • after_update
  • after_save
  • after_commit/after_rollback

DELETE

  • before_destroy
  • around_destroy
  • after_destroy
  • after_commit/after_rollback

Bạn có thể tìm hiểu thêm tại đây.

Rails – Filter

Filter là các phương thức chạy trước, sau hoặc cùng với một phương thức action (phương thức của controller).

Các phương thức filter có tính thừa kế, tức là nếu chúng ta gọi các phương thức filter trong lớp ApplicationController, thì các lớp kế thừa nó cũng chạy các phương thức filter đó.

Trong phần này chúng ta sẽ sử dụng filter để chặn truy cập vào trang /admin/index nếu người dùng chưa đăng nhập.

Đầu tiên chúng ta sửa lại lớp ApplicationController như sau:

class ApplicationController < ActionController::Base
    # Prevent CSRF attacks by raising an exception.
    # For APIs, you may want to use :null_session instead.
    before_filter :authorize
 
    protect_from_forgery with: :exception
 
  private
   
    def current_cart
        Cart.find(session[:cart_id])
            rescue ActiveRecord::RecordNotFound
            cart = Cart.create
            session[:cart_id] = cart.id
        cart
    end
 
    helper_method :current_cart
 
  protected
 
    def authorize
        @user = User.find_by_id(session[:user_id]) 
        if @user == nil
            redirect_to '/login', :notice => 'You must login first'
        end
    end
end

Filter có 3 loại là before, afteraround, tương ứng với chạy trước, sau hoặc chạy cùng.

Ở đây chúng ta dùng phương thức before_filter, và truyền vào tham số :authorize, đây là phương thức kiểm tra xem người dùng có đăng nhập hay chưa do chúng ta tự định nghĩa. Nếu người dùng chưa đăng nhập thì chúng ta cho trỏ tới trang /login.

Phương thức authorize sẽ chạy trước các phương thức action khác, tuy nhiên có một vấn đề, như đã nói ở trên là các filter sẽ được thừa kế, tức là các lớp controller con cũng sẽ chạy dòng before_filter ở trên. Trong số các controller kế thừa có cả controller chịu trách nhiệm việc hiển thị trang /login, tức là ở đây chúng ta không cho người dùng đăng nhập luôn.

Để giải quyết việc này thì chúng ta dùng các phương thức skip, ví dụ chúng ta thêm dòng này vào đầu lớp SessionsController như sau:

class SessionsController < ApplicationController
    skip_before_filter :authorize
    .
    .
    .
end

Có thể hiểu phương thức skip_before_filter :authorize sẽ đưa phương thức authorize vào danh sách “đen”, tức là không được chạy. Và do đó chúng ta có thể chạy các phương thức khác bình thường.

Và không chỉ có lớp SessionsController mà tất cả các lớp controller con khác cũng cần được “mở khóa” nữa. Chúng ta lần lượt sửa lại như sau:

Lớp StoreController:

class StoreController < ApplicationController
    skip_before_filter :authorize
    .
    .
    .
end

Lớp CartsController:

class CartsController < ApplicationController
    skip_before_filter :authorize, :only => [:create, :update, :destroy]
    .
    .
    .
end

Chúng ta có thể truyền vào tham số :only, tham số này sẽ quy định chỉ có một số phương thức nhất định được “mở khóa”.

Lớp LineItemsController:

class LineItemsController < ApplicationController
    skip_before_filter :authorize, :only => :create
    .
    .
    .
end

Lớp OrdersController:

class OrdersController < ApplicationController
    skip_before_filter :authorize, :only => [:new, :create]
    .
    .
    .
end

Vậy là xong, bây giờ nếu chúng ta vào trang /admin/index hay một số trang nhất định mà chưa đăng nhập thì chúng ta sẽ được trỏ về trang /login.

Bạn có thể tìm hiểu thêm về các phương thức filter tại đây.