Jednoduchý návod, jak používat i18n taglibs spolu s ukládáním překladů v databázové tabulce a nikoli v .properties souborech.

Před nějakou dobou jsem sháněl na internetu návod jak sprovoznit překládání stránek jsp pomocí i18n(zkratka anglického Internationalization) taglibu tak,že se překlady budou nacházet v databázi,a ne v .properties souboru, tak jak je to standartně.

Nevýhodou těchto souborů je jejich špatná editovatelnost (špatně dostupné při editaci přes internet), nutnost převádění na ascii kódování, nezaznamenávání výrazů, které jsou použity na stránkých ale nejsou přeloženy…

Oproti tomu výhody ukládání slovníku jsou zřejmé, snadné vytvoření editačního rozhraní, při chybějícím překladu se prostě slovo zanese do databáze a čeká na přeložení, všechny překlady na jediném místě,ne v několika souborech.

Problém je to, že pokud chceme jako backend používat databázi,je třeba provést několik kroků:

  1. vytvoření vhodné tabulky v databázi
  2. Vytvoření třídy Dictionary, která se stará a práci s databází, generuje HashMapu s překladem do daného jazyka,a vkládá slovo do databáze,pokud v ní nebylo nalezeno
  3. Vytvoření třídy mujResource rozšiřující třídu java.util.ResourceBundle
  4. Nastavení jsp stránek tak, aby používaly náš resourceBundle
  5. Zbytek je standartní a stejný, tak jako při použití .properties souborů
  6. Přepínání jazyků proměnnou v session

1) Tabulka Translate v databázi
Vzhledem k tomu, že překladový klíč musí být unikátní, ideální je použít ho i jako primary key v tabulce, zajistí se tak jeho unikátnost, stejně tak jako optimalizace při vyhledávání v tabulce podle klíče překladu
Struktura tabulky by pak mohla být

- varchar(50) - – text - – text - – text -
– key - – cs - – en - – de -

tedy pro každý jazyk bude jeden sloupec v tabulce

2) Třída Dictionary
Má na starosti práci se samotnou databází a generování hashMapy slovník.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;

public class Dictionary {

    String machine;
    String db;
    String user;
    String pass;
    String dbUri;
    HashMap slovnik;
    String lang;
    public final String not_found = "NOT TRANSLATED";

    public Dictionary(String lang) {

        slovnik = new HashMap();
        this.machine = "localhost";
        this.db = "databaze";
        this.user = "uzivatel";
        this.pass = "heslo";
        this.dbUri = "jdbc:postgresql://" + machine + "/" + db +
"?user=" + user + "&password=" + pass +
"&characterEncoding=utf-8";
        this.lang = lang;
        Load();
    }

    private void Load() {
        try {
            Class.forName("org.postgresql.Driver");
            Connection con = DriverManager.getConnection(dbUri);
            Statement s = con.createStatement();
            String sql = "SELECT key, " + lang + " FROM \"Translate\"";
            ResultSet rs = s.executeQuery(sql);
            while (rs.next()) {
                slovnik.put((String) rs.getString("key"), (String) rs.getString(lang));
            }
            rs.close();
            s.close();
            con.close();
        } catch (Exception e) {
            System.out.println("Load:"+e.toString());
        }
    }

    public HashMap getDictionary() {
        return this.slovnik;
    }

    public boolean isKeyInDB(String key){
        int pocet=0;
          try {
            Class.forName("org.postgresql.Driver");
            Connection con = DriverManager.getConnection(dbUri);
            Statement s = con.createStatement();
            String sql = "SELECT COUNT(*) FROM \"Translate\" WHERE key='"+key+"'";
            ResultSet rs = s.executeQuery(sql);
            rs.next();
            pocet=rs.getInt("count");
            rs.close();
            s.close();
            con.close();
        } catch (Exception e) {
            System.out.println("isKeyInDB:"+e.toString());
        }
        if(pocet>0){
            return true;
        }else{
            return false;
        }
    }

    private ArrayList existingLanguages() {
        ArrayList languages=new ArrayList();
        try {
            Class.forName("org.postgresql.Driver");
            Connection con = DriverManager.getConnection(dbUri);
            Statement s = con.createStatement();
            String sql = "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.Columns
 WHERE TABLE_NAME = 'Translate'";
            ResultSet rs = s.executeQuery(sql);
            rs.next();
            while (rs.next()) {
                languages.add((String) rs.getString(1));
            }
            rs.close();
            s.close();
            con.close();
        } catch (Exception e) {
            System.out.println(e.toString());
        }
        return languages;
    }

    public void putKey(String key) {
        if(!isKeyInDB(key)){
        try {
            Class.forName("org.postgresql.Driver");
            Connection con = DriverManager.getConnection(dbUri);
            Statement s = con.createStatement();
            String sql="INSERT INTO \"Translate\" (key) VALUES('"+key+"')";
            boolean rs = s.execute(sql);
            s.close();
            con.close();
        } catch (Exception e) {
            System.out.println("putKey:"+e.toString());
        }
      }
    }
}

Takže popořádku, v konstruktoru vytvoříme spojení přes jdbc s databází ( v příkladu postgreSQL). Nastavíme jazyk překladu,a do hashMapy pomocí metody Load() načteme slovník ve tvaru klíč-překlad. Pro pořádek, hashMapa neumožnuje duplicitní klíče, což odpovídá našemu záměru vyhnout se duplicitám i v databázi).

Metoda getDictionary() vrátí hashMapu se slovníkem, tuto metodu používá resourceBundle pro získání slovníku.

Metoda isKeyInDB() ověřuje,zda je klíč v databázi již obsažen. Uvažujme následující situaci, v databázi máme slovo, které má překlad v češtině,ale v angličtině dosud přeloženo nebylo, Tedy resourceBundle ho nenajde ve slovníku, a pokusí se ho vložit do databáze, protože si myslí že jej neobsahuje. Pokud ale slovo existuje,jen ne v daném jazyce, databáze bude hlásit chybu,že daný klíč již existuje, a není tedy možné ho vložit. Proto nejprve otestujeme zda již klíč není obsažen, byť v jiném jazyce.

Metoda existingLanguages() vrací arrayList jazyků, které jsou evidovány v databázi.

Poslední metodou je putKey() která je volána v případě nenalezení klíče ve slovníku. Nejprve otestuje zda slovo není jen nepřeloženo do daného jazyka, ale databáze již příslušný řádek obsahuje, jinak ho vytvoří.

Třída slovník tedy nemá přímou návaznost na nternacionalizaci, je pouze služební třídou pro pokračování.

3) Vlastní resourceBundle
Tato třída je velmi jednoduchá, pouze získá slovník od třídy Dictionary, zjistí zda obsahuje slovník hledaný klíč, pokud ano, vrátí ho jako String, jinak se pokusí uložit ho do databáze, a vrátí konstantu značící, že text není přeložen.

import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;

public class mujResource extends java.util.ResourceBundle{

    HashMap slovnik;
    String slovo="";
    Dictionary d;

    public mujResource() {
        d= new Dictionary("en");
        slovnik=d.getDictionary();

    }

    @Override
    protected Object handleGetObject(String key) {

        this.slovo=(String) this.slovnik.get(key);
        if(slovo!=null)
            return slovo;
        else{
            d.putKey(key);
            return d.not_found;
        }
    }

    @Override
    public Enumeration<String> getKeys() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

}

Zde nastává v zásadě nejvetší komplikace. Pro každý jazyk je třeba vytvořit jednu takovou třídu, s volbou jazyka. třída mujResource bude použita,v případě že se pokoušíme načíst řeč, pro kterou překlad není nastaven, nebo pokud nelze použít jiný vhodnější. Třída pro češtinu by se tedy měla jmenovat mujResource_cs, pro angličtinu mujResource_en, pro němčinu mujResource_de, a podobně. Tyto třídy je vytvoříme prostým skopírováním původní, a přejmenováním, spolu s nahrazením jazyka v řádku
d= new Dictionary("en");
kde „en“ nahradíme jazykem podle jména třídy.

4) Nastavení JSP stránek

Na začátek stránky vložíme kód:

<i18n:bundle baseName="translation.mujResource"
             localeRef="userLocale"
             scope="request"
             changeResponseLocale="true"

                         /></code></pre>

Kde v atribudu baseName je cesta k dané standartní třídě slovníku, v mém případě v balíčku translation

Pro kompletnost si uvedeme i jak si na stránce vyžádat překlad:

<i18n:message key="informace" />

Pro běh tohoto taglibu je třeba ještě nastavit v web.xml cestu k tld souboru

<taglib>
    <taglib-uri>http://jakarta.apache.org/taglibs/i18n-1.0</taglib-uri>
    <taglib-location>/WEB-INF/taglibs-i18n.tld</taglib-location>
  </taglib>

a ve složce lib mít uložen jar soubor taglibs-i18n.jar

6) Volba jazyků v aplikaci
Jedna z možností je vytvořit servlet, který bude do session ukládat proměnnou s názvem userLocale a její hodnotou dané locale, např:

session.setAttribute("userLocale",new Locale("cs","CZ"));

Úryvek ze servletu:

public void doGet(HttpServletRequest request,
           HttpServletResponse response)
           throws ServletException, IOException {
       request.setCharacterEncoding("UTF-8");
       response.setCharacterEncoding("UTF-8");

       HttpSession session = request.getSession();

      String lang = request.getParameter("language");
 if ( "en".equals(lang) )
   {
   session.setAttribute("userLocale",Locale.ENGLISH);
   }

Závěr

Po provdedení těchto několika kroků bude možné překlady uchovávat v databázi, při nově překládané stráce se všechny klíče uloží samy do databáze, a není již nijak komplikované vytvořit editační rozhraní pro doplnění překladů ke klíčum. Důležité je podotknout, že proměnná slovnik se vytváří při prvním vyžádání, a do restartu servlet kontejneru se nemění. To znamená dvě veci: zátěž na databázi negenerujeme během celého spuštění aplikace,ale jen při prvním, a do restartu kontejneru se nově přeložené fráze nikde neprojeví. Proto po přeložení vetší části klíčů nezapoměnte provést restart.

Třídy zde uvedené jistě nejsou optimalizované a ošetřené tak,jak by měly být, ale základní funkčnost splňují, Každý si tak může následně doplnit třídy dle libosti.

Jakékoli dotazy rád zodpovím,ať již na emailu data_4 (at) seznam.cz nebo v komentářím pod článkem.