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, což zahrnuje národní zvyklosti pro
    • psaní různými směry tj. zleva doprava nebo naopak
    • seřazování textů podle abecedy
    • převádění malých písmen na velká
  • jsou zvyklí zapisovat časové údaje, čísla a množství peněz podle svých národních zvyklostí
  • mohou se nacházet v libovolném časovém pásmu
  • mohou mít nezvyklá jména, např. jen jedno jméno, nebo dvě příjmení, různé přídomky atp.

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 (100 Kč). 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 NumberFormat a DateFormat. Zobrazení časových údajů ve správné časové zóně řeší třída TimeZone. A nalézání textů ve správném jazyce řeší třída ResourceBundle.

Locale

Všechny tyto třídy používají třídu Locale pro specifikaci jazyka či národních zvyklostí. Pozor, od Javy 7 došlo v této třídě ke změnám. Zatímco do verze 6 byl správný postup pro vytvoření českého Locale

 
 Locale cestina = new Locale("cs", "CZ");

od verze 7 je doporučovaný způsob

 
 Locale cestina = Locale.forLanguageTag("cs-CZ");

Pro češtinu nejde o podstatnou změnu, změna má význam zejména u jazyků, které používají více abeced, např. uzbečtina nebo čínština. Detaily viz Java Locale Enhancement.

Čí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;
 
BigDecimal rcislo = new BigDecimal("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

Ovšem pozor, stejný počet korun a dolarů má rozdílnou hodnotu. Proto je vhodné formátu explicitně nastavit, kterou měnu má zobrazovat:

Currency usd = Currency.getInstance("USD");
 
csFormat.setCurrency(usd);
usaFormat.setCurrency(usd);

výstup je pak lokalizovaný, ale jedná se v obou případech o stejnou měnu:

cs_CZ     1 234 567,12 USD
en_US     $1,234,567.12

Číselné typy pro měnu

Pro cenu je vhodné používat typ BigDecimal, naopak nevhodné je používat float nebo double, protože u nich kvůli zaokrouhlovacím chybám může docházet ke ztrátám haléřů, centů a podobně. Program

 
 float suma = 0f;
 for (int i = 0; i < 100; i++) suma += 0.01f;
 System.out.println("100x 1 haléř = " + suma + " Kč");

vytiskne

100x 1 haléř = 0.99999934 Kč

kdežto program

 
 BigDecimal suma = BigDecimal.ZERO;
 BigDecimal haler = new BigDecimal("0.01");
 for (int i = 0; i < 100; i++) suma = suma.add(haler);
 System.out.println("100x 1 haléř = " + suma + " Kč");

vytiskne

100x 1 haléř = 1.00 Kč

Časové údaje

Motivace

https://www.youtube.com/watch?v=-5wpm-gesOY

Java 1 až 7 - Date, GregorianCalendar a TimeZone

Až do příchodu Javy 8 se pro práci s časem používaly tyto třídy:

  • třída Date reprezentuje časový okamžik jako takový, vyjádřený v milisekundách od začátku unixové epochy (půlnoc 1.1.1970 GMT). Interně používá typ long, takže může reprezentovat čas zhruba v úseku 584 milionů let (konkrétně od 2. 12. 292 269 055 př.n.l. do 17. 8. 292 278 994).
  • třída Calendar a její potomek GregorianCalendar reprezentují časový okamžik vyjádřeným lidským způsobem (tedy způsobem závislým na určité kultuře)
  • třída TimeZone reprezentuje časovou zónu včetně případných přechodů mezi zimním a letním časem (anglicky daylight savings)

Ukázka jak zobrazovat časové údaje:

 
    showLocalTime(new Locale("cs", "CZ"), TimeZone.getTimeZone("Europe/Prague"));
    showLocalTime(             Locale.US, TimeZone.getTimeZone("America/Atka"));
    showLocalTime(new Locale("ar", "SA"), TimeZone.getTimeZone("Asia/Riyadh89"));
    showLocalTime(new Locale("en", "NZ"), TimeZone.getTimeZone("Pacific/Auckland"));
 
//...
 
public static void showLocalTime(Locale locale, TimeZone tz) {
        Date now = new Date();
        String zoneName = tz.getDisplayName(tz.inDaylightTime(now),TimeZone.LONG,locale);
        DateFormat full = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, locale);
        full.setTimeZone(tz);
        System.out.println(locale + ": " + full.format(now)+ " ("+zoneName+")");
    }

vypíše tentýž časový údaj podle místních zvyklostí u nás, ve městě Atka (Aljaška, USA), v Rijádu v Saudské Arábii a v Aucklandu na Novém Zélandu. Všimněte si, že na Novém Zélandu mají v březnu letní čas (je to jižní polokoule) a mají jiné datum (leží těsně na západ od datové hranice), v Rijádu je zase časový posun i o minuty, nejen celé hodiny:

cs_CZ: Středa, 9. březen 2011 13:00:00 CET (Central European Time)
en_US: Wednesday, March 9, 2011 2:00:00 AM HAST (Hawaii-Aleutian Standard Time)
ar_SA: 09 مارس, 2011 GMT+03:07 03:07:04 م (GMT+03:07)
en_NZ: Thursday, 10 March 2011 1:00:00 AM NZDT (New Zealand Daylight Time)

Posuny kalendáře:

 
//vytvořit konkrétní časový okamžik 
GregorianCalendar c1 = new GregorianCalendar(TimeZone.getTimeZone("CET"), cestina);
c1.clear(); //vynulovat včetně milisekund
c1.set(2014, Calendar.JANUARY, 31, 18, 30, 0); //měsíce se počítají od NULY !!!
        
//formátovat pro výstup
DateFormat csfull = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, cestina);
System.out.println(csfull.format(c1.getTime()));
        
//přičíst měsíc
GregorianCalendar c2 = (GregorianCalendar) c1.clone();
c2.add(Calendar.MONTH, 1);
System.out.println(csfull.format(c2.getTime()));

nastaví 31.1.2014 18:30 a pak zjistí, který okamžik je o měsíc později:

Pátek, 31. leden 2014 18:30:00 CET
Pátek, 28. únor 2014 18:30:00 CET

Joda time

Třídy Date a GregorianCalendar jsou dost neohrabané. Existuje mnohem pohodlnější alternativa, knihovna Joda time.

Příklad uvedený výše lze s použitím Joda time přepsat:

 
DateTime t1 = new DateTime(2014, 1, 31, 18, 30, 0, DateTimeZone.forID("CET"));
DateTimeFormatter dtf = DateTimeFormat.fullDateTime().withLocale(cestina);
System.out.println(dtf.print(t1));
System.out.println(dtf.print(t1.plusMonths(1)));

Java 8 - Instant, ZonedDateTime a ZoneId

Od Javy verze 8 bylo zcela předěláno rozhraní pro práci s časem, podle vzoru Joda time.

Třída java.time.Instant představuje časový okamžik vyjádřený sekundami a nanosekundami od epochy. Jeho rozsah je větší než u java.util.Date, a to plus mínus miliarda let, konkrétně od Instant.MIN po Instant.MAX.

Kód odpovídající ukázce lokálních časů pro Javu 7 tak v Javě 8 bude vypadat:

 
        showLocalTime(Locale.forLanguageTag("cs-CZ"), ZoneId.of("Europe/Prague"));
        showLocalTime(Locale.US, ZoneId.of("America/Atka"));
        showLocalTime(Locale.forLanguageTag("ja-JP"), ZoneId.of("Asia/Tokyo"));
        showLocalTime(Locale.forLanguageTag("en-NZ"), ZoneId.of("Pacific/Auckland"));
    }
 
    public static void showLocalTime(Locale locale, ZoneId tz) {
        ZonedDateTime now = ZonedDateTime.now(tz);
        String zoneName = tz.getDisplayName(TextStyle.FULL, locale);
        DateTimeFormatter full = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withLocale(locale);
        System.out.println(locale + ": " + full.format(now)+ " ("+zoneName+")");
 
    }

a vypíše

cs_CZ: Úterý, 18. února 2014 13:07:15 CET (Central European Time)
en_US: Tuesday, February 18, 2014 2:07:15 AM HAST (Hawaii-Aleutian Time)
ja_JP: 2014年2月18日 21時07分15秒 JST (日本時間)
en_NZ: Wednesday, 19 February 2014 1:07:15 AM NZDT (New Zealand Time)

a vytvoření konkrétního časového okamžiku v určité časové zóně a jeho posunutí o měsíc bude vypadat:

 
 ZonedDateTime t1 = ZonedDateTime.of(2014, 1, 31, 18, 30, 0, 0, ZoneId.of("CET"));
 DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withLocale(locale);
 System.out.println(dtf.format(t1));
 System.out.println(dtf.format(t1.plusMonths(1)));

Řazení 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 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);
 
Arrays.sort(pole,csCollator);
System.out.println("cs : "+Arrays.asList(pole));
Arrays.sort(pole,usaCollator);
System.out.println("usa: "+Arrays.asList(pole));

výsledek:

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

Nicméně standardní Collator neřadí zcela podle české normy, ignoruje mezery a velká písmena umisťuje za malá, což má být naopak. Podrobnosti jsou popsány v článku České řazení na serveru java.cz.

Jména

Lidé z různých kultur mají velice různá pravidla pro tvoření jmen. Detaily se lze dočíst na stránce Personal names around the world. Je chybou předpokládat, že lidé mají křestní jméno a příjmení jako v češtině, tomu je nutné přizpůsobit i položky formulářů a sloupce v databázích.

Na Islandu nemají příjmení, ale jméno určující otce (anglicky patronymic), takže Guðmundur Gunnarsson (Guðmundur, syn Gunnara) má dceru Björk Guðmundsdóttir (Björk, dcera Guðmundura), a v oficiálním jednání chce být oslovována jako slečna Björk.

V Číně a v Maďarsku uvádějí rodinné jméno jako první, tedy 毛泽东 (Mao Ce Tung) je Ce Tung z rodiny Mao, podobně Balaton Zoltán je Zoltán z rodiny Balatonů.

V Nizozemí mívají jako součást příjmení předpony van der, např. Rob van der Meer, ale při řazení v telefonním seznamu je vynechávají, takže bude zařazen pod M.

Španělé mají dvě příjmení, první dědí z prvního příjmení otce a druhé dědí z prvního příjmení matky. Ženy si po sňatku ponechají první příjmení a druhé si změní na první manželovo s předponou de, takže když si muž Manuel Pérez Quiñones vezme ženu jménem María Padilla Falto, jí se změní jméno na María Padilla de Pérez a jejich dítě se bude jmenovat třeba Jose Pérez Padilla. Jinak řečeno, děti dědí příjmení po obou dědečcích.

V jižní Indii, Malajsii a Indonésii lidé často mají jen jedno jméno, nemají vůbec příjmení ani nic jiného. Nemají tedy co vyplnit do formuláře požadujícího druhou část jména.

Mnoho jmen, třebaže jsou psaná latinkou, obsahují jiné znaky než písmena, třeba Daniel O'Connell,

U jmen psaných jinou abecedou než latinkou může být třeba evidovat dvě formy, v původní abecedě a přepis do latinky, viz již zmíněný 毛泽东, protože automatický přepis nemusí být proveditelný. Japonská jména potřebují ještě speciální pole pro zápis výslovnosti, protože výslovnost znaků není vždy známa, a řazení v japonských seznamech probíhá podle výslovnosti.

Navíc třeba arabská jména mohou být strukturálně velmi složitá, obsahující jak osobní jméno ("first name"), tak vztah k rodičům (otci) a/nebo příslušnost ke klanu (rodu). např. současný saúdský král se jmenuje v přepisu do latinky Salman bin Abdulaziz Al Saud, tedy Salmán, syn Adbulazize z rodu Saúdů. Plné jméno jeho otce bylo Abdulaziz ibn Abdul Rahman ibn Faisal ibn Turki ibn Abdullah ibn Muhammad Al Saud. V případě jazyků a jmen nepsaných latinkou pak řadu problémů působí již samotná transkripce (nebo transliterace) do latinky. Přepisování se buďto řídí mezinárodními standardy (např. ALA) nebo národními zvyklostmi dle jazykového prostředí.

Formuláře by tedy měly obsahovat pole pro původní celé jméno, pole pro celé jméno přepsané do latinky a případně další pole pro jméno rozložené na části.

Regulární výrazy

Množiny řetězců lze detekovat pomocí tzv. regulárních výrazů. (Ty ale v dnešní době mají větší popisnou sílu než regulární jazyky a konečné automaty, takže se doporučuje je označovat slovem regex).

V Javě lze regexy detekovat pomocí třídy Pattern, která podporuje rozšířené množiny znaků. Sekvence českých znaků lze detekovat pomocí

 
String s = "příliš žlutoučký kůň";
Pattern pattern = Pattern.compile("[\\p{InBASIC_LATIN}\\p{InLATIN_EXTENDED_A}\\p{InLATIN_1_SUPPLEMENT}]*");
if (pattern.matcher(s).matches()) {
 //...
}

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řídu 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říklad potřebujeme zobrazit celá i reálná čísla a skloňovat:

 
Locale kultura = new Locale("cs", "CZ");
String sablonaTextu = "Blok  {0,number,integer}  {1,choice,0#nemá turbíny|1#má 1 turbínu|2#má {1} turbíny|4<má {1} turbín } a je na výkonu  {2,number,#,##0.0##}  MW";
MessageFormat veta = new MessageFormat(sablonaTextu,kultura);
for (int i = 0; i < 10; i++) {
    System.out.println(veta.format(new Object[]{998+i, i, 12345.6789d}));
}

produkuje správně skloňované texty:

Blok  998  nemá turbíny a je na výkonu  12 345,679  MW
Blok  999  má 1 turbínu a je na výkonu  12 345,679  MW
Blok  1 000  má 2 turbíny a je na výkonu  12 345,679  MW
Blok  1 001  má 3 turbíny a je na výkonu  12 345,679  MW
Blok  1 002  má 4 turbíny a je na výkonu  12 345,679  MW
Blok  1 003  má 5 turbín  a je na výkonu  12 345,679  MW
Blok  1 004  má 6 turbín  a je na výkonu  12 345,679  MW
Blok  1 005  má 7 turbín  a je na výkonu  12 345,679  MW
Blok  1 006  má 8 turbín  a je na výkonu  12 345,679  MW
Blok  1 007  má 9 turbín  a je na výkonu  12 345,679  MW

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 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 6.3) je přiřazeno 110187 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

Detekce skupin znaků

Třída Character umožňuje práci se všemi UNICODE znaky.

Například, pokud potřebujeme zdetekovat, zda řetězec používá pouze latinku včetně diakritiky, ale ne jiné abecedy, můžeme to udělat takto:

 
import static java.lang.Character.UnicodeBlock;
//...
 
static boolean isExtendedLatin(String s) {
        for (Character ch : s.toCharArray()) {
            UnicodeBlock block = UnicodeBlock.of(ch);
            if (block != UnicodeBlock.BASIC_LATIN
                    && block != UnicodeBlock.LATIN_1_SUPPLEMENT
                    && block != UnicodeBlock.LATIN_EXTENDED_A
                    && block != UnicodeBlock.LATIN_EXTENDED_B
                    && block != UnicodeBlock.LATIN_EXTENDED_C
                    && block != UnicodeBlock.LATIN_EXTENDED_D
                    && block != UnicodeBlock.LATIN_EXTENDED_ADDITIONAL
                    ) {
                return false;
            }
        }
        return true;
    }

S pomocí této metody dokážeme např. rozdělit jména na psaná latinkou a ostatní, kód

 
    System.out.println("Jiří Šídlo "+isExtendedLatin("Jiří Šídlo"));
    System.out.println("Björk Guðmundsdóttir "+isExtendedLatin("Björk Guðmundsdóttir"));
    System.out.println("Петренко Анатолій "+isExtendedLatin("Петренко Анатолій"));
    System.out.println("Ευάγγελος Αγγέλου "+isExtendedLatin("Ευάγγελος Αγγέλου"));

vypíše

Jiří Šídlo true
Björk Guðmundsdóttir true
Петренко Анатолій false
Ευάγγελος Αγγέλου false

Odstranění diakritiky

 
String s_háčky = "Příliš žlutoučký kůň úpěl ďábelské ódy.";
String odháčkováno = Normalizer.normalize(s_háčky, Normalizer.Form.NFD).replaceAll("[^\\w\\s\\.]", "");
System.out.println(s_háčky);
System.out.println(odháčkováno);

vypíše

Příliš žlutoučký kůň úpěl ďábelské ódy.
Prilis zlutoucky kun upel dabelske ody.

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 InputStream (resp. OutputStream) 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}" type="both" 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.