I18n - Internacionalizace

Z FI WIKI
Přejít na: navigace, hledání


Webové aplikace zpřístupněné na Internetu by měly počítat s tím, že jejich uživatelé mluví různými jazyky, používají různé abecedy a obrázková písma, píšou různými směry tj. zleva doprava nebo naopak, jsou zvyklí zapisovat časové údaje, čísla a množství peněz podle svých národních zvyklostí, a mohou se nacházet v libovolném časovém pásmu. Různé abecedy a národní zvyklosti způsobují i různá pravidla pro seřazování textů podle abecedy či převádění malých písmen na velká a naopak.

Představte si například server s webovou aplikací běžící v Brně, ke kterému přistupuje občan USA mluvící anglicky, jenž je v té chvíli na služební cestě v Japonsku. Takový uživatel očekává, že časové údaje (čas kdy došlo k nějaké události, např. příchodu emailu) budou zobrazeny v jeho místním (tj. japonském) čase, nikoliv ve středoevropském čase, který se liší o mnoho hodin. Bude očekávat, že čísla používají desetinou tečku a řády tisíců jsou odděleny čárkou (1,000,000.5), což je odlišné od české konvence, kdy čísla používají desetinou čárku a řády tisíců jsou odděleny mezerou (1 000 000,5). Označení měny bude očekávat před číslem ($100), nikoliv za číslem (100Kč). Bude užitečné, když zobrazené texty budou v angličtině, nikoliv v češtině. A pokud si bude chtít třeba poznamenat nějaký název místní japonské pamětihodnosti, může chtít zadat text v obrázkovém písmu.

Příprava aplikace na podporu různých kulturních zvyklostí se nazýva internacionalizace, používá se zkratka i18n, protože anglické slovo internationalization má mezi "i" a "n" 18 písmen. Úprava internacionalizované aplikace pro konkrétní jazyk se pak nazývá lokalizace, spočívá především v přeložení textů do konkrétního jazyka.

Java má pro tyto účely velice propracované nástroje. Zobrazování čísel a časových údajů řeší třídy java.text.NumberFormat a java.text.DateFormat. Zobrazení časových údajů ve správné časové zóně řeší třída java.util.TimeZone. A nalézání textů ve správném jazyce řeší třída java.util.ResourceBundle. Všechny tyto třídy používají třídu java.util.Locale pro specifikaci jazyka či národních zvyklostí.

Čísla a měna

Ukázka rozdílu zobrazování ceny v ČR a USA:

 
Locale cestina = new Locale("cs", "CZ");
Locale usa = Locale.US;
 
double rcislo = 1234567.1234;
 
NumberFormat csFormat = NumberFormat.getCurrencyInstance(cestina);
NumberFormat usaFormat = NumberFormat.getCurrencyInstance(usa);
 
System.out.println(cestina.toString()+"     " + csFormat.format(rcislo));
System.out.println(usa.toString()+"     " + usaFormat.format(rcislo));

výsledek:

cs_CZ    1 234 567,12 Kč
en_US    $1,234,567.12

Časové údaje

Malá ukázka jak správně zobrazovat časové údaje:

 Locale cestina = new Locale("cs","CZ");
 Locale usa = Locale.US;
 TimeZone stredoevropskyCas = TimeZone.getTimeZone("CET");
 TimeZone vychodniUsaCas = TimeZone.getTimeZone("EST");
 
 DateFormat csfull = DateFormat.getDateTimeInstance(DateFormat.FULL,DateFormat.FULL,cestina);
 csfull.setTimeZone(stredoevropskyCas);
 
 DateFormat usafull = DateFormat.getDateTimeInstance(DateFormat.FULL,DateFormat.FULL,usa);
 usafull.setTimeZone(vychodniUsaCas);
 
 Date praveTed = new Date();
 
 System.out.println(cestina.toString()+": "+csfull.format(praveTed));
 System.out.println(usa.toString()+": "+usafull.format(praveTed));

vypíše ten stejný časový údaj podle českých a amerických zvyklostí, a přepočtený na středoevropský a americký východní čas. Všimněte si, že o letní a zimní čas je postaráno:

cs_CZ: Úterý, 12. září 2006 14:10:15 CEST
en_US: Tuesday, September 12, 2006 8:10:15 AM EDT

Seřazování podle abecedy

Abecedy se liší množinami znaků, a pokud se překrývají, i jejich pořadím. Proto je vhodné používat třídu java.text.Collator pro porovnávání řetězců:

 
String[] pole = new String[]{
        "Cecilka",
        "Chranislava",
        "Ilona",
        "Dana",
        "Anička"
};
 
Locale cestina = new Locale("cs", "CZ");
Locale usa = Locale.US;
 
final Collator csCollator = Collator.getInstance(cestina);
final Collator usaCollator = Collator.getInstance(usa);
 
Comparator<String> csComp = new Comparator<String>() {
    public int compare(String s1, String s2) {
        return csCollator.compare(s1,s2);
    }
};
 
Comparator<String> usaComp = new Comparator<String>() {
    public int compare(String s1, String s2) {
        return usaCollator.compare(s1,s2);
    }
};
 
Arrays.sort(pole,csComp);
System.out.println("cs : "+Arrays.asList(pole));
Arrays.sort(pole,usaComp);
System.out.println("usa: "+Arrays.asList(pole));

výsledek:

cs : [Anička, Cecilka, Dana, Chranislava, Ilona]
usa: [Anička, Cecilka, Chranislava, Dana, Ilona]

ResourceBundle texty

Texty by neměly být v aplikaci nikdy přímo v kódu, ať už Java kódu nebo uvnitř JSP. Správný postup je používat třídujava.util.ResourceBundle. V Java kódu pak odkazujeme na texty jen pomocí klíčů, a konkrétní text se dohledává podle specifikovaného Locale. Ukázka:

 ResourceBundle csTexty = ResourceBundle.getBundle("Texty",cestina);
 System.out.println(csTexty.getString("ted_je_prave"));

Metoda getBundle() se pokouší najít pro dané Locale soubor, jehož jméno je tvořeno zadaným řetězcem, určením locale a příponou .properties, a pokud jej nenalezne, zkouší hledat soubor s obecnějším locale. V tomto případě tedy zkouší hledat soubory

  • Texty_cs_CZ.properties
  • Texty_cs.properties
  • Texty.properties

Podrobnosti viz javadoc ke třídě ResourceBundle. Soubor s texty má mít formát určený třídou java.util.Properties, tedy řetězcové klíče následované rovnítkem a řetězcovou hodnotou, kde ne-ASCII znaky jsou zapsány pomocí \uXXXX konvence (hexadecimálně číslo UNICODE znaku), např:

#Texty_cs_CZ.properties
ted_je_prave=Te\u010f je pr\u00e1v\u011b
#Texty_en_US.properties
ted_je_prave=It is now 

Pro převod českých znaků na \uXXXX tvar můžeme použít nástroj native2ascii, nebo vývojová prostředí mohou provést konverzi automaticky. Alternativně můžeme použít XML formát, což umožní použít přímo např. utf-8.

Parametry a skloňování

Do textů je možné vkládat hodnoty, a dokonce skloňovat podle čísel. Např. máme seznam měst a počty tuleňů v místní ZOO. Chceme vypsat:

V ZOO Ostrava nežije žádný tuleň
V ZOO Brno žije jeden tuleň
V ZOO Jihlava  žijí 2 tuleni
V ZOO Znojmo  žijí 3 tuleni
V ZOO Praha  žijí 4 tuleni
V ZOO Tokyo  žije 5 tuleňů
V ZOO Sydney  žije 6 tuleňů
V ZOO Reykjavik  je moc tuleňů

Toho dosáhneme definicí textu

tuleni=V ZOO {0} {1,choice,0#nežije žádný tuleň|1#žije jeden tuleň|2# žijí {1} tuleni|5# žije {1} tuleňů|6< je moc tuleňů}

a kódem

 
 Locale cestina = new Locale("cs");
 ResourceBundle csTexty = ResourceBundle.getBundle("Texty",cestina);
 MessageFormat veta = new MessageFormat(csTexty.getString("tuleni"));
 String[] mesta = new String[] { "Ostrava", "Brno", "Jihlava", "Znojmo", "Praha", "Tokyo", "Sydney", "Reykjavik"};
 for(int i=0;i<mesta.length;i++) {
     System.out.println(veta.format(new Object[] { mesta[i], i }));
 }
 

Java a UNICODE

Java používá vnitřně pro zápis znaků sadu UNICODE. Aby to nebylo jednoduché, UNICODE se postupně vyvíjí. V době vzniku Javy obsahovala sada UNICODE méně než 65536 znaků, proto bylo možné každý znak zapsat do dvou bajtů pořadovým číslem znaku v rámci sady UNICODE, a proto má Javový typ char rozsah dvou bajtů. Bohužel dnešní verze UNICODE 5.0 obsahuje znaků mnohem více. Proto bylo stanoveno, že dnes typ char, a potažmo String jakožto obal pole znaků, vyjadřuje znaky v kódování UTF-16, tedy některé znaky je nutné vyjádřit pomocí dvojic charů, tzv. surrogate pairs. Naštěstí všechny znaky, kterých se to týká, pocházejí z dnes již mrtvých jazyků, a proto je pravděpodobnost jejich použití velmi malá.

UNICODE je abstraktní sada znaků, seřazených a očíslovaných od 0 do potenciálně 1114112 (hexadecimálně 0x110000). V současnosti (UNICODE 5.0) je přiřazeno 101063 znaků. Pokud chceme znak v UNICODE zapsat, musíme si zvolit kódování (encoding). Pozor, pro zmatení je v MIME hlavičce Content-type kódování označeno parametrem s názvem charset, což je nesmysl, ale historicky vžitý. Na výběr máme tato kódování

UTF-32 4 bajty na každý znak
UTF-16 2 bajty na většinu znaků, občas 2 páry bajtů
UTF-8 1 až 6 bajtů na znak
8bitové (iso-8859-2, windows-1250, ...) vždy 1 bajt na znak, ale většinu znaků nelze vyjádřit

Zápis znaků při styku s vnějším světem

Uvnitř Javy není nutné kódování znaků jakkoliv řešit, char je znak a String je obal na char[]. Ale při styku s vnějším světem je nutné znaky převést na bajty a naopak. To je nutné typicky na těchto místech:

  • čtení a zápis souborů, komunikace přes síť (sockety)

je nutné specifikovat kódování když obalujeme java.io.InputStream (resp. OutputStream) java.io.Readerem (resp. Writerem)

 
 BufferedReader in = new BufferedReader(
                      new InputStreamReader(
                       new FileInputStream("soubor_win.txt"),
                       "windows-1250"
                      )
                     );
 PrintWriter out = new PrintWriter(
                    new BufferedWriter(
                     new OutputStreamWriter(
                      socket.getOutputStream(),
                      "utf-8"
                     )
                    )
                   );
  • práce s databází

je odpovědností JDBC ovladače správně znaky zapsat, není tedy nutné nic dělat, maximálně nastavit nějakou property JDBC driveru

  • generování webových stránek
je nutné zajistit zavolání
response.setContentType("text/html; charset=utf-8");
  • přijímání dat z webového prohlížeče
je nutné zajistit zavolání
request.setCharacterEncoding("utf-8");(

I18n v Servletech

Webové prohlížeče hlásí, kterým jazykům uživatel rozumí, v sestupném pořadí, v HTTP hlavičce Accept-Language. V Servlet API lze získat buď první preferovaný jazyk voláním

 
Locale l = request.getLocale()

nebo všechny preferované jazyky voláním

 
Enumeration<Locale> en = request.getLocales()

Pozor, čeština je hlášena jako locale cs, tedy ne cs_CZ, proto ResourceBundle soubory s názvy končícími na _cs_CZ.properties nebudou nalezeny !

I18n v JSTL

Při použití ve webové aplikaci můžeme s výhodou použít Formatting knihovnu JSTL. Pro zobrazení času (uloženého v proměnné ted) a doprovodného textu uloženého pod klíčem ted_je_prave můžeme použít:

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="f" uri="http://java.sun.com/jsp/jstl/fmt" %>
<f:setBundle basename="Texty" />
<f:message key="ted_je_prave" />: <f:formatDate value="${ted}" dateStyle="full" timeStyle="full" timeZone="${zona}" />

Zatímco locale (požadovaný jazyk) hlásí webový prohlížeč v HTTP hlavičce Accept-language, a tudíž ho nemusíme nastavovat, protože značky z Formatting JSTL knihovny si ho zjistí samy, požadovanou časovou zónu prohlížeč nehlásí. Ale můžeme uživatele nechat časovou zónu vybrat, seznam dostupných zón vrací metoda TimeZone.getAvailableIDs().

České znaky v JSP stránkách

Jak už bylo vysvětleno v hesle servlety, pro správnou funkci webové aplikace je nejlepší používat důsledně kódování znaků UTF-8 na vstupu i výstupu. Častou chybou je, že vývojáři mají české texty přímo v JSP stránce, a spletou si výstupní kódování stránky s kódováním zdroje JSP stránky. Pokud máme JSP stránku uvozenou:

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="iso-8859-2"%>

pak generovaná HTML stránka bude používat kódování UTF-8, ale zdroj JSP stránky je v kódování iso-8859-2. Uvědomme si, že JSP stránka je na pozadí převedena na servlet, tedy Java třídu, a ta je zkompilována. Veškeré texty tak budou převedeny na řetězce v Java zdrojovém kódu, a kompilátor potřebuje vědět, jaké vstupní kódování má použít.

Pro vstup znaků z HTML formulářů je nejlepší použít jednoduchý filter:

 
package cz.moje;
import java.io.IOException;
import javax.servlet.*;
 
public class SetCharacterEncodingFilter implements Filter {
 
    public void init(FilterConfig filterConfig) throws ServletException { }
 
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (request.getCharacterEncoding() == null) {
            request.setCharacterEncoding("utf-8");
        }
        chain.doFilter(request, response);
    }
}

a namapovat jej na všechny příchozí requesty (editováním souboru web.xml):

   <filter>
       <filter-name>kodovani</filter-name>
       <filter-class>cz.moje.SetCharacterEncodingFilter</filter-class>
   </filter>
   <filter-mapping>
       <filter-name>kodovani</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>

Tím je o vstupní znaky postaráno.